Skip to content
Merged
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
2 changes: 2 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/thalassa-cloud/cli/cmd/iaas/networking"
"github.com/thalassa-cloud/cli/cmd/iaas/regions"
"github.com/thalassa-cloud/cli/cmd/iaas/storage"
"github.com/thalassa-cloud/cli/cmd/iam"
"github.com/thalassa-cloud/cli/cmd/kubernetes"
"github.com/thalassa-cloud/cli/cmd/me"
"github.com/thalassa-cloud/cli/cmd/objectstorage"
Expand Down Expand Up @@ -69,6 +70,7 @@ func init() {
RootCmd.AddCommand(kubernetes.KubernetesCmd)
RootCmd.AddCommand(dbaas.DbaasCmd)
RootCmd.AddCommand(me.MeCmd)
RootCmd.AddCommand(iam.IamCmd)
RootCmd.AddCommand(audit.AuditCmd)
RootCmd.AddCommand(oidc.OidcCmd)

Expand Down
110 changes: 110 additions & 0 deletions cmd/iam/federatedidentities/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package federatedidentities

import (
"fmt"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/cmd/iam/internal/shared"
"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/table"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
clientiam "github.com/thalassa-cloud/client-go/iam"
)

var (
createName string
createDescription string
createLabels []string
createAnnotations []string
createSA string
createProvider string
createSubject string
createAudiences []string
createAudienceMode string
createScopes []string
createExpiresAt string
createConditions string
createConditionsFile string
)

var createCmd = &cobra.Command{
Use: "create",
Short: "Create a federated identity",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
if createName == "" || createSA == "" || createProvider == "" || createSubject == "" {
return fmt.Errorf("--name, --service-account, --provider, and --subject are required")
}
scopes, err := shared.ParseAccessCredentialScopes(createScopes)
if err != nil {
return err
}
mode := clientiam.AudienceMatchMode(createAudienceMode)
if createAudienceMode == "" {
mode = clientiam.AudienceMatchModeAny
} else if mode != clientiam.AudienceMatchModeExact && mode != clientiam.AudienceMatchModeAny && mode != clientiam.AudienceMatchModeAll {
return fmt.Errorf("invalid --audience-match-mode (use exact, any, or all)")
}
expires, err := shared.ParseOptionalRFC3339(createExpiresAt)
if err != nil {
return err
}
conds, err := shared.ParseConditionsJSON(createConditions, createConditionsFile)
if err != nil {
return err
}
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
fi, err := client.IAM().CreateFederatedIdentity(ctx, clientiam.CreateFederatedIdentityRequest{
Name: createName,
Description: createDescription,
Labels: shared.KeyValuePairsToMap(createLabels),
Annotations: shared.KeyValuePairsToMap(createAnnotations),
ServiceAccountIdentity: createSA,
ProviderIdentity: createProvider,
ProviderSubject: createSubject,
TrustedAudiences: createAudiences,
AudienceMatchMode: mode,
AllowedScopes: scopes,
ExpiresAt: expires,
Conditions: conds,
})
if err != nil {
return fmt.Errorf("failed to create federated identity: %w", err)
}
if fi == nil {
return nil
}
body := [][]string{{fi.Identity, fi.Name, string(fi.Status)}}
if noHeader {
table.Print(nil, body)
} else {
table.Print([]string{"ID", "Name", "Status"}, body)
}
return nil
},
}

func init() {
FederatedIdentitiesCmd.AddCommand(createCmd)
createCmd.Flags().BoolVar(&noHeader, shared.NoHeaderKey, false, "Do not print table headers")
createCmd.Flags().StringVar(&createName, "name", "", "Display name")
createCmd.Flags().StringVar(&createDescription, "description", "", "Description")
createCmd.Flags().StringSliceVar(&createLabels, "labels", nil, "Labels as key=value (repeatable)")
createCmd.Flags().StringSliceVar(&createAnnotations, "annotations", nil, "Annotations as key=value (repeatable)")
createCmd.Flags().StringVar(&createSA, "service-account", "", "Service account identity to bind")
createCmd.Flags().StringVar(&createProvider, "provider", "", "Federated identity provider identity")
createCmd.Flags().StringVar(&createSubject, "subject", "", "OIDC sub claim for this identity")
createCmd.Flags().StringSliceVar(&createAudiences, "trusted-audience", nil, "Trusted JWT audiences (repeatable)")
createCmd.Flags().StringVar(&createAudienceMode, "audience-match-mode", "", "exact, any (default), or all")
createCmd.Flags().StringSliceVar(&createScopes, "scope", nil, "Allowed scopes: api:read, api:write, kubernetes, objectStorage (repeatable)")
createCmd.Flags().StringVar(&createExpiresAt, "expires-at", "", "RFC3339 expiry time")
createCmd.Flags().StringVar(&createConditions, "conditions", "", "Conditions as JSON object")
createCmd.Flags().StringVar(&createConditionsFile, "conditions-file", "", "Path to JSON file for conditions")
_ = createCmd.RegisterFlagCompletionFunc("service-account", completion.CompleteIAMServiceAccountIdentityFlag)
_ = createCmd.RegisterFlagCompletionFunc("provider", completion.CompleteIAMFederatedIdentityProviderIdentityFlag)
}
45 changes: 45 additions & 0 deletions cmd/iam/federatedidentities/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package federatedidentities

import (
"fmt"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/cmd/iam/internal/shared"
"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
)

var deleteForce bool

