diff --git a/keys.go b/keys.go index 1513f0c..11d9e3e 100644 --- a/keys.go +++ b/keys.go @@ -45,21 +45,71 @@ type createOAuthClientWithKeyTypeRequest struct { CreateOAuthClientRequest } +// SetOAuthClientRequest describes the definition of an existing OAuth client to +// set (wholesale update) the configuration of. +type SetOAuthClientRequest struct { + Scopes []string `json:"scopes"` + Tags []string `json:"tags"` + Description string `json:"description"` +} + +type setOAuthClientWithKeyTypeRequest struct { + KeyType string `json:"keyType"` + SetOAuthClientRequest +} + +// CreateFederatedIdentityRequest describes the definition of a federated identity to create. +type CreateFederatedIdentityRequest struct { + Scopes []string `json:"scopes"` + Tags []string `json:"tags"` + Audience string `json:"audience"` + Issuer string `json:"issuer"` + Subject string `json:"subject"` + CustomClaimRules map[string]string `json:"customClaimRules"` + Description string `json:"description"` +} + +type createFederatedIdentityWithKeyTypeRequest struct { + KeyType string `json:"keyType"` + CreateFederatedIdentityRequest +} + +// SetFederatedIdentityRequest describes the definition of a federated identity to create. +type SetFederatedIdentityRequest struct { + Scopes []string `json:"scopes"` + Tags []string `json:"tags"` + Audience string `json:"audience"` + Issuer string `json:"issuer"` + Subject string `json:"subject"` + CustomClaimRules map[string]string `json:"customClaimRules"` + Description string `json:"description"` +} + +type setFederatedIdentityWithKeyTypeRequest struct { + KeyType string `json:"keyType"` + SetFederatedIdentityRequest +} + // Key describes an authentication key within the tailnet. type Key struct { - ID string `json:"id"` - KeyType string `json:"keyType"` - Key string `json:"key"` - Description string `json:"description"` - ExpirySeconds *time.Duration `json:"expirySeconds"` - Created time.Time `json:"created"` - Expires time.Time `json:"expires"` - Revoked time.Time `json:"revoked"` - Invalid bool `json:"invalid"` - Capabilities KeyCapabilities `json:"capabilities"` - Scopes []string `json:"scopes,omitempty"` - Tags []string `json:"tags,omitempty"` - UserID string `json:"userId"` + ID string `json:"id"` + KeyType string `json:"keyType"` + Key string `json:"key"` + Description string `json:"description"` + ExpirySeconds *time.Duration `json:"expirySeconds"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Expires time.Time `json:"expires"` + Revoked time.Time `json:"revoked"` + Invalid bool `json:"invalid"` + Capabilities KeyCapabilities `json:"capabilities"` + Scopes []string `json:"scopes,omitempty"` + Tags []string `json:"tags,omitempty"` + UserID string `json:"userId"` + Audience string `json:"audience"` + Issuer string `json:"issuer"` + Subject string `json:"subject"` + CustomClaimRules map[string]string `json:"customClaimRules"` } // Create creates a new authentication key. Returns the generated [Key] if successful. @@ -91,6 +141,45 @@ func (kr *KeysResource) CreateOAuthClient(ctx context.Context, ckr CreateOAuthCl return body[Key](kr, req) } +// SetOAuthClient sets the configuration for an existing OAuth client. Returns the generated [Key] if successful. +func (kr *KeysResource) SetOAuthClient(ctx context.Context, id string, skr SetOAuthClientRequest) (*Key, error) { + req, err := kr.buildRequest(ctx, http.MethodPut, kr.buildTailnetURL("keys", id), requestBody(setOAuthClientWithKeyTypeRequest{ + KeyType: "client", + SetOAuthClientRequest: skr, + })) + if err != nil { + return nil, err + } + + return body[Key](kr, req) +} + +// CreateFederatedIdentity creates a new federated identity. Returns the generated [Key] if successful. +func (kr *KeysResource) CreateFederatedIdentity(ctx context.Context, ckr CreateFederatedIdentityRequest) (*Key, error) { + req, err := kr.buildRequest(ctx, http.MethodPost, kr.buildTailnetURL("keys"), requestBody(createFederatedIdentityWithKeyTypeRequest{ + KeyType: "federated", + CreateFederatedIdentityRequest: ckr, + })) + if err != nil { + return nil, err + } + + return body[Key](kr, req) +} + +// SetFederatedIdentity sets the configuration for an existing federated identity. Returns the generated [Key] if successful. +func (kr *KeysResource) SetFederatedIdentity(ctx context.Context, id string, skr SetFederatedIdentityRequest) (*Key, error) { + req, err := kr.buildRequest(ctx, http.MethodPut, kr.buildTailnetURL("keys", id), requestBody(setFederatedIdentityWithKeyTypeRequest{ + KeyType: "federated", + SetFederatedIdentityRequest: skr, + })) + if err != nil { + return nil, err + } + + return body[Key](kr, req) +} + // Get returns all information on a [Key] whose identifier matches the one provided. This will not return the // authentication key itself, just the metadata. func (kr *KeysResource) Get(ctx context.Context, id string) (*Key, error) { diff --git a/keys_test.go b/keys_test.go index 0ffaf69..04b864a 100644 --- a/keys_test.go +++ b/keys_test.go @@ -184,6 +184,141 @@ func TestClient_CreateOAuthClient(t *testing.T) { assert.EqualValues(t, "", actualReq.Description) } +func TestClient_SetOAuthClient(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + expected := &Key{ + ID: "test", + KeyType: "client", + Key: "thisisatestclient", + ExpirySeconds: nil, + Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + Description: "", + } + + server.ResponseBody = expected + + actual, err := client.Keys().SetOAuthClient(context.Background(), "test", SetOAuthClientRequest{ + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + }) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + assert.Equal(t, http.MethodPut, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/keys/test", server.Path) + + var actualReq createOAuthClientWithKeyTypeRequest + assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq)) + assert.EqualValues(t, "client", actualReq.KeyType) + assert.EqualValues(t, 1, len(actualReq.Scopes)) + assert.EqualValues(t, "all:read", actualReq.Scopes[0]) + assert.EqualValues(t, 1, len(actualReq.Tags)) + assert.EqualValues(t, "tag:test", actualReq.Tags[0]) + assert.EqualValues(t, "", actualReq.Description) +} + +func TestClient_CreateFederatedIdentity(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + expected := &Key{ + ID: "test", + KeyType: "federated", + Key: "thisisatestclient", + ExpirySeconds: nil, + Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + Description: "", + Audience: "exampleaud", + Subject: "examplesubject", + CustomClaimRules: map[string]string{ + "foo": "bar", + }, + } + + server.ResponseBody = expected + + actual, err := client.Keys().CreateFederatedIdentity(context.Background(), CreateFederatedIdentityRequest{ + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + Subject: "examplesubject", + CustomClaimRules: map[string]string{ + "foo": "bar", + }, + }) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + assert.Equal(t, http.MethodPost, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/keys", server.Path) + + var actualReq createFederatedIdentityWithKeyTypeRequest + assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq)) + assert.EqualValues(t, "federated", actualReq.KeyType) + assert.EqualValues(t, 1, len(actualReq.Scopes)) + assert.EqualValues(t, "all:read", actualReq.Scopes[0]) + assert.EqualValues(t, 1, len(actualReq.Tags)) + assert.EqualValues(t, "tag:test", actualReq.Tags[0]) + assert.EqualValues(t, "", actualReq.Description) +} + +func TestClient_SetFederatedIdentity(t *testing.T) { + t.Parallel() + + client, server := NewTestHarness(t) + server.ResponseCode = http.StatusOK + + expected := &Key{ + ID: "test", + KeyType: "federated", + Key: "thisisatestclient", + ExpirySeconds: nil, + Created: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Expires: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + Description: "", + Audience: "exampleaud", + Subject: "examplesubject", + CustomClaimRules: map[string]string{ + "foo": "bar", + }, + } + + server.ResponseBody = expected + + actual, err := client.Keys().SetFederatedIdentity(context.Background(), "test", SetFederatedIdentityRequest{ + Scopes: []string{"all:read"}, + Tags: []string{"tag:test"}, + Subject: "examplesubject", + CustomClaimRules: map[string]string{ + "foo": "bar", + }, + }) + assert.NoError(t, err) + assert.EqualValues(t, expected, actual) + assert.Equal(t, http.MethodPut, server.Method) + assert.Equal(t, "/api/v2/tailnet/example.com/keys/test", server.Path) + + var actualReq createFederatedIdentityWithKeyTypeRequest + assert.NoError(t, json.Unmarshal(server.Body.Bytes(), &actualReq)) + assert.EqualValues(t, "federated", actualReq.KeyType) + assert.EqualValues(t, 1, len(actualReq.Scopes)) + assert.EqualValues(t, "all:read", actualReq.Scopes[0]) + assert.EqualValues(t, 1, len(actualReq.Tags)) + assert.EqualValues(t, "tag:test", actualReq.Tags[0]) + assert.EqualValues(t, "", actualReq.Description) +} + func TestClient_GetKey(t *testing.T) { t.Parallel()