diff --git a/cmd/mciasgrpcctl/main.go b/cmd/mciasgrpcctl/main.go index 6330352..35151f0 100644 --- a/cmd/mciasgrpcctl/main.go +++ b/cmd/mciasgrpcctl/main.go @@ -10,41 +10,41 @@ // // Global flags: // -// -server gRPC server address (default: mcias.metacircular.net:9443) -// -token Bearer token for authentication (or set MCIAS_TOKEN env var) -// -cacert Path to CA certificate for TLS verification (optional) +// --server gRPC server address (default: mcias.metacircular.net:9443) +// --token Bearer token for authentication (or set MCIAS_TOKEN env var) +// --cacert Path to CA certificate for TLS verification (optional) // // Commands: // // health // pubkey // -// auth login -username NAME [-totp CODE] +// auth login --username NAME [--totp CODE] // auth logout // // account list -// account create -username NAME -password PASS [-type human|system] -// account get -id UUID -// account update -id UUID -status active|inactive -// account delete -id UUID +// account create --username NAME --password PASS [--type human|system] +// account get --id UUID +// account update --id UUID --status active|inactive +// account delete --id UUID // -// role list -id UUID -// role set -id UUID -roles role1,role2,... -// role grant -id UUID -role ROLE -// role revoke -id UUID -role ROLE +// role list --id UUID +// role set --id UUID --roles role1,role2,... +// role grant --id UUID --role ROLE +// role revoke --id UUID --role ROLE // -// token validate -token TOKEN -// token issue -id UUID -// token revoke -jti JTI +// token validate --token TOKEN +// token issue --id UUID +// token revoke --jti JTI // -// pgcreds get -id UUID -// pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER -password PASS +// pgcreds get --id UUID +// pgcreds set --id UUID --host HOST [--port PORT] --db DB --user USER --password PASS // // policy list -// policy create -description STR -json FILE [-priority N] [-not-before RFC3339] [-expires-at RFC3339] -// policy get -id ID -// policy update -id ID [-priority N] [-enabled true|false] [-not-before RFC3339] [-expires-at RFC3339] [-clear-not-before] [-clear-expires-at] -// policy delete -id ID +// policy create --description STR --json FILE [--priority N] [--not-before RFC3339] [--expires-at RFC3339] +// policy get --id ID +// policy update --id ID [--priority N] [--enabled true|false] [--not-before RFC3339] [--expires-at RFC3339] [--clear-not-before] [--clear-expires-at] +// policy delete --id ID package main import ( @@ -52,13 +52,13 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" - "flag" "fmt" "os" "strconv" "strings" "time" + "github.com/spf13/cobra" "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -67,63 +67,6 @@ import ( mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1" ) -func main() { - // Global flags. - serverAddr := flag.String("server", "mcias.metacircular.net:9443", "gRPC server address (host:port)") - tokenFlag := flag.String("token", "", "bearer token (or set MCIAS_TOKEN)") - caCert := flag.String("cacert", "", "path to CA certificate for TLS") - flag.Usage = usage - flag.Parse() - - // Resolve token from flag or environment. - bearerToken := *tokenFlag - if bearerToken == "" { - bearerToken = os.Getenv("MCIAS_TOKEN") - } - - args := flag.Args() - if len(args) == 0 { - usage() - os.Exit(1) - } - - // Build gRPC connection. - conn, err := newGRPCConn(*serverAddr, *caCert) - if err != nil { - fatalf("connect to gRPC server: %v", err) - } - defer func() { _ = conn.Close() }() - - ctl := &controller{ - conn: conn, - token: bearerToken, - } - - command := args[0] - subArgs := args[1:] - - switch command { - case "health": - ctl.runHealth() - case "pubkey": - ctl.runPubKey() - case "auth": - ctl.runAuth(subArgs) - case "account": - ctl.runAccount(subArgs) - case "role": - ctl.runRole(subArgs) - case "token": - ctl.runToken(subArgs) - case "pgcreds": - ctl.runPGCreds(subArgs) - case "policy": - ctl.runPolicy(subArgs) - default: - fatalf("unknown command %q; run with no args to see usage", command) - } -} - // controller holds the shared gRPC connection and token for all subcommands. type controller struct { conn *grpc.ClientConn @@ -148,55 +91,122 @@ func (c *controller) callCtx() (context.Context, context.CancelFunc) { return context.WithTimeout(c.authCtx(), 30*time.Second) } -// ---- health / pubkey ---- +func main() { + var ( + serverAddr string + tokenFlag string + caCert string + ) -func (c *controller) runHealth() { - adminCl := mciasv1.NewAdminServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() + ctl := &controller{} - resp, err := adminCl.Health(ctx, &mciasv1.HealthRequest{}) - if err != nil { - fatalf("health: %v", err) + root := &cobra.Command{ + Use: "mciasgrpcctl", + Short: "MCIAS gRPC admin CLI", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + // Resolve token from flag or environment. + ctl.token = tokenFlag + if ctl.token == "" { + ctl.token = os.Getenv("MCIAS_TOKEN") + } + + // Skip gRPC connection for completion commands. + if cmd.Name() == "completion" || cmd.Parent() != nil && cmd.Parent().Name() == "completion" { + return + } + // Also skip for help commands. + if cmd.Name() == "help" { + return + } + + conn, err := newGRPCConn(serverAddr, caCert) + if err != nil { + fatalf("connect to gRPC server: %v", err) + } + ctl.conn = conn + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + if ctl.conn != nil { + _ = ctl.conn.Close() + } + }, + } + + root.PersistentFlags().StringVar(&serverAddr, "server", "mcias.metacircular.net:9443", "gRPC server address (host:port)") + root.PersistentFlags().StringVar(&tokenFlag, "token", "", "bearer token (or set MCIAS_TOKEN)") + root.PersistentFlags().StringVar(&caCert, "cacert", "", "path to CA certificate for TLS") + + root.AddCommand(healthCmd(ctl)) + root.AddCommand(pubkeyCmd(ctl)) + root.AddCommand(authCmd(ctl)) + root.AddCommand(accountCmd(ctl)) + root.AddCommand(roleCmd(ctl)) + root.AddCommand(tokenCmd(ctl)) + root.AddCommand(pgcredsCmd(ctl)) + root.AddCommand(policyCmd(ctl)) + + if err := root.Execute(); err != nil { + os.Exit(1) } - printJSON(map[string]string{"status": resp.Status}) } -func (c *controller) runPubKey() { - adminCl := mciasv1.NewAdminServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() +// ---- health / pubkey ---- - resp, err := adminCl.GetPublicKey(ctx, &mciasv1.GetPublicKeyRequest{}) - if err != nil { - fatalf("pubkey: %v", err) +func healthCmd(ctl *controller) *cobra.Command { + return &cobra.Command{ + Use: "health", + Short: "Check server health", + Run: func(cmd *cobra.Command, args []string) { + adminCl := mciasv1.NewAdminServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := adminCl.Health(ctx, &mciasv1.HealthRequest{}) + if err != nil { + fatalf("health: %v", err) + } + printJSON(map[string]string{"status": resp.Status}) + }, + } +} + +func pubkeyCmd(ctl *controller) *cobra.Command { + return &cobra.Command{ + Use: "pubkey", + Short: "Get server public key", + Run: func(cmd *cobra.Command, args []string) { + adminCl := mciasv1.NewAdminServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := adminCl.GetPublicKey(ctx, &mciasv1.GetPublicKeyRequest{}) + if err != nil { + fatalf("pubkey: %v", err) + } + printJSON(map[string]string{ + "kty": resp.Kty, + "crv": resp.Crv, + "use": resp.Use, + "alg": resp.Alg, + "x": resp.X, + }) + }, } - printJSON(map[string]string{ - "kty": resp.Kty, - "crv": resp.Crv, - "use": resp.Use, - "alg": resp.Alg, - "x": resp.X, - }) } // ---- auth subcommands ---- -func (c *controller) runAuth(args []string) { - if len(args) == 0 { - fatalf("auth requires a subcommand: login, logout") - } - switch args[0] { - case "login": - c.authLogin(args[1:]) - case "logout": - c.authLogout() - default: - fatalf("unknown auth subcommand %q", args[0]) +func authCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Short: "Authentication commands", } + cmd.AddCommand(authLoginCmd(ctl)) + cmd.AddCommand(authLogoutCmd(ctl)) + return cmd } -// authLogin authenticates with the gRPC server using username and password, +// authLoginCmd authenticates with the gRPC server using username and password, // then prints the resulting bearer token to stdout. The password is always // prompted interactively; it is never accepted as a command-line flag to // prevent it from appearing in shell history, ps output, and process argument @@ -204,673 +214,791 @@ func (c *controller) runAuth(args []string) { // // Security: terminal echo is disabled during password entry // (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use. -func (c *controller) authLogin(args []string) { - fs := flag.NewFlagSet("auth login", flag.ExitOnError) - username := fs.String("username", "", "username (required)") - totpCode := fs.String("totp", "", "TOTP code (required if TOTP is enrolled)") - _ = fs.Parse(args) +func authLoginCmd(ctl *controller) *cobra.Command { + var ( + username string + totpCode string + ) + cmd := &cobra.Command{ + Use: "login", + Short: "Obtain a bearer token (password prompted interactively)", + Run: func(cmd *cobra.Command, args []string) { + if username == "" { + fatalf("auth login: --username is required") + } - if *username == "" { - fatalf("auth login: -username is required") - } + // Security: always prompt interactively; never accept password as a flag. + // This prevents the credential from appearing in shell history, ps output, + // and /proc/PID/cmdline. + fmt.Fprint(os.Stderr, "Password: ") + raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms + fmt.Fprintln(os.Stderr) + if err != nil { + fatalf("read password: %v", err) + } + passwd := string(raw) + // Zero the raw byte slice once copied into the string. + for i := range raw { + raw[i] = 0 + } - // Security: always prompt interactively; never accept password as a flag. - // This prevents the credential from appearing in shell history, ps output, - // and /proc/PID/cmdline. - fmt.Fprint(os.Stderr, "Password: ") - raw, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms - fmt.Fprintln(os.Stderr) - if err != nil { - fatalf("read password: %v", err) - } - passwd := string(raw) - // Zero the raw byte slice once copied into the string. - for i := range raw { - raw[i] = 0 - } + authCl := mciasv1.NewAuthServiceClient(ctl.conn) + // Login is a public RPC — no auth context needed. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() - authCl := mciasv1.NewAuthServiceClient(c.conn) - // Login is a public RPC — no auth context needed. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() + resp, err := authCl.Login(ctx, &mciasv1.LoginRequest{ + Username: username, + Password: passwd, + TotpCode: totpCode, + }) + if err != nil { + fatalf("auth login: %v", err) + } - resp, err := authCl.Login(ctx, &mciasv1.LoginRequest{ - Username: *username, - Password: passwd, - TotpCode: *totpCode, - }) - if err != nil { - fatalf("auth login: %v", err) - } - - // Print token to stdout so it can be captured by scripts, e.g.: - // export MCIAS_TOKEN=$(mciasgrpcctl auth login -username alice) - fmt.Println(resp.Token) - if resp.ExpiresAt != nil { - fmt.Fprintf(os.Stderr, "expires: %s\n", resp.ExpiresAt.AsTime().UTC().Format(time.RFC3339)) + // Print token to stdout so it can be captured by scripts, e.g.: + // export MCIAS_TOKEN=$(mciasgrpcctl auth login --username alice) + fmt.Println(resp.Token) + if resp.ExpiresAt != nil { + fmt.Fprintf(os.Stderr, "expires: %s\n", resp.ExpiresAt.AsTime().UTC().Format(time.RFC3339)) + } + }, } + cmd.Flags().StringVar(&username, "username", "", "username (required)") + cmd.Flags().StringVar(&totpCode, "totp", "", "TOTP code (required if TOTP is enrolled)") + return cmd } -// authLogout revokes the caller's current JWT via the gRPC AuthService. -func (c *controller) authLogout() { - authCl := mciasv1.NewAuthServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() +func authLogoutCmd(ctl *controller) *cobra.Command { + return &cobra.Command{ + Use: "logout", + Short: "Revoke the current bearer token", + Run: func(cmd *cobra.Command, args []string) { + authCl := mciasv1.NewAuthServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - if _, err := authCl.Logout(ctx, &mciasv1.LogoutRequest{}); err != nil { - fatalf("auth logout: %v", err) + if _, err := authCl.Logout(ctx, &mciasv1.LogoutRequest{}); err != nil { + fatalf("auth logout: %v", err) + } + fmt.Println("logged out") + }, } - fmt.Println("logged out") } // ---- account subcommands ---- -func (c *controller) runAccount(args []string) { - if len(args) == 0 { - fatalf("account requires a subcommand: list, create, get, update, delete") +func accountCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "account", + Short: "Account management commands", } - switch args[0] { - case "list": - c.accountList() - case "create": - c.accountCreate(args[1:]) - case "get": - c.accountGet(args[1:]) - case "update": - c.accountUpdate(args[1:]) - case "delete": - c.accountDelete(args[1:]) - default: - fatalf("unknown account subcommand %q", args[0]) + cmd.AddCommand(accountListCmd(ctl)) + cmd.AddCommand(accountCreateCmd(ctl)) + cmd.AddCommand(accountGetCmd(ctl)) + cmd.AddCommand(accountUpdateCmd(ctl)) + cmd.AddCommand(accountDeleteCmd(ctl)) + return cmd +} + +func accountListCmd(ctl *controller) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all accounts", + Run: func(cmd *cobra.Command, args []string) { + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.ListAccounts(ctx, &mciasv1.ListAccountsRequest{}) + if err != nil { + fatalf("account list: %v", err) + } + printJSON(resp.Accounts) + }, } } -func (c *controller) accountList() { - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() +func accountCreateCmd(ctl *controller) *cobra.Command { + var ( + username string + password string + accountType string + ) + cmd := &cobra.Command{ + Use: "create", + Short: "Create an account", + Run: func(cmd *cobra.Command, args []string) { + if username == "" { + fatalf("account create: --username is required") + } - resp, err := cl.ListAccounts(ctx, &mciasv1.ListAccountsRequest{}) - if err != nil { - fatalf("account list: %v", err) + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.CreateAccount(ctx, &mciasv1.CreateAccountRequest{ + Username: username, + Password: password, + AccountType: accountType, + }) + if err != nil { + fatalf("account create: %v", err) + } + printJSON(resp.Account) + }, } - printJSON(resp.Accounts) + cmd.Flags().StringVar(&username, "username", "", "username (required)") + cmd.Flags().StringVar(&password, "password", "", "password (required for human accounts)") + cmd.Flags().StringVar(&accountType, "type", "human", "account type: human or system") + return cmd } -func (c *controller) accountCreate(args []string) { - fs := flag.NewFlagSet("account create", flag.ExitOnError) - username := fs.String("username", "", "username (required)") - password := fs.String("password", "", "password (required for human accounts)") - accountType := fs.String("type", "human", "account type: human or system") - _ = fs.Parse(args) +func accountGetCmd(ctl *controller) *cobra.Command { + var id string + cmd := &cobra.Command{ + Use: "get", + Short: "Get an account by UUID", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("account get: --id is required") + } - if *username == "" { - fatalf("account create: -username is required") + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.GetAccount(ctx, &mciasv1.GetAccountRequest{Id: id}) + if err != nil { + fatalf("account get: %v", err) + } + printJSON(resp.Account) + }, } - - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.CreateAccount(ctx, &mciasv1.CreateAccountRequest{ - Username: *username, - Password: *password, - AccountType: *accountType, - }) - if err != nil { - fatalf("account create: %v", err) - } - printJSON(resp.Account) + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + return cmd } -func (c *controller) accountGet(args []string) { - fs := flag.NewFlagSet("account get", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - _ = fs.Parse(args) +func accountUpdateCmd(ctl *controller) *cobra.Command { + var ( + id string + status string + ) + cmd := &cobra.Command{ + Use: "update", + Short: "Update an account status", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("account update: --id is required") + } + if status == "" { + fatalf("account update: --status is required") + } - if *id == "" { - fatalf("account get: -id is required") + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + _, err := cl.UpdateAccount(ctx, &mciasv1.UpdateAccountRequest{ + Id: id, + Status: status, + }) + if err != nil { + fatalf("account update: %v", err) + } + fmt.Println("account updated") + }, } - - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.GetAccount(ctx, &mciasv1.GetAccountRequest{Id: *id}) - if err != nil { - fatalf("account get: %v", err) - } - printJSON(resp.Account) + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + cmd.Flags().StringVar(&status, "status", "", "new status: active or inactive (required)") + return cmd } -func (c *controller) accountUpdate(args []string) { - fs := flag.NewFlagSet("account update", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - status := fs.String("status", "", "new status: active or inactive (required)") - _ = fs.Parse(args) +func accountDeleteCmd(ctl *controller) *cobra.Command { + var id string + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("account delete: --id is required") + } - if *id == "" { - fatalf("account update: -id is required") + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + _, err := cl.DeleteAccount(ctx, &mciasv1.DeleteAccountRequest{Id: id}) + if err != nil { + fatalf("account delete: %v", err) + } + fmt.Println("account deleted") + }, } - if *status == "" { - fatalf("account update: -status is required") - } - - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - _, err := cl.UpdateAccount(ctx, &mciasv1.UpdateAccountRequest{ - Id: *id, - Status: *status, - }) - if err != nil { - fatalf("account update: %v", err) - } - fmt.Println("account updated") -} - -func (c *controller) accountDelete(args []string) { - fs := flag.NewFlagSet("account delete", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - _ = fs.Parse(args) - - if *id == "" { - fatalf("account delete: -id is required") - } - - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - _, err := cl.DeleteAccount(ctx, &mciasv1.DeleteAccountRequest{Id: *id}) - if err != nil { - fatalf("account delete: %v", err) - } - fmt.Println("account deleted") + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + return cmd } // ---- role subcommands ---- -func (c *controller) runRole(args []string) { - if len(args) == 0 { - fatalf("role requires a subcommand: list, set, grant, revoke") - } - switch args[0] { - case "list": - c.roleList(args[1:]) - case "set": - c.roleSet(args[1:]) - case "grant": - c.roleGrant(args[1:]) - case "revoke": - c.roleRevoke(args[1:]) - default: - fatalf("unknown role subcommand %q", args[0]) +func roleCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "role", + Short: "Role management commands", } + cmd.AddCommand(roleListCmd(ctl)) + cmd.AddCommand(roleSetCmd(ctl)) + cmd.AddCommand(roleGrantCmd(ctl)) + cmd.AddCommand(roleRevokeCmd(ctl)) + return cmd } -func (c *controller) roleList(args []string) { - fs := flag.NewFlagSet("role list", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - _ = fs.Parse(args) - - if *id == "" { - fatalf("role list: -id is required") - } - - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.GetRoles(ctx, &mciasv1.GetRolesRequest{Id: *id}) - if err != nil { - fatalf("role list: %v", err) - } - printJSON(resp.Roles) -} - -func (c *controller) roleSet(args []string) { - fs := flag.NewFlagSet("role set", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - rolesFlag := fs.String("roles", "", "comma-separated list of roles") - _ = fs.Parse(args) - - if *id == "" { - fatalf("role set: -id is required") - } - - var roles []string - if *rolesFlag != "" { - for _, r := range strings.Split(*rolesFlag, ",") { - r = strings.TrimSpace(r) - if r != "" { - roles = append(roles, r) +func roleListCmd(ctl *controller) *cobra.Command { + var id string + cmd := &cobra.Command{ + Use: "list", + Short: "List roles for an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("role list: --id is required") } - } - } - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - _, err := cl.SetRoles(ctx, &mciasv1.SetRolesRequest{Id: *id, Roles: roles}) - if err != nil { - fatalf("role set: %v", err) + resp, err := cl.GetRoles(ctx, &mciasv1.GetRolesRequest{Id: id}) + if err != nil { + fatalf("role list: %v", err) + } + printJSON(resp.Roles) + }, } - fmt.Printf("roles set: %v\n", roles) + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + return cmd } -func (c *controller) roleGrant(args []string) { - fs := flag.NewFlagSet("role grant", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - role := fs.String("role", "", "role name (required)") - _ = fs.Parse(args) +func roleSetCmd(ctl *controller) *cobra.Command { + var ( + id string + rolesFlag string + ) + cmd := &cobra.Command{ + Use: "set", + Short: "Set roles for an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("role set: --id is required") + } - if *id == "" { - fatalf("role grant: -id is required") - } - if *role == "" { - fatalf("role grant: -role is required") - } + var roles []string + if rolesFlag != "" { + for _, r := range strings.Split(rolesFlag, ",") { + r = strings.TrimSpace(r) + if r != "" { + roles = append(roles, r) + } + } + } - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - _, err := cl.GrantRole(ctx, &mciasv1.GrantRoleRequest{Id: *id, Role: *role}) - if err != nil { - fatalf("role grant: %v", err) + _, err := cl.SetRoles(ctx, &mciasv1.SetRolesRequest{Id: id, Roles: roles}) + if err != nil { + fatalf("role set: %v", err) + } + fmt.Printf("roles set: %v\n", roles) + }, } - fmt.Printf("role granted: %s\n", *role) + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + cmd.Flags().StringVar(&rolesFlag, "roles", "", "comma-separated list of roles") + return cmd } -func (c *controller) roleRevoke(args []string) { - fs := flag.NewFlagSet("role revoke", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - role := fs.String("role", "", "role name (required)") - _ = fs.Parse(args) +func roleGrantCmd(ctl *controller) *cobra.Command { + var ( + id string + role string + ) + cmd := &cobra.Command{ + Use: "grant", + Short: "Grant a role to an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("role grant: --id is required") + } + if role == "" { + fatalf("role grant: --role is required") + } - if *id == "" { - fatalf("role revoke: -id is required") - } - if *role == "" { - fatalf("role revoke: -role is required") - } + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - cl := mciasv1.NewAccountServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - _, err := cl.RevokeRole(ctx, &mciasv1.RevokeRoleRequest{Id: *id, Role: *role}) - if err != nil { - fatalf("role revoke: %v", err) + _, err := cl.GrantRole(ctx, &mciasv1.GrantRoleRequest{Id: id, Role: role}) + if err != nil { + fatalf("role grant: %v", err) + } + fmt.Printf("role granted: %s\n", role) + }, } - fmt.Printf("role revoked: %s\n", *role) + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + cmd.Flags().StringVar(&role, "role", "", "role name (required)") + return cmd +} + +func roleRevokeCmd(ctl *controller) *cobra.Command { + var ( + id string + role string + ) + cmd := &cobra.Command{ + Use: "revoke", + Short: "Revoke a role from an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("role revoke: --id is required") + } + if role == "" { + fatalf("role revoke: --role is required") + } + + cl := mciasv1.NewAccountServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + _, err := cl.RevokeRole(ctx, &mciasv1.RevokeRoleRequest{Id: id, Role: role}) + if err != nil { + fatalf("role revoke: %v", err) + } + fmt.Printf("role revoked: %s\n", role) + }, + } + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + cmd.Flags().StringVar(&role, "role", "", "role name (required)") + return cmd } // ---- token subcommands ---- -func (c *controller) runToken(args []string) { - if len(args) == 0 { - fatalf("token requires a subcommand: validate, issue, revoke") - } - switch args[0] { - case "validate": - c.tokenValidate(args[1:]) - case "issue": - c.tokenIssue(args[1:]) - case "revoke": - c.tokenRevoke(args[1:]) - default: - fatalf("unknown token subcommand %q", args[0]) +func tokenCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "token", + Short: "Token management commands", } + cmd.AddCommand(tokenValidateCmd(ctl)) + cmd.AddCommand(tokenIssueCmd(ctl)) + cmd.AddCommand(tokenRevokeCmd(ctl)) + return cmd } -func (c *controller) tokenValidate(args []string) { - fs := flag.NewFlagSet("token validate", flag.ExitOnError) - tok := fs.String("token", "", "JWT to validate (required)") - _ = fs.Parse(args) +func tokenValidateCmd(ctl *controller) *cobra.Command { + var tok string + cmd := &cobra.Command{ + Use: "validate", + Short: "Validate a JWT", + Run: func(cmd *cobra.Command, args []string) { + if tok == "" { + fatalf("token validate: --token is required") + } - if *tok == "" { - fatalf("token validate: -token is required") + cl := mciasv1.NewTokenServiceClient(ctl.conn) + // ValidateToken is public — no auth context needed. + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := cl.ValidateToken(ctx, &mciasv1.ValidateTokenRequest{Token: tok}) + if err != nil { + fatalf("token validate: %v", err) + } + printJSON(map[string]interface{}{ + "valid": resp.Valid, + "subject": resp.Subject, + "roles": resp.Roles, + }) + }, } - - cl := mciasv1.NewTokenServiceClient(c.conn) - // ValidateToken is public — no auth context needed. - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - resp, err := cl.ValidateToken(ctx, &mciasv1.ValidateTokenRequest{Token: *tok}) - if err != nil { - fatalf("token validate: %v", err) - } - printJSON(map[string]interface{}{ - "valid": resp.Valid, - "subject": resp.Subject, - "roles": resp.Roles, - }) + cmd.Flags().StringVar(&tok, "token", "", "JWT to validate (required)") + return cmd } -func (c *controller) tokenIssue(args []string) { - fs := flag.NewFlagSet("token issue", flag.ExitOnError) - id := fs.String("id", "", "system account UUID (required)") - _ = fs.Parse(args) +func tokenIssueCmd(ctl *controller) *cobra.Command { + var id string + cmd := &cobra.Command{ + Use: "issue", + Short: "Issue a service token", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("token issue: --id is required") + } - if *id == "" { - fatalf("token issue: -id is required") + cl := mciasv1.NewTokenServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.IssueServiceToken(ctx, &mciasv1.IssueServiceTokenRequest{AccountId: id}) + if err != nil { + fatalf("token issue: %v", err) + } + printJSON(map[string]string{"token": resp.Token}) + }, } - - cl := mciasv1.NewTokenServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.IssueServiceToken(ctx, &mciasv1.IssueServiceTokenRequest{AccountId: *id}) - if err != nil { - fatalf("token issue: %v", err) - } - printJSON(map[string]string{"token": resp.Token}) + cmd.Flags().StringVar(&id, "id", "", "system account UUID (required)") + return cmd } -func (c *controller) tokenRevoke(args []string) { - fs := flag.NewFlagSet("token revoke", flag.ExitOnError) - jti := fs.String("jti", "", "JTI of the token to revoke (required)") - _ = fs.Parse(args) +func tokenRevokeCmd(ctl *controller) *cobra.Command { + var jti string + cmd := &cobra.Command{ + Use: "revoke", + Short: "Revoke a token by JTI", + Run: func(cmd *cobra.Command, args []string) { + if jti == "" { + fatalf("token revoke: --jti is required") + } - if *jti == "" { - fatalf("token revoke: -jti is required") + cl := mciasv1.NewTokenServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + _, err := cl.RevokeToken(ctx, &mciasv1.RevokeTokenRequest{Jti: jti}) + if err != nil { + fatalf("token revoke: %v", err) + } + fmt.Println("token revoked") + }, } - - cl := mciasv1.NewTokenServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - _, err := cl.RevokeToken(ctx, &mciasv1.RevokeTokenRequest{Jti: *jti}) - if err != nil { - fatalf("token revoke: %v", err) - } - fmt.Println("token revoked") + cmd.Flags().StringVar(&jti, "jti", "", "JTI of the token to revoke (required)") + return cmd } // ---- pgcreds subcommands ---- -func (c *controller) runPGCreds(args []string) { - if len(args) == 0 { - fatalf("pgcreds requires a subcommand: get, set") - } - switch args[0] { - case "get": - c.pgCredsGet(args[1:]) - case "set": - c.pgCredsSet(args[1:]) - default: - fatalf("unknown pgcreds subcommand %q", args[0]) +func pgcredsCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "pgcreds", + Short: "Postgres credential management commands", } + cmd.AddCommand(pgcredsGetCmd(ctl)) + cmd.AddCommand(pgcredsSetCmd(ctl)) + return cmd } -func (c *controller) pgCredsGet(args []string) { - fs := flag.NewFlagSet("pgcreds get", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - _ = fs.Parse(args) +func pgcredsGetCmd(ctl *controller) *cobra.Command { + var id string + cmd := &cobra.Command{ + Use: "get", + Short: "Get Postgres credentials for an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" { + fatalf("pgcreds get: --id is required") + } - if *id == "" { - fatalf("pgcreds get: -id is required") - } + cl := mciasv1.NewCredentialServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - cl := mciasv1.NewCredentialServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.GetPGCreds(ctx, &mciasv1.GetPGCredsRequest{Id: *id}) - if err != nil { - fatalf("pgcreds get: %v", err) - } - if resp.Creds == nil { - fatalf("pgcreds get: no credentials returned") - } - printJSON(map[string]interface{}{ - "host": resp.Creds.Host, - "port": resp.Creds.Port, - "database": resp.Creds.Database, - "username": resp.Creds.Username, - "password": resp.Creds.Password, - }) -} - -func (c *controller) pgCredsSet(args []string) { - fs := flag.NewFlagSet("pgcreds set", flag.ExitOnError) - id := fs.String("id", "", "account UUID (required)") - host := fs.String("host", "", "Postgres host (required)") - port := fs.Int("port", 5432, "Postgres port") - dbName := fs.String("db", "", "Postgres database name (required)") - username := fs.String("user", "", "Postgres username (required)") - password := fs.String("password", "", "Postgres password (required)") - _ = fs.Parse(args) - - if *id == "" || *host == "" || *dbName == "" || *username == "" || *password == "" { - fatalf("pgcreds set: -id, -host, -db, -user, and -password are required") - } - - cl := mciasv1.NewCredentialServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - _, err := cl.SetPGCreds(ctx, &mciasv1.SetPGCredsRequest{ - Id: *id, - Creds: &mciasv1.PGCreds{ - Host: *host, - Port: int32(*port), //nolint:gosec // G115: port validated as [1,65535] by flag parsing - Database: *dbName, - Username: *username, - Password: *password, + resp, err := cl.GetPGCreds(ctx, &mciasv1.GetPGCredsRequest{Id: id}) + if err != nil { + fatalf("pgcreds get: %v", err) + } + if resp.Creds == nil { + fatalf("pgcreds get: no credentials returned") + } + printJSON(map[string]interface{}{ + "host": resp.Creds.Host, + "port": resp.Creds.Port, + "database": resp.Creds.Database, + "username": resp.Creds.Username, + "password": resp.Creds.Password, + }) }, - }) - if err != nil { - fatalf("pgcreds set: %v", err) } - fmt.Println("credentials stored") + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + return cmd +} + +func pgcredsSetCmd(ctl *controller) *cobra.Command { + var ( + id string + host string + port int + dbName string + username string + password string + ) + cmd := &cobra.Command{ + Use: "set", + Short: "Set Postgres credentials for an account", + Run: func(cmd *cobra.Command, args []string) { + if id == "" || host == "" || dbName == "" || username == "" || password == "" { + fatalf("pgcreds set: --id, --host, --db, --user, and --password are required") + } + + cl := mciasv1.NewCredentialServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + _, err := cl.SetPGCreds(ctx, &mciasv1.SetPGCredsRequest{ + Id: id, + Creds: &mciasv1.PGCreds{ + Host: host, + Port: int32(port), //nolint:gosec // G115: port validated as [1,65535] by flag parsing + Database: dbName, + Username: username, + Password: password, + }, + }) + if err != nil { + fatalf("pgcreds set: %v", err) + } + fmt.Println("credentials stored") + }, + } + cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") + cmd.Flags().StringVar(&host, "host", "", "Postgres host (required)") + cmd.Flags().IntVar(&port, "port", 5432, "Postgres port") + cmd.Flags().StringVar(&dbName, "db", "", "Postgres database name (required)") + cmd.Flags().StringVar(&username, "user", "", "Postgres username (required)") + cmd.Flags().StringVar(&password, "password", "", "Postgres password (required)") + return cmd } // ---- policy subcommands ---- -func (c *controller) runPolicy(args []string) { - if len(args) == 0 { - fatalf("policy requires a subcommand: list, create, get, update, delete") +func policyCmd(ctl *controller) *cobra.Command { + cmd := &cobra.Command{ + Use: "policy", + Short: "Policy rule management commands", } - switch args[0] { - case "list": - c.policyList() - case "create": - c.policyCreate(args[1:]) - case "get": - c.policyGet(args[1:]) - case "update": - c.policyUpdate(args[1:]) - case "delete": - c.policyDelete(args[1:]) - default: - fatalf("unknown policy subcommand %q", args[0]) + cmd.AddCommand(policyListCmd(ctl)) + cmd.AddCommand(policyCreateCmd(ctl)) + cmd.AddCommand(policyGetCmd(ctl)) + cmd.AddCommand(policyUpdateCmd(ctl)) + cmd.AddCommand(policyDeleteCmd(ctl)) + return cmd +} + +func policyListCmd(ctl *controller) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all policy rules", + Run: func(cmd *cobra.Command, args []string) { + cl := mciasv1.NewPolicyServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.ListPolicyRules(ctx, &mciasv1.ListPolicyRulesRequest{}) + if err != nil { + fatalf("policy list: %v", err) + } + printJSON(resp.Rules) + }, } } -func (c *controller) policyList() { - cl := mciasv1.NewPolicyServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() +func policyCreateCmd(ctl *controller) *cobra.Command { + var ( + description string + jsonFile string + priority int + notBefore string + expiresAt string + ) + cmd := &cobra.Command{ + Use: "create", + Short: "Create a policy rule", + Run: func(cmd *cobra.Command, args []string) { + if description == "" { + fatalf("policy create: --description is required") + } + if jsonFile == "" { + fatalf("policy create: --json is required (path to rule body JSON file)") + } - resp, err := cl.ListPolicyRules(ctx, &mciasv1.ListPolicyRulesRequest{}) - if err != nil { - fatalf("policy list: %v", err) + // G304: path comes from a CLI flag supplied by the operator. + ruleBytes, err := os.ReadFile(jsonFile) //nolint:gosec + if err != nil { + fatalf("policy create: read %s: %v", jsonFile, err) + } + + // Validate that the file contains valid JSON before sending. + var ruleBody interface{} + if err := json.Unmarshal(ruleBytes, &ruleBody); err != nil { + fatalf("policy create: invalid JSON in %s: %v", jsonFile, err) + } + + if notBefore != "" { + if _, err := time.Parse(time.RFC3339, notBefore); err != nil { + fatalf("policy create: --not-before must be RFC3339: %v", err) + } + } + if expiresAt != "" { + if _, err := time.Parse(time.RFC3339, expiresAt); err != nil { + fatalf("policy create: --expires-at must be RFC3339: %v", err) + } + } + + cl := mciasv1.NewPolicyServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.CreatePolicyRule(ctx, &mciasv1.CreatePolicyRuleRequest{ + Description: description, + RuleJson: string(ruleBytes), + Priority: int32(priority), //nolint:gosec // priority is a small positive integer + NotBefore: notBefore, + ExpiresAt: expiresAt, + }) + if err != nil { + fatalf("policy create: %v", err) + } + printJSON(resp.Rule) + }, } - printJSON(resp.Rules) + cmd.Flags().StringVar(&description, "description", "", "rule description (required)") + cmd.Flags().StringVar(&jsonFile, "json", "", "path to JSON file containing the rule body (required)") + cmd.Flags().IntVar(&priority, "priority", 100, "rule priority (lower = evaluated first)") + cmd.Flags().StringVar(¬Before, "not-before", "", "earliest activation time (RFC3339, optional)") + cmd.Flags().StringVar(&expiresAt, "expires-at", "", "expiry time (RFC3339, optional)") + return cmd } -func (c *controller) policyCreate(args []string) { - fs := flag.NewFlagSet("policy create", flag.ExitOnError) - description := fs.String("description", "", "rule description (required)") - jsonFile := fs.String("json", "", "path to JSON file containing the rule body (required)") - priority := fs.Int("priority", 100, "rule priority (lower = evaluated first)") - notBefore := fs.String("not-before", "", "earliest activation time (RFC3339, optional)") - expiresAt := fs.String("expires-at", "", "expiry time (RFC3339, optional)") - _ = fs.Parse(args) +func policyGetCmd(ctl *controller) *cobra.Command { + var idStr string + cmd := &cobra.Command{ + Use: "get", + Short: "Get a policy rule by ID", + Run: func(cmd *cobra.Command, args []string) { + if idStr == "" { + fatalf("policy get: --id is required") + } + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + fatalf("policy get: --id must be an integer") + } - if *description == "" { - fatalf("policy create: -description is required") - } - if *jsonFile == "" { - fatalf("policy create: -json is required (path to rule body JSON file)") - } + cl := mciasv1.NewPolicyServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - // G304: path comes from a CLI flag supplied by the operator. - ruleBytes, err := os.ReadFile(*jsonFile) //nolint:gosec - if err != nil { - fatalf("policy create: read %s: %v", *jsonFile, err) + resp, err := cl.GetPolicyRule(ctx, &mciasv1.GetPolicyRuleRequest{Id: id}) + if err != nil { + fatalf("policy get: %v", err) + } + printJSON(resp.Rule) + }, } - - // Validate that the file contains valid JSON before sending. - var ruleBody interface{} - if err := json.Unmarshal(ruleBytes, &ruleBody); err != nil { - fatalf("policy create: invalid JSON in %s: %v", *jsonFile, err) - } - - if *notBefore != "" { - if _, err := time.Parse(time.RFC3339, *notBefore); err != nil { - fatalf("policy create: -not-before must be RFC3339: %v", err) - } - } - if *expiresAt != "" { - if _, err := time.Parse(time.RFC3339, *expiresAt); err != nil { - fatalf("policy create: -expires-at must be RFC3339: %v", err) - } - } - - cl := mciasv1.NewPolicyServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.CreatePolicyRule(ctx, &mciasv1.CreatePolicyRuleRequest{ - Description: *description, - RuleJson: string(ruleBytes), - Priority: int32(*priority), //nolint:gosec // priority is a small positive integer - NotBefore: *notBefore, - ExpiresAt: *expiresAt, - }) - if err != nil { - fatalf("policy create: %v", err) - } - printJSON(resp.Rule) + cmd.Flags().StringVar(&idStr, "id", "", "rule ID (required)") + return cmd } -func (c *controller) policyGet(args []string) { - fs := flag.NewFlagSet("policy get", flag.ExitOnError) - idStr := fs.String("id", "", "rule ID (required)") - _ = fs.Parse(args) +func policyUpdateCmd(ctl *controller) *cobra.Command { + var ( + idStr string + priority int + enabled string + notBefore string + expiresAt string + clearNotBefore bool + clearExpiresAt bool + ) + cmd := &cobra.Command{ + Use: "update", + Short: "Update a policy rule", + Run: func(cmd *cobra.Command, args []string) { + if idStr == "" { + fatalf("policy update: --id is required") + } + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + fatalf("policy update: --id must be an integer") + } - if *idStr == "" { - fatalf("policy get: -id is required") - } - id, err := strconv.ParseInt(*idStr, 10, 64) - if err != nil { - fatalf("policy get: -id must be an integer") - } + req := &mciasv1.UpdatePolicyRuleRequest{ + Id: id, + ClearNotBefore: clearNotBefore, + ClearExpiresAt: clearExpiresAt, + } - cl := mciasv1.NewPolicyServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() + if priority >= 0 { + v := int32(priority) //nolint:gosec // priority is a small positive integer + req.Priority = &v + } + if enabled != "" { + switch enabled { + case "true": + b := true + req.Enabled = &b + case "false": + b := false + req.Enabled = &b + default: + fatalf("policy update: --enabled must be true or false") + } + } + if !clearNotBefore && notBefore != "" { + if _, err := time.Parse(time.RFC3339, notBefore); err != nil { + fatalf("policy update: --not-before must be RFC3339: %v", err) + } + req.NotBefore = notBefore + } + if !clearExpiresAt && expiresAt != "" { + if _, err := time.Parse(time.RFC3339, expiresAt); err != nil { + fatalf("policy update: --expires-at must be RFC3339: %v", err) + } + req.ExpiresAt = expiresAt + } - resp, err := cl.GetPolicyRule(ctx, &mciasv1.GetPolicyRuleRequest{Id: id}) - if err != nil { - fatalf("policy get: %v", err) + cl := mciasv1.NewPolicyServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() + + resp, err := cl.UpdatePolicyRule(ctx, req) + if err != nil { + fatalf("policy update: %v", err) + } + printJSON(resp.Rule) + }, } - printJSON(resp.Rule) + cmd.Flags().StringVar(&idStr, "id", "", "rule ID (required)") + cmd.Flags().IntVar(&priority, "priority", -1, "new priority (-1 = no change)") + cmd.Flags().StringVar(&enabled, "enabled", "", "true or false") + cmd.Flags().StringVar(¬Before, "not-before", "", "earliest activation time (RFC3339)") + cmd.Flags().StringVar(&expiresAt, "expires-at", "", "expiry time (RFC3339)") + cmd.Flags().BoolVar(&clearNotBefore, "clear-not-before", false, "remove not_before constraint") + cmd.Flags().BoolVar(&clearExpiresAt, "clear-expires-at", false, "remove expires_at constraint") + return cmd } -func (c *controller) policyUpdate(args []string) { - fs := flag.NewFlagSet("policy update", flag.ExitOnError) - idStr := fs.String("id", "", "rule ID (required)") - priority := fs.Int("priority", -1, "new priority (-1 = no change)") - enabled := fs.String("enabled", "", "true or false") - notBefore := fs.String("not-before", "", "earliest activation time (RFC3339)") - expiresAt := fs.String("expires-at", "", "expiry time (RFC3339)") - clearNotBefore := fs.Bool("clear-not-before", false, "remove not_before constraint") - clearExpiresAt := fs.Bool("clear-expires-at", false, "remove expires_at constraint") - _ = fs.Parse(args) +func policyDeleteCmd(ctl *controller) *cobra.Command { + var idStr string + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a policy rule", + Run: func(cmd *cobra.Command, args []string) { + if idStr == "" { + fatalf("policy delete: --id is required") + } + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + fatalf("policy delete: --id must be an integer") + } - if *idStr == "" { - fatalf("policy update: -id is required") - } - id, err := strconv.ParseInt(*idStr, 10, 64) - if err != nil { - fatalf("policy update: -id must be an integer") - } + cl := mciasv1.NewPolicyServiceClient(ctl.conn) + ctx, cancel := ctl.callCtx() + defer cancel() - req := &mciasv1.UpdatePolicyRuleRequest{ - Id: id, - ClearNotBefore: *clearNotBefore, - ClearExpiresAt: *clearExpiresAt, + if _, err := cl.DeletePolicyRule(ctx, &mciasv1.DeletePolicyRuleRequest{Id: id}); err != nil { + fatalf("policy delete: %v", err) + } + fmt.Println("policy rule deleted") + }, } - - if *priority >= 0 { - v := int32(*priority) //nolint:gosec // priority is a small positive integer - req.Priority = &v - } - if *enabled != "" { - switch *enabled { - case "true": - b := true - req.Enabled = &b - case "false": - b := false - req.Enabled = &b - default: - fatalf("policy update: -enabled must be true or false") - } - } - if !*clearNotBefore && *notBefore != "" { - if _, err := time.Parse(time.RFC3339, *notBefore); err != nil { - fatalf("policy update: -not-before must be RFC3339: %v", err) - } - req.NotBefore = *notBefore - } - if !*clearExpiresAt && *expiresAt != "" { - if _, err := time.Parse(time.RFC3339, *expiresAt); err != nil { - fatalf("policy update: -expires-at must be RFC3339: %v", err) - } - req.ExpiresAt = *expiresAt - } - - cl := mciasv1.NewPolicyServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - resp, err := cl.UpdatePolicyRule(ctx, req) - if err != nil { - fatalf("policy update: %v", err) - } - printJSON(resp.Rule) -} - -func (c *controller) policyDelete(args []string) { - fs := flag.NewFlagSet("policy delete", flag.ExitOnError) - idStr := fs.String("id", "", "rule ID (required)") - _ = fs.Parse(args) - - if *idStr == "" { - fatalf("policy delete: -id is required") - } - id, err := strconv.ParseInt(*idStr, 10, 64) - if err != nil { - fatalf("policy delete: -id must be an integer") - } - - cl := mciasv1.NewPolicyServiceClient(c.conn) - ctx, cancel := c.callCtx() - defer cancel() - - if _, err := cl.DeletePolicyRule(ctx, &mciasv1.DeletePolicyRuleRequest{Id: id}); err != nil { - fatalf("policy delete: %v", err) - } - fmt.Println("policy rule deleted") + cmd.Flags().StringVar(&idStr, "id", "", "rule ID (required)") + return cmd } // ---- gRPC connection ---- @@ -923,52 +1051,3 @@ func fatalf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "mciasgrpcctl: "+format+"\n", args...) os.Exit(1) } - -func usage() { - fmt.Fprintf(os.Stderr, `mciasgrpcctl - MCIAS gRPC admin CLI - -Usage: mciasgrpcctl [global flags] [args] - -Global flags: - -server gRPC server address (default: mcias.metacircular.net:9443) - -token Bearer token (or set MCIAS_TOKEN env var) - -cacert Path to CA certificate for TLS verification - -Commands: - health - pubkey - - auth login -username NAME [-totp CODE] - Obtain a bearer token. Password is always prompted interactively. - Token is written to stdout; expiry to stderr. - Example: export MCIAS_TOKEN=$(mciasgrpcctl auth login -username alice) - auth logout Revoke the current bearer token. - - account list - account create -username NAME -password PASS [-type human|system] - account get -id UUID - account update -id UUID -status active|inactive - account delete -id UUID - - role list -id UUID - role set -id UUID -roles role1,role2,... - - token validate -token TOKEN - token issue -id UUID - token revoke -jti JTI - - pgcreds get -id UUID - pgcreds set -id UUID -host HOST [-port PORT] -db DB -user USER -password PASS - - policy list - policy create -description STR -json FILE [-priority N] - [-not-before RFC3339] [-expires-at RFC3339] - FILE must contain a JSON rule body, e.g.: - {"effect":"allow","actions":["pgcreds:read"],"resource_type":"pgcreds","owner_matches_subject":true} - policy get -id ID - policy update -id ID [-priority N] [-enabled true|false] - [-not-before RFC3339] [-expires-at RFC3339] - [-clear-not-before] [-clear-expires-at] - policy delete -id ID -`) -}