Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
github.com/charmbracelet/glamour v0.7.0
github.com/charmbracelet/lipgloss v0.11.0
github.com/client9/gospell v0.0.0-20160306015952-90dfc71015df
github.com/confluentinc/cc-structs/kafka/flow v0.2725.0
github.com/confluentinc/ccloud-sdk-go-v1 v0.0.293
github.com/confluentinc/ccloud-sdk-go-v1-public v0.0.0-20250521223017-0e8f6f971b52
github.com/confluentinc/ccloud-sdk-go-v2/ai v0.1.0
github.com/confluentinc/ccloud-sdk-go-v2/apikeys v0.4.0
Expand Down Expand Up @@ -164,6 +166,25 @@ require (
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/confluentinc/cc-structs/common v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/auth v0.2336.0 // indirect
github.com/confluentinc/cc-structs/kafka/authz v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/billing v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/clusterlink v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/connect v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/controlplanekafka v0.2507.0 // indirect
github.com/confluentinc/cc-structs/kafka/core v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/marketplace v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/metrics v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/org v0.2725.0 // indirect
github.com/confluentinc/cc-structs/kafka/product/core v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/scheduler v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/streamgovernance v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/support v0.1989.0 // indirect
github.com/confluentinc/cc-structs/kafka/util v0.1989.0 // indirect
github.com/confluentinc/cc-structs/operator v0.2413.0 // indirect
github.com/confluentinc/cire-bucket-service/protos/bucket v0.50.0 // indirect
github.com/confluentinc/cire-obelisk/pkg/apis/network v0.507.0 // indirect
github.com/confluentinc/proto-go-setter v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/distribution/reference v0.6.0 // indirect
Expand All @@ -172,6 +193,7 @@ require (
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
Expand All @@ -193,6 +215,7 @@ require (
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hamba/avro/v2 v2.24.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
Expand Down Expand Up @@ -238,6 +261,7 @@ require (
github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/afero v1.10.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
Expand Down Expand Up @@ -281,6 +305,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.4.0 // indirect
k8s.io/api v0.29.2 // indirect
k8s.io/apiextensions-apiserver v0.17.12 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
Expand Down
952 changes: 952 additions & 0 deletions go.sum

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/organization/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
71 changes: 71 additions & 0 deletions internal/organization/command_scim_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package organization

import (
"time"

"github.com/gogo/protobuf/types"
"github.com/spf13/cobra"

ccloud "github.com/confluentinc/ccloud-sdk-go-v1"
pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
"github.com/confluentinc/cli/v4/pkg/errors"
)

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 (c *scimTokenCommand) getSCIMClient() (ccloud.SCIM, string, string, error) {
// Get organization from current user
user, err := c.Client.Auth.User()
if err != nil {
return nil, "", "", err
}
org := user.GetOrganization()
orgId := org.GetResourceId()

// Get connection name from organization's SSO configuration
sso := org.GetSso()
if sso == nil || sso.GetAuth0ConnectionName() == "" {
return nil, "", "", errors.NewErrorWithSuggestions(
"No SSO connection found for organization.",
"SCIM tokens require SSO to be configured for the organization.",
)
}
connectionName := sso.GetAuth0ConnectionName()

// Create v1 (non-public) client for SCIM operations
params := &ccloud.Params{
BaseURL: c.Context.GetPlatformServer(),
HttpClient: c.Client.HttpClient,
Logger: c.Client.Logger,
UserAgent: c.Config.Version.UserAgent,
}
v1Client := ccloud.NewClient(params)

return v1Client.SCIM, orgId, connectionName, nil
}

func formatTimestamp(ts *types.Timestamp) string {
if ts == nil {
return ""
}
t := time.Unix(ts.Seconds, int64(ts.Nanos))
return t.UTC().Format(time.RFC3339)
}
71 changes: 71 additions & 0 deletions internal/organization/command_scim_token_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package organization

import (
"context"

"github.com/spf13/cobra"

flowv1 "github.com/confluentinc/cc-structs/kafka/flow/v1"

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 {
scimClient, orgId, connectionName, err := c.getSCIMClient()
if err != nil {
return err
}

req := &flowv1.CreateSCIMTokenRequest{
OrgResourceId: orgId,
ConnectionName: connectionName,
}

// 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
}
req.ExpireDurationMins = expireDurationMins
}

token, err := scimClient.CreateToken(context.Background(), req)
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: token.Id,
Token: token.Token,
CreatedAt: formatTimestamp(token.CreatedAt),
ExpiresAt: formatTimestamp(token.ExpiresAt),
})
return table.Print()
}
49 changes: 49 additions & 0 deletions internal/organization/command_scim_token_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package organization

import (
"context"

"github.com/spf13/cobra"

flowv1 "github.com/confluentinc/cc-structs/kafka/flow/v1"

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 <id>",
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 {
scimClient, orgId, connectionName, err := c.getSCIMClient()
if err != nil {
return err
}

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 {
req := &flowv1.DeleteSCIMTokenRequest{
OrgResourceId: orgId,
ConnectionName: connectionName,
TokenId: tokenId,
}
return scimClient.DeleteToken(context.Background(), req)
}, resource.ScimToken)

return err
}
59 changes: 59 additions & 0 deletions internal/organization/command_scim_token_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package organization

import (
"context"

"github.com/spf13/cobra"

flowv1 "github.com/confluentinc/cc-structs/kafka/flow/v1"

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 {
scimClient, orgId, connectionName, err := c.getSCIMClient()
if err != nil {
return err
}

req := &flowv1.ListSCIMTokensRequest{
OrgResourceId: orgId,
ConnectionName: connectionName,
}

tokens, err := scimClient.ListTokens(context.Background(), req)
if err != nil {
return err
}

list := output.NewList(cmd)
for _, token := range tokens {
list.Add(&scimTokenListOut{
Id: token.Id,
CreatedAt: formatTimestamp(token.CreatedAt),
ExpiresAt: formatTimestamp(token.ExpiresAt),
})
}
return list.Print()
}
1 change: 1 addition & 0 deletions pkg/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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-08-12T10:58:00Z |
+------------+--------------------------------------+
Original file line number Diff line number Diff line change
@@ -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"
}
7 changes: 7 additions & 0 deletions test/fixtures/output/organization/scim-token-create.golden
Original file line number Diff line number Diff line change
@@ -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 |
+------------+--------------------------------------+
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: failed to delete nonexistent: error deleting scim token: SCIM token not found
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Are you sure you want to delete SCIM token "scim_token-67890"? (y/n): Deleted SCIM token "scim_token-67890".
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deleted SCIM token "scim_token-12345".
27 changes: 27 additions & 0 deletions test/fixtures/output/organization/scim-token-list-json.golden
Original file line number Diff line number Diff line change
@@ -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-08-12T10:58:00Z"
},
{
"id": "scim_token-67890",
"created_at": "2025-02-01T01:00:00Z",
"expires_at": "2026-02-01T01:00:00Z"
}
]
Loading