diff --git a/go.mod b/go.mod index 53e7369566..b451a33bdd 100644 --- a/go.mod +++ b/go.mod @@ -295,3 +295,4 @@ require ( sigs.k8s.io/yaml v1.3.0 // indirect ) +replace github.com/confluentinc/ccloud-sdk-go-v2/org => github.com/confluentinc/ccloud-sdk-go-v2-internal/org v0.2.0 diff --git a/go.sum b/go.sum index ca9b563dde..8b9ff610b7 100644 --- a/go.sum +++ b/go.sum @@ -190,10 +190,8 @@ github.com/compose-spec/compose-go/v2 v2.1.3 h1:bD67uqLuL/XgkAK6ir3xZvNLFPxPScEi github.com/compose-spec/compose-go/v2 v2.1.3/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52 h1:19qEGhkbZa5fopKCe0VPIV+Sasby4Pv10z9ZaktwWso= github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52/go.mod h1:62EMf+5uFEt1BJ2q8WMrUoI9VUSxAbDnmZCGRt/MbA0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/networking-access-point v0.13.0 h1:crG9ZKL8WlY7fX1ooSsODyNKSLhWuOpvThQ/GYlDjK0= -github.com/confluentinc/ccloud-sdk-go-v2-internal/networking-access-point v0.13.0/go.mod h1:94S6dsLwNBa6zumSRQi81/aM750kLu+HJFXd4jcJz7A= -github.com/confluentinc/ccloud-sdk-go-v2-internal/networking-gateway v0.13.0 h1:RDHQh6GcaL6JrZock9jsPQCUiwHNNZBiJOrXt7/v2lw= -github.com/confluentinc/ccloud-sdk-go-v2-internal/networking-gateway v0.13.0/go.mod h1:xr1v1dpdLoFSozNL4Qiv4gNjVlaTgr7mBMVnfjtzdas= +github.com/confluentinc/ccloud-sdk-go-v2-internal/org v0.2.0 h1:M2CN2vnXsgsi+mxS9Mbq8NQdi/dfRE0ooYVFrZ81R3s= +github.com/confluentinc/ccloud-sdk-go-v2-internal/org v0.2.0/go.mod h1:G9+rxKPBSPWLEbeYBGLlvt4DzPjifsYBRkDog8sCcRk= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0 h1:zSF4OQUJXWH2JeAo9rsq13ibk+JFdzITGR8S7cFMpzw= github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0/go.mod h1:DoxqzzF3JzvJr3fWkvCiOHFlE0GoYpozWxFZ1Ud9ntA= github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0 h1:8fWyLwMuy8ec0MVF5Avd54UvbIxhDFhZzanHBVwgxdw= @@ -254,8 +252,6 @@ github.com/confluentinc/ccloud-sdk-go-v2/networking-ip v0.2.0 h1:ZHNF2DeqVlNPuKG github.com/confluentinc/ccloud-sdk-go-v2/networking-ip v0.2.0/go.mod h1:KTShFBZA7WG8LcxlWjJpoZFdWkJ+uOw3dDuwAHs5eKU= github.com/confluentinc/ccloud-sdk-go-v2/networking-privatelink v0.3.0 h1:mC0E1nKUt57AxMM4Lpdfd+KA/YZwJVwro9ER+dCUFi8= github.com/confluentinc/ccloud-sdk-go-v2/networking-privatelink v0.3.0/go.mod h1:GIHF2cYOUKx+6ycYokr4i8E4cuNBC22xqvO/IhqZ31U= -github.com/confluentinc/ccloud-sdk-go-v2/org v0.9.0 h1:FtaqHX0kBTK7fCQK+9SJcOso+XpWCWzY2roT3gBQGfw= -github.com/confluentinc/ccloud-sdk-go-v2/org v0.9.0/go.mod h1:X0uaTYPp+mr19W1R/Z1LuB1ePZJZrH7kxnQckDx6zoc= github.com/confluentinc/ccloud-sdk-go-v2/provider-integration v0.2.0 h1:UN2a+aqYhk95ro+wVLkeB/8W7n+UV2KsE3jNFbbDCSw= github.com/confluentinc/ccloud-sdk-go-v2/provider-integration v0.2.0/go.mod h1:TzompS9F0G6awN5xMC+nguNG8ULElN5UqX2XOBOIPuM= github.com/confluentinc/ccloud-sdk-go-v2/service-quota v0.2.0 h1:xVSmwdycExze1E2Jta99CaFuMOlL6k6KExOOSY1hSFg= diff --git a/internal/organization/command.go b/internal/organization/command.go index 557a924eb7..0826e7492f 100644 --- a/internal/organization/command.go +++ b/internal/organization/command.go @@ -23,6 +23,7 @@ func New(prerunner pcmd.PreRunner) *cobra.Command { cmd.AddCommand(c.newDescribeCommand()) cmd.AddCommand(c.newListCommand()) cmd.AddCommand(c.newUpdateCommand()) + cmd.AddCommand(c.newScimTokenCommand()) return cmd } diff --git a/internal/organization/command_scim_token.go b/internal/organization/command_scim_token.go new file mode 100644 index 0000000000..4af7192143 --- /dev/null +++ b/internal/organization/command_scim_token.go @@ -0,0 +1,36 @@ +package organization + +import ( + "time" + + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" +) + +type scimTokenCommand struct { + *pcmd.AuthenticatedCLICommand +} + +func (c *command) newScimTokenCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "scim-token", + Short: "Manage SCIM tokens.", + Long: "Manage SCIM tokens for the current organization.", + } + + scimCmd := &scimTokenCommand{c.AuthenticatedCLICommand} + + cmd.AddCommand(scimCmd.newCreateCommand()) + cmd.AddCommand(scimCmd.newListCommand()) + cmd.AddCommand(scimCmd.newDeleteCommand()) + + return cmd +} + +func formatTimestamp(ts *time.Time) string { + if ts == nil { + return "" + } + return ts.UTC().Format(time.RFC3339) +} diff --git a/internal/organization/command_scim_token_create.go b/internal/organization/command_scim_token_create.go new file mode 100644 index 0000000000..a79ed9a45f --- /dev/null +++ b/internal/organization/command_scim_token_create.go @@ -0,0 +1,61 @@ +package organization + +import ( + "github.com/spf13/cobra" + + orgv2 "github.com/confluentinc/ccloud-sdk-go-v2/org/v2" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" +) + +type scimTokenCreateOut struct { + Id string `human:"ID" serialized:"id"` + Token string `human:"Token" serialized:"token"` + CreatedAt string `human:"Created At" serialized:"created_at"` + ExpiresAt string `human:"Expires At" serialized:"expires_at"` +} + +func (c *scimTokenCommand) newCreateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a SCIM token.", + Long: "Create a SCIM token for the current organization.\n\nSave the token as it is not retrievable later.", + Args: cobra.NoArgs, + RunE: c.create, + } + + cmd.Flags().Int32("expire-duration-mins", 0, "Token expiration duration in minutes. Defaults to 6 months if not specified.") + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *scimTokenCommand) create(cmd *cobra.Command, _ []string) error { + token := orgv2.InlineObject{} + + // Only set expiration duration if explicitly provided + if cmd.Flags().Changed("expire-duration-mins") { + expireDurationMins, err := cmd.Flags().GetInt32("expire-duration-mins") + if err != nil { + return err + } + token.ExpireDurationMins = orgv2.PtrInt32(expireDurationMins) + } + + createdToken, err := c.V2Client.CreateScimToken(token) + if err != nil { + return err + } + + output.Println(c.Config.EnableColor, "Save the token as it is not retrievable later.") + + table := output.NewTable(cmd) + table.Add(&scimTokenCreateOut{ + Id: createdToken.GetId(), + Token: createdToken.GetToken(), + CreatedAt: formatTimestamp(createdToken.CreatedAt), + ExpiresAt: formatTimestamp(createdToken.ExpiresAt), + }) + return table.Print() +} diff --git a/internal/organization/command_scim_token_delete.go b/internal/organization/command_scim_token_delete.go new file mode 100644 index 0000000000..205abe8d9d --- /dev/null +++ b/internal/organization/command_scim_token_delete.go @@ -0,0 +1,35 @@ +package organization + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/deletion" + "github.com/confluentinc/cli/v4/pkg/resource" +) + +func (c *scimTokenCommand) newDeleteCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a SCIM token.", + Long: "Delete a SCIM token for the current organization.", + Args: cobra.ExactArgs(1), + RunE: c.delete, + } + + pcmd.AddForceFlag(cmd) + + return cmd +} + +func (c *scimTokenCommand) delete(cmd *cobra.Command, args []string) error { + if err := deletion.ValidateAndConfirm(cmd, args, func(id string) bool { return true }, resource.ScimToken); err != nil { + return err + } + + _, err := deletion.Delete(cmd, args, func(tokenId string) error { + return c.V2Client.DeleteScimToken(tokenId) + }, resource.ScimToken) + + return err +} diff --git a/internal/organization/command_scim_token_list.go b/internal/organization/command_scim_token_list.go new file mode 100644 index 0000000000..17f479ccc5 --- /dev/null +++ b/internal/organization/command_scim_token_list.go @@ -0,0 +1,45 @@ +package organization + +import ( + "github.com/spf13/cobra" + + pcmd "github.com/confluentinc/cli/v4/pkg/cmd" + "github.com/confluentinc/cli/v4/pkg/output" +) + +type scimTokenListOut struct { + Id string `human:"ID" serialized:"id"` + CreatedAt string `human:"Created At" serialized:"created_at"` + ExpiresAt string `human:"Expires At" serialized:"expires_at"` +} + +func (c *scimTokenCommand) newListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List SCIM tokens.", + Long: "List SCIM tokens for the current organization.", + Args: cobra.NoArgs, + RunE: c.list, + } + + pcmd.AddOutputFlag(cmd) + + return cmd +} + +func (c *scimTokenCommand) list(cmd *cobra.Command, _ []string) error { + tokens, err := c.V2Client.ListScimTokens() + if err != nil { + return err + } + + list := output.NewList(cmd) + for _, token := range tokens { + list.Add(&scimTokenListOut{ + Id: token.GetId(), + CreatedAt: formatTimestamp(token.CreatedAt), + ExpiresAt: formatTimestamp(token.ExpiresAt), + }) + } + return list.Print() +} diff --git a/pkg/ccloudv2/scim.go b/pkg/ccloudv2/scim.go new file mode 100644 index 0000000000..b5508ecb24 --- /dev/null +++ b/pkg/ccloudv2/scim.go @@ -0,0 +1,49 @@ +package ccloudv2 + +import ( + "net/http" + + orgv2 "github.com/confluentinc/ccloud-sdk-go-v2/org/v2" + + "github.com/confluentinc/cli/v4/pkg/errors" +) + +// SCIM token API calls + +func (c *Client) CreateScimToken(token orgv2.InlineObject) (orgv2.OrgV2ScimToken, error) { + resp, httpResp, err := c.OrgClient.ScimTokensOrgV2Api.CreateOrgV2ScimToken(c.orgApiContext()).InlineObject(token).Execute() + return resp, errors.CatchCCloudV2Error(err, httpResp) +} + +func (c *Client) ListScimTokens() ([]orgv2.OrgV2ScimToken, error) { + var list []orgv2.OrgV2ScimToken + + done := false + pageToken := "" + for !done { + page, httpResp, err := c.executeListScimTokens(pageToken) + if err != nil { + return nil, errors.CatchCCloudV2Error(err, httpResp) + } + list = append(list, page.GetData()...) + + pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) + if err != nil { + return nil, err + } + } + return list, nil +} + +func (c *Client) executeListScimTokens(pageToken string) (orgv2.OrgV2ScimTokenList, *http.Response, error) { + req := c.OrgClient.ScimTokensOrgV2Api.ListOrgV2ScimTokens(c.orgApiContext()).PageSize(ccloudV2ListPageSize) + if pageToken != "" { + req = req.PageToken(pageToken) + } + return req.Execute() +} + +func (c *Client) DeleteScimToken(id string) error { + httpResp, err := c.OrgClient.ScimTokensOrgV2Api.DeleteOrgV2ScimToken(c.orgApiContext(), id).Execute() + return errors.CatchCCloudV2Error(err, httpResp) +} diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 57de4a56c8..87166f167a 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -76,6 +76,7 @@ const ( SchemaExporter = "schema exporter" SchemaRegistryCluster = "Schema Registry cluster" SchemaRegistryConfiguration = "Schema Registry configuration" + ScimToken = "SCIM token" ServiceAccount = "service account" SsoGroupMapping = "SSO group mapping" Tableflow = "tableflow" diff --git a/test/fixtures/output/organization/scim-token-create-custom-expiration.golden b/test/fixtures/output/organization/scim-token-create-custom-expiration.golden new file mode 100644 index 0000000000..05f7246b9a --- /dev/null +++ b/test/fixtures/output/organization/scim-token-create-custom-expiration.golden @@ -0,0 +1,7 @@ +Save the token as it is not retrievable later. ++------------+--------------------------------------+ +| ID | scim_token-5 | +| Token | scim_secret_token_value_scim_token-5 | +| Created At | 2026-02-12T10:58:00Z | +| Expires At | 2026-03-14T10:58:00Z | ++------------+--------------------------------------+ diff --git a/test/fixtures/output/organization/scim-token-create-json.golden b/test/fixtures/output/organization/scim-token-create-json.golden new file mode 100644 index 0000000000..aa80cdeaad --- /dev/null +++ b/test/fixtures/output/organization/scim-token-create-json.golden @@ -0,0 +1,7 @@ +Save the token as it is not retrievable later. +{ + "id": "scim_token-4", + "token": "scim_secret_token_value_scim_token-4", + "created_at": "2026-02-12T10:57:59Z", + "expires_at": "2026-08-12T10:57:59Z" +} diff --git a/test/fixtures/output/organization/scim-token-create.golden b/test/fixtures/output/organization/scim-token-create.golden new file mode 100644 index 0000000000..b7dcae3cb9 --- /dev/null +++ b/test/fixtures/output/organization/scim-token-create.golden @@ -0,0 +1,7 @@ +Save the token as it is not retrievable later. ++------------+--------------------------------------+ +| ID | scim_token-3 | +| Token | scim_secret_token_value_scim_token-3 | +| Created At | 2026-02-12T10:57:58Z | +| Expires At | 2026-08-12T10:57:58Z | ++------------+--------------------------------------+ diff --git a/test/fixtures/output/organization/scim-token-delete-not-found.golden b/test/fixtures/output/organization/scim-token-delete-not-found.golden new file mode 100644 index 0000000000..0411184607 --- /dev/null +++ b/test/fixtures/output/organization/scim-token-delete-not-found.golden @@ -0,0 +1 @@ +Error: failed to delete nonexistent: SCIM token not found diff --git a/test/fixtures/output/organization/scim-token-delete-prompt.golden b/test/fixtures/output/organization/scim-token-delete-prompt.golden new file mode 100644 index 0000000000..1a36ae16b8 --- /dev/null +++ b/test/fixtures/output/organization/scim-token-delete-prompt.golden @@ -0,0 +1 @@ +Are you sure you want to delete SCIM token "scim_token-67890"? (y/n): Deleted SCIM token "scim_token-67890". diff --git a/test/fixtures/output/organization/scim-token-delete.golden b/test/fixtures/output/organization/scim-token-delete.golden new file mode 100644 index 0000000000..de67417f3e --- /dev/null +++ b/test/fixtures/output/organization/scim-token-delete.golden @@ -0,0 +1 @@ +Deleted SCIM token "scim_token-12345". diff --git a/test/fixtures/output/organization/scim-token-list-json.golden b/test/fixtures/output/organization/scim-token-list-json.golden new file mode 100644 index 0000000000..c9a9c4b03e --- /dev/null +++ b/test/fixtures/output/organization/scim-token-list-json.golden @@ -0,0 +1,27 @@ +[ + { + "id": "scim_token-12345", + "created_at": "2025-01-01T01:00:00Z", + "expires_at": "2026-01-01T01:00:00Z" + }, + { + "id": "scim_token-3", + "created_at": "2026-02-12T10:57:58Z", + "expires_at": "2026-08-12T10:57:58Z" + }, + { + "id": "scim_token-4", + "created_at": "2026-02-12T10:57:59Z", + "expires_at": "2026-08-12T10:57:59Z" + }, + { + "id": "scim_token-5", + "created_at": "2026-02-12T10:58:00Z", + "expires_at": "2026-03-14T10:58:00Z" + }, + { + "id": "scim_token-67890", + "created_at": "2025-02-01T01:00:00Z", + "expires_at": "2026-02-01T01:00:00Z" + } +] diff --git a/test/fixtures/output/organization/scim-token-list.golden b/test/fixtures/output/organization/scim-token-list.golden new file mode 100644 index 0000000000..add1812edd --- /dev/null +++ b/test/fixtures/output/organization/scim-token-list.golden @@ -0,0 +1,7 @@ + ID | Created At | Expires At +-------------------+----------------------+----------------------- + scim_token-12345 | 2025-01-01T01:00:00Z | 2026-01-01T01:00:00Z + scim_token-3 | 2026-02-12T10:57:58Z | 2026-08-12T10:57:58Z + scim_token-4 | 2026-02-12T10:57:59Z | 2026-08-12T10:57:59Z + scim_token-5 | 2026-02-12T10:58:00Z | 2026-03-14T10:58:00Z + scim_token-67890 | 2025-02-01T01:00:00Z | 2026-02-01T01:00:00Z diff --git a/test/organization_scim_test.go b/test/organization_scim_test.go new file mode 100644 index 0000000000..f62f14b398 --- /dev/null +++ b/test/organization_scim_test.go @@ -0,0 +1,20 @@ +package test + +func (s *CLITestSuite) TestOrganizationSCIM() { + tests := []CLITest{ + {args: "organization scim-token create", fixture: "organization/scim-token-create.golden"}, + {args: "organization scim-token create -o json", fixture: "organization/scim-token-create-json.golden"}, + {args: "organization scim-token create --expire-duration-mins 43200", fixture: "organization/scim-token-create-custom-expiration.golden"}, + {args: "organization scim-token list", fixture: "organization/scim-token-list.golden"}, + {args: "organization scim-token list -o json", fixture: "organization/scim-token-list-json.golden"}, + {args: "organization scim-token delete scim_token-12345 --force", fixture: "organization/scim-token-delete.golden"}, + {args: "organization scim-token delete scim_token-67890", input: "y\n", fixture: "organization/scim-token-delete-prompt.golden"}, + {args: "organization scim-token delete nonexistent --force", fixture: "organization/scim-token-delete-not-found.golden", exitCode: 1}, + } + + for _, test := range tests { + test.login = "cloud" + test.workflow = true + s.runIntegrationTest(test) + } +} diff --git a/test/test-server/ccloud_handlers.go b/test/test-server/ccloud_handlers.go index e6d87a4410..fd2f07e8b1 100644 --- a/test/test-server/ccloud_handlers.go +++ b/test/test-server/ccloud_handlers.go @@ -74,6 +74,9 @@ func handleMe(t *testing.T, isAuditLogEnabled bool) http.HandlerFunc { Id: 42, ResourceId: organizationId, Name: "Confluent", + Sso: &ccloudv1.Sso{ + Auth0ConnectionName: "op-12345", + }, } if isAuditLogEnabled { org.AuditLog = &ccloudv1.AuditLog{ diff --git a/test/test-server/ccloudv2_router.go b/test/test-server/ccloudv2_router.go index d942681e18..0510271437 100644 --- a/test/test-server/ccloudv2_router.go +++ b/test/test-server/ccloudv2_router.go @@ -146,5 +146,8 @@ func NewV2Router(t *testing.T) *mux.Router { router.HandleFunc(route.path, route.handler(t)) } + // Register SCIM token v2 routes + RegisterSCIMRoutes(router, t) + return router } diff --git a/test/test-server/scim_handlers.go b/test/test-server/scim_handlers.go new file mode 100644 index 0000000000..531ab483e6 --- /dev/null +++ b/test/test-server/scim_handlers.go @@ -0,0 +1,145 @@ +package testserver + +import ( + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" + + orgv2 "github.com/confluentinc/ccloud-sdk-go-v2/org/v2" +) + +// Mock SCIM tokens storage - shared mutable state across all tests +var scimTokenCounter = 3 + +func getInitialSCIMTokensV2() map[string]*orgv2.OrgV2ScimToken { + return map[string]*orgv2.OrgV2ScimToken{ + "scim_token-12345": { + ApiVersion: orgv2.PtrString("org/v2"), + Kind: orgv2.PtrString("ScimToken"), + Id: orgv2.PtrString("scim_token-12345"), + ConnectionName: orgv2.PtrString("op-12345"), + Token: orgv2.PtrString("scim_secret_token_value_12345"), + CreatedAt: orgv2.PtrTime(time.Date(2025, 1, 1, 1, 0, 0, 0, time.UTC)), + ExpiresAt: orgv2.PtrTime(time.Date(2026, 1, 1, 1, 0, 0, 0, time.UTC)), + }, + "scim_token-67890": { + ApiVersion: orgv2.PtrString("org/v2"), + Kind: orgv2.PtrString("ScimToken"), + Id: orgv2.PtrString("scim_token-67890"), + ConnectionName: orgv2.PtrString("op-12345"), + Token: orgv2.PtrString("scim_secret_token_value_67890"), + CreatedAt: orgv2.PtrTime(time.Date(2025, 2, 1, 1, 0, 0, 0, time.UTC)), + ExpiresAt: orgv2.PtrTime(time.Date(2026, 2, 1, 1, 0, 0, 0, time.UTC)), + }, + } +} + +var scimTokensV2 = getInitialSCIMTokensV2() + +// Handler for: POST "/org/v2/scim-tokens" +func handleCreateSCIMTokenV2(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode request body + var request orgv2.InlineObject + require.NoError(t, json.NewDecoder(r.Body).Decode(&request)) + + // Create new token + newTokenId := "scim_token-" + string(rune('0'+scimTokenCounter)) + + // Use fixed timestamps for test consistency, incrementing by 1 second for each new token + now := time.Date(2026, 2, 12, 10, 57, 58, 0, time.UTC).Add(time.Duration(scimTokenCounter-3) * time.Second) + scimTokenCounter++ + + var expiresAt time.Time + + // Use ExpireDurationMins from request if provided, otherwise default to 6 months + if request.ExpireDurationMins != nil && *request.ExpireDurationMins > 0 { + expiresAt = now.Add(time.Duration(*request.ExpireDurationMins) * time.Minute) + } else { + expiresAt = now.AddDate(0, 6, 0) // Default 6 months + } + + token := &orgv2.OrgV2ScimToken{ + ApiVersion: orgv2.PtrString("org/v2"), + Kind: orgv2.PtrString("ScimToken"), + Id: orgv2.PtrString(newTokenId), + ConnectionName: orgv2.PtrString("op-12345"), + Token: orgv2.PtrString("scim_secret_token_value_" + newTokenId), + CreatedAt: orgv2.PtrTime(now), + ExpiresAt: orgv2.PtrTime(expiresAt), + } + + scimTokensV2[newTokenId] = token + + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(token)) + } +} + +// Handler for: GET "/org/v2/scim-tokens" +func handleListSCIMTokensV2(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var tokens []orgv2.OrgV2ScimToken + for _, token := range scimTokensV2 { + // Don't include the token value in list response + tokenCopy := *token + tokenCopy.Token = nil + tokens = append(tokens, tokenCopy) + } + + firstLink := *orgv2.NewNullableString(orgv2.PtrString("https://api.confluent.cloud/org/v2/scim-tokens")) + lastLink := *orgv2.NewNullableString(orgv2.PtrString("https://api.confluent.cloud/org/v2/scim-tokens")) + + reply := orgv2.OrgV2ScimTokenList{ + ApiVersion: "org/v2", + Kind: "ScimTokenList", + Metadata: orgv2.ListMeta{ + First: firstLink, + Last: lastLink, + }, + Data: tokens, + } + + w.Header().Set("Content-Type", "application/json") + require.NoError(t, json.NewEncoder(w).Encode(reply)) + } +} + +// Handler for: DELETE "/org/v2/scim-tokens/{id}" +func handleDeleteSCIMTokenV2(t *testing.T) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + tokenId := vars["id"] + + if _, exists := scimTokensV2[tokenId]; !exists { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + errorResponse := map[string]interface{}{ + "errors": []map[string]interface{}{ + { + "status": "404", + "detail": "SCIM token not found", + }, + }, + } + require.NoError(t, json.NewEncoder(w).Encode(errorResponse)) + return + } + + delete(scimTokensV2, tokenId) + + w.WriteHeader(http.StatusNoContent) + } +} + +// Register SCIM routes +func RegisterSCIMRoutes(router *mux.Router, t *testing.T) { + // SCIM token v2 routes + router.HandleFunc("/org/v2/scim-tokens", handleCreateSCIMTokenV2(t)).Methods("POST") + router.HandleFunc("/org/v2/scim-tokens", handleListSCIMTokensV2(t)).Methods("GET") + router.HandleFunc("/org/v2/scim-tokens/{id}", handleDeleteSCIMTokenV2(t)).Methods("DELETE") +}