var deleteCmd = &cobra.Command{
Use: "delete <identity>",
Short: "Delete a federated identity",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.CompleteIAMFederatedIdentityIdentity,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
ok, err := shared.PromptDestructiveUnlessForce(deleteForce, fmt.Sprintf("Are you sure you want to delete this federated identity?\n Identity: %s\n", args[0]))
if err != nil {
return err
}
if !ok {
return nil
}
if err := client.IAM().DeleteFederatedIdentity(ctx, args[0]); err != nil {
return fmt.Errorf("failed to delete federated identity: %w", err)
}
fmt.Printf("Deleted federated identity %s\n", args[0])
return nil
},
}

func init() {
FederatedIdentitiesCmd.AddCommand(deleteCmd)
deleteCmd.Flags().BoolVar(&deleteForce, shared.ForceKey, false, "Skip the confirmation prompt and delete")
deleteCmd.Flags().BoolVar(&noHeader, shared.NoHeaderKey, false, "Do not print table headers")
}
10 changes: 10 additions & 0 deletions cmd/iam/federatedidentities/federatedidentities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package federatedidentities

import "github.com/spf13/cobra"

// FederatedIdentitiesCmd manages federated OIDC identities.
var FederatedIdentitiesCmd = &cobra.Command{
Use: "federated-identities",
Aliases: []string{"fed-ids", "federated-identity"},
Short: "Federated identities (OIDC subject bindings)",
}
61 changes: 61 additions & 0 deletions cmd/iam/federatedidentities/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package federatedidentities

import (
"fmt"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/cmd/iam/internal/shared"
"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/formattime"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
)

var getCmd = &cobra.Command{
Use: "get <identity>",
Short: "Show a federated identity",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.CompleteIAMFederatedIdentityIdentity,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
fi, err := client.IAM().GetFederatedIdentity(ctx, args[0])
if err != nil {
return fmt.Errorf("failed to get federated identity: %w", err)
}
fmt.Printf("Identity: %s\n", fi.Identity)
fmt.Printf("Name: %s\n", fi.Name)
fmt.Printf("Description: %s\n", fi.Description)
fmt.Printf("Provider subject: %s\n", fi.ProviderSubject)
fmt.Printf("Status: %s\n", fi.Status)
fmt.Printf("Audience mode: %s\n", fi.AudienceMatchMode)
fmt.Printf("Trusted audiences: %s\n", strings.Join(fi.TrustedAudiences, ","))
scopes := make([]string, 0, len(fi.AllowedScopes))
for _, s := range fi.AllowedScopes {
scopes = append(scopes, string(s))
}
fmt.Printf("Allowed scopes: %s\n", strings.Join(scopes, ","))
if fi.Provider != nil {
fmt.Printf("Provider: %s (%s)\n", fi.Provider.Name, fi.Provider.Identity)
}
if fi.ServiceAccount != nil {
fmt.Printf("Service account: %s (%s)\n", fi.ServiceAccount.Name, fi.ServiceAccount.Identity)
}
if fi.ExpiresAt != nil {
fmt.Printf("Expires at: %s\n", fi.ExpiresAt.Format(time.RFC3339))
}
fmt.Printf("Created: %s\n", formattime.FormatTime(fi.CreatedAt.Local(), showExactTime))
return nil
},
}

func init() {
FederatedIdentitiesCmd.AddCommand(getCmd)
getCmd.Flags().BoolVar(&noHeader, shared.NoHeaderKey, false, "Do not print table headers")
getCmd.Flags().BoolVar(&showExactTime, "exact-time", false, "Show full timestamps instead of relative time")
}
73 changes: 73 additions & 0 deletions cmd/iam/federatedidentities/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package federatedidentities

import (
"fmt"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/cmd/iam/internal/shared"
"github.com/thalassa-cloud/cli/internal/formattime"
"github.com/thalassa-cloud/cli/internal/labels"
"github.com/thalassa-cloud/cli/internal/table"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
"github.com/thalassa-cloud/client-go/filters"
clientiam "github.com/thalassa-cloud/client-go/iam"
)

var (
noHeader bool
showExactTime bool
listSelector string
)

var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List federated identities",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}
req := &clientiam.ListFederatedIdentitiesRequest{}
if listSelector != "" {
req.Filters = []filters.Filter{
&filters.LabelFilter{MatchLabels: labels.ParseLabelSelector(listSelector)},
}
}
list, err := client.IAM().ListFederatedIdentities(ctx, req)
if err != nil {
return fmt.Errorf("failed to list federated identities: %w", err)
}
body := make([][]string, 0, len(list))
for _, fi := range list {
prov := ""
if fi.Provider != nil {
prov = fi.Provider.Name
}
body = append(body, []string{
fi.Identity,
fi.Name,
fi.ProviderSubject,
prov,
string(fi.Status),
formattime.FormatTime(fi.CreatedAt.Local(), showExactTime),
})
}
if noHeader {
table.Print(nil, body)
} else {
table.Print([]string{"ID", "Name", "Subject", "Provider", "Status", "Created"}, body)
}
return nil
},
}

func init() {
FederatedIdentitiesCmd.AddCommand(listCmd)
listCmd.Flags().BoolVar(&noHeader, shared.NoHeaderKey, false, "Do not print table headers")
listCmd.Flags().BoolVar(&showExactTime, "exact-time", false, "Show full timestamps instead of relative time")
listCmd.Flags().StringVar(&listSelector, "label-selector", "", "Filter by labels (key=value,key2=value2)")
}
Loading
Loading