diff --git a/v2/client.go b/v2/client.go index c6a305f..a5e3ff7 100644 --- a/v2/client.go +++ b/v2/client.go @@ -45,6 +45,7 @@ type ( keys *KeysResource logging *LoggingResource policyFile *PolicyFileResource + settings *SettingsResource users *UsersResource webhooks *WebhooksResource } @@ -105,6 +106,7 @@ func (c *Client) init() { c.keys = &KeysResource{c} c.logging = &LoggingResource{c} c.policyFile = &PolicyFileResource{c} + c.settings = &SettingsResource{c} c.users = &UsersResource{c} c.webhooks = &WebhooksResource{c} }) @@ -159,6 +161,11 @@ func (c *Client) PolicyFile() *PolicyFileResource { return c.policyFile } +func (c *Client) Settings() *SettingsResource { + c.init() + return c.settings +} + func (c *Client) Users() *UsersResource { c.init() return c.users diff --git a/v2/settings.go b/v2/settings.go new file mode 100644 index 0000000..437202f --- /dev/null +++ b/v2/settings.go @@ -0,0 +1,73 @@ +package tsclient + +import ( + "context" + "net/http" +) + +// SettingsResource provides an API to view/control settings for a tailnet. +type SettingsResource struct { + *Client +} + +type ( + // TailnetSettings represents the current settings of a tailnet. + TailnetSettings struct { + DevicesApprovalOn bool `json:"devicesApprovalOn"` + DevicesAutoUpdatesOn bool `json:"devicesAutoUpdatesOn"` + DevicesKeyDurationDays int `json:"devicesKeyDurationDays"` // days before device key expiry + + UsersApprovalOn bool `json:"usersApprovalOn"` + UsersRoleAllowedToJoinExternalTailnets RoleAllowedToJoinExternalTailnets `json:"usersRoleAllowedToJoinExternalTailnets"` + + NetworkFlowLoggingOn bool `json:"networkFlowLoggingOn"` + RegionalRoutingOn bool `json:"regionalRoutingOn"` + PostureIdentityCollectionOn bool `json:"postureIdentityCollectionOn"` + } + + // UpdateTailnetSettingsRequest is a request to update the settings of a tailnet. + // Nil values indicate that the existing setting should be left unchanged. + UpdateTailnetSettingsRequest struct { + DevicesApprovalOn *bool `json:"devicesApprovalOn,omitempty"` + DevicesAutoUpdatesOn *bool `json:"devicesAutoUpdatesOn,omitempty"` + DevicesKeyDurationDays *int `json:"devicesKeyDurationDays,omitempty"` // days before device key expiry + + UsersApprovalOn *bool `json:"usersApprovalOn,omitempty"` + UsersRoleAllowedToJoinExternalTailnets *RoleAllowedToJoinExternalTailnets `json:"usersRoleAllowedToJoinExternalTailnets,omitempty"` + + NetworkFlowLoggingOn *bool `json:"networkFlowLoggingOn,omitempty"` + RegionalRoutingOn *bool `json:"regionalRoutingOn,omitempty"` + PostureIdentityCollectionOn *bool `json:"postureIdentityCollectionOn,omitempty"` + } + + // RoleAllowedToJoinExternalTailnets constrains which users are allowed to join external tailnets + // based on their role. + RoleAllowedToJoinExternalTailnets string +) + +const ( + RoleAllowedToJoinExternalTailnetsNone RoleAllowedToJoinExternalTailnets = "none" + RoleAllowedToJoinExternalTailnetsAdmin RoleAllowedToJoinExternalTailnets = "admin" + RoleAllowedToJoinExternalTailnetsMember RoleAllowedToJoinExternalTailnets = "member" +) + +// Get retrieves the current TailnetSettings. +func (sr *SettingsResource) Get(ctx context.Context) (*TailnetSettings, error) { + req, err := sr.buildRequest(ctx, http.MethodGet, sr.buildTailnetURL("settings")) + if err != nil { + return nil, err + } + + var resp TailnetSettings + return &resp, sr.do(req, &resp) +} + +// Update updates the tailnet settings. +func (sr *SettingsResource) Update(ctx context.Context, request UpdateTailnetSettingsRequest) error { + req, err := sr.buildRequest(ctx, http.MethodPatch, sr.buildTailnetURL("settings"), requestBody(request)) + if err != nil { + return err + } + + return sr.do(req, nil) +} diff --git a/v2/settings_test.go b/v2/settings_test.go new file mode 100644 index 0000000..17a1ba3 --- /dev/null +++ b/v2/settings_test.go @@ -0,0 +1,63 @@ +package tsclient_test + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + tsclient "github.com/tailscale/tailscale-client-go/v2" +) + +func TestClient_Settings_Get(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + expected := tsclient.TailnetSettings{ + DevicesApprovalOn: true, + DevicesAutoUpdatesOn: true, + DevicesKeyDurationDays: 5, + UsersApprovalOn: true, + UsersRoleAllowedToJoinExternalTailnets: tsclient.RoleAllowedToJoinExternalTailnetsMember, + NetworkFlowLoggingOn: true, + RegionalRoutingOn: true, + PostureIdentityCollectionOn: true, + } + server.ResponseBody = expected + + actual, err := client.Settings().Get(context.Background()) + assert.NoError(t, err) + assert.Equal(t, http.MethodGet, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/settings", server.Path) + assert.Equal(t, &expected, actual) +} + +func TestClient_Settings_Update(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + server.ResponseBody = nil + + updateRequest := tsclient.UpdateTailnetSettingsRequest{ + DevicesApprovalOn: tsclient.PointerTo(true), + DevicesAutoUpdatesOn: tsclient.PointerTo(true), + DevicesKeyDurationDays: tsclient.PointerTo(5), + UsersApprovalOn: tsclient.PointerTo(true), + UsersRoleAllowedToJoinExternalTailnets: tsclient.PointerTo(tsclient.RoleAllowedToJoinExternalTailnetsMember), + NetworkFlowLoggingOn: tsclient.PointerTo(true), + RegionalRoutingOn: tsclient.PointerTo(true), + PostureIdentityCollectionOn: tsclient.PointerTo(true), + } + err := client.Settings().Update(context.Background(), updateRequest) + assert.NoError(t, err) + assert.Equal(t, http.MethodPatch, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/settings", server.Path) + var receivedRequest tsclient.UpdateTailnetSettingsRequest + err = json.Unmarshal(server.Body.Bytes(), &receivedRequest) + assert.NoError(t, err) + assert.EqualValues(t, updateRequest, receivedRequest) +}