diff --git a/v2/client.go b/v2/client.go index c6a305f..222a7ae 100644 --- a/v2/client.go +++ b/v2/client.go @@ -38,15 +38,16 @@ type ( initOnce sync.Once // Specific resources - contacts *ContactsResource - devicePosture *DevicePostureResource - devices *DevicesResource - dns *DNSResource - keys *KeysResource - logging *LoggingResource - policyFile *PolicyFileResource - users *UsersResource - webhooks *WebhooksResource + contacts *ContactsResource + devicePosture *DevicePostureResource + devices *DevicesResource + dns *DNSResource + keys *KeysResource + logging *LoggingResource + policyFile *PolicyFileResource + tailnetSettings *TailnetSettingsResource + users *UsersResource + webhooks *WebhooksResource } // APIError type describes an error as returned by the Tailscale API. @@ -105,6 +106,7 @@ func (c *Client) init() { c.keys = &KeysResource{c} c.logging = &LoggingResource{c} c.policyFile = &PolicyFileResource{c} + c.tailnetSettings = &TailnetSettingsResource{c} c.users = &UsersResource{c} c.webhooks = &WebhooksResource{c} }) @@ -159,6 +161,11 @@ func (c *Client) PolicyFile() *PolicyFileResource { return c.policyFile } +func (c *Client) TailnetSettings() *TailnetSettingsResource { + c.init() + return c.tailnetSettings +} + func (c *Client) Users() *UsersResource { c.init() return c.users diff --git a/v2/tailnet_settings.go b/v2/tailnet_settings.go new file mode 100644 index 0000000..5b8926a --- /dev/null +++ b/v2/tailnet_settings.go @@ -0,0 +1,76 @@ +package tsclient + +import ( + "context" + "net/http" +) + +// TailnetSettingsResource provides an API to view/control settings for a tailnet. +type TailnetSettingsResource struct { + *Client +} + +type ( + // TailnetSettings represents the current settings of a tailnet. + // See https://tailscale.com/api#model/tailnetsettings. + 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. +// See https://tailscale.com/api#tag/tailnetsettings/GET/tailnet/{tailnet}/settings. +func (tsr *TailnetSettingsResource) Get(ctx context.Context) (*TailnetSettings, error) { + req, err := tsr.buildRequest(ctx, http.MethodGet, tsr.buildTailnetURL("settings")) + if err != nil { + return nil, err + } + + var resp TailnetSettings + return &resp, tsr.do(req, &resp) +} + +// Update updates the tailnet settings. +// See https://tailscale.com/api#tag/tailnetsettings/PATCH/tailnet/{tailnet}/settings. +func (tsr *TailnetSettingsResource) Update(ctx context.Context, request UpdateTailnetSettingsRequest) error { + req, err := tsr.buildRequest(ctx, http.MethodPatch, tsr.buildTailnetURL("settings"), requestBody(request)) + if err != nil { + return err + } + + return tsr.do(req, nil) +} diff --git a/v2/tailnet_settings_test.go b/v2/tailnet_settings_test.go new file mode 100644 index 0000000..ba9c2d3 --- /dev/null +++ b/v2/tailnet_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_TailnetSettings_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.TailnetSettings().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_TailnetSettings_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.TailnetSettings().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) +}