// Command mciasctl is the MCIAS admin CLI. // // It connects to a running mciassrv instance and provides subcommands for // managing accounts, roles, tokens, Postgres credentials, policy rules, and // account tags. // // Usage: // // mciasctl [global flags] [args] // // Global flags: // // --server URL of the mciassrv instance (default: https://mcias.metacircular.net:8443) // --token Bearer token for authentication (or set MCIAS_TOKEN env var) // --cacert Path to CA certificate for TLS verification (optional) // // Commands: // // auth login --username NAME [--totp CODE] // auth change-password (passwords always prompted interactively) // // account list // account create --username NAME [--type human|system] // account get --id UUID // account update --id UUID [--status active|inactive] // account delete --id UUID // account set-password --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 // // token issue --id UUID // token revoke --jti JTI // // pgcreds list // pgcreds set --id UUID --host HOST [--port PORT] --db DB --user USER [--password PASS] // pgcreds get --id UUID // // 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 // // tag list --id UUID // tag set --id UUID --tags tag1,tag2,... package main import ( "crypto/tls" "crypto/x509" "encoding/json" "fmt" "net/http" "os" "strings" "time" "github.com/spf13/cobra" "golang.org/x/term" ) // Global flags bound by the root command's PersistentFlags. var ( serverURL string tokenFlag string caCertPath string ) func main() { root := &cobra.Command{ Use: "mciasctl", Short: "MCIAS admin CLI", Long: "mciasctl connects to a running mciassrv instance and provides subcommands\nfor managing accounts, roles, tokens, Postgres credentials, policy rules,\nand account tags.", } root.PersistentFlags().StringVar(&serverURL, "server", "https://mcias.metacircular.net:8443", "mciassrv base URL") root.PersistentFlags().StringVar(&tokenFlag, "token", "", "bearer token (or set MCIAS_TOKEN)") root.PersistentFlags().StringVar(&caCertPath, "cacert", "", "path to CA certificate for TLS") root.AddCommand(authCmd()) root.AddCommand(accountCmd()) root.AddCommand(roleCmd()) root.AddCommand(tokenCmd()) root.AddCommand(pgcredsCmd()) root.AddCommand(policyCmd()) root.AddCommand(tagCmd()) if err := root.Execute(); err != nil { os.Exit(1) } } // newController builds a controller from the global flags. Called at the start // of every leaf command's Run function. func newController() *controller { // Resolve token from flag or environment. bearerToken := tokenFlag if bearerToken == "" { bearerToken = os.Getenv("MCIAS_TOKEN") } client, err := newHTTPClient(caCertPath) if err != nil { fatalf("build HTTP client: %v", err) } return &controller{ serverURL: strings.TrimRight(serverURL, "/"), token: bearerToken, client: client, } } // controller holds shared state for all subcommands. type controller struct { client *http.Client serverURL string token string } // ---- auth subcommands ---- func authCmd() *cobra.Command { cmd := &cobra.Command{ Use: "auth", Short: "Authentication commands", } cmd.AddCommand(authLoginCmd()) cmd.AddCommand(authChangePasswordCmd()) return cmd } // authLoginCmd authenticates with the 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 lists. // // Security: terminal echo is disabled during password entry // (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use. func authLoginCmd() *cobra.Command { var username string var totpCode string cmd := &cobra.Command{ Use: "login", Short: "Obtain a bearer token", Long: `Obtain a bearer token. Password is always prompted interactively (never accepted as a flag) to avoid shell-history exposure. Token is written to stdout; expiry to stderr. Example: export MCIAS_TOKEN=$(mciasctl auth login --username alice)`, Run: func(cmd *cobra.Command, args []string) { if username == "" { fatalf("auth login: --username is required") } c := newController() // 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) // newline after hidden input 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 } body := map[string]string{ "username": username, "password": passwd, } if totpCode != "" { body["totp_code"] = totpCode } var result struct { Token string `json:"token"` ExpiresAt string `json:"expires_at"` } c.doRequest("POST", "/v1/auth/login", body, &result) // Print token to stdout so it can be captured by scripts, e.g.: // export MCIAS_TOKEN=$(mciasctl auth login --username alice) fmt.Println(result.Token) if result.ExpiresAt != "" { fmt.Fprintf(os.Stderr, "expires: %s\n", result.ExpiresAt) } }, } cmd.Flags().StringVar(&username, "username", "", "username (required)") cmd.Flags().StringVar(&totpCode, "totp", "", "TOTP code (required if TOTP is enrolled)") return cmd } // authChangePasswordCmd allows an authenticated user to change their own password. // A valid bearer token must be set (via --token flag or MCIAS_TOKEN env var). // Both passwords are always prompted interactively; they are never accepted as // command-line flags to prevent them from appearing in shell history, ps // output, and process argument lists. // // Security: terminal echo is disabled during entry (golang.org/x/term); // raw byte slices are zeroed after use. The server requires the current // password to prevent token-theft attacks. On success all other active // sessions are revoked server-side. func authChangePasswordCmd() *cobra.Command { return &cobra.Command{ Use: "change-password", Short: "Change the current user's own password", Long: `Change the current user's own password. Requires a valid bearer token. Current and new passwords are always prompted interactively. Revokes all other active sessions on success.`, Run: func(cmd *cobra.Command, args []string) { c := newController() // Security: always prompt interactively; never accept passwords as flags. fmt.Fprint(os.Stderr, "Current password: ") rawCurrent, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read current password: %v", err) } currentPasswd := string(rawCurrent) for i := range rawCurrent { rawCurrent[i] = 0 } fmt.Fprint(os.Stderr, "New password: ") rawNew, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // uintptr==int on all target platforms fmt.Fprintln(os.Stderr) if err != nil { fatalf("read new password: %v", err) } newPasswd := string(rawNew) for i := range rawNew { rawNew[i] = 0 } body := map[string]string{ "current_password": currentPasswd, "new_password": newPasswd, } c.doRequest("PUT", "/v1/auth/password", body, nil) fmt.Println("password changed; other active sessions revoked") }, } } // ---- account subcommands ---- func accountCmd() *cobra.Command { cmd := &cobra.Command{ Use: "account", Short: "Account management commands", } cmd.AddCommand(accountListCmd()) cmd.AddCommand(accountCreateCmd()) cmd.AddCommand(accountGetCmd()) cmd.AddCommand(accountUpdateCmd()) cmd.AddCommand(accountDeleteCmd()) cmd.AddCommand(accountSetPasswordCmd()) return cmd } func accountListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List all accounts", Run: func(cmd *cobra.Command, args []string) { c := newController() var result []json.RawMessage c.doRequest("GET", "/v1/accounts", nil, &result) printJSON(result) }, } } func accountCreateCmd() *cobra.Command { var username string var accountType string cmd := &cobra.Command{ Use: "create", Short: "Create a new account", Run: func(cmd *cobra.Command, args []string) { if username == "" { fatalf("account create: --username is required") } c := newController() // Security: always prompt interactively for human-account passwords; never // accept them as a flag. Terminal echo is disabled; the raw byte slice is // zeroed after conversion to string. System accounts have no password. var passwd string if accountType == "human" { 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) for i := range raw { raw[i] = 0 } } body := map[string]string{ "username": username, "account_type": accountType, } if passwd != "" { body["password"] = passwd } var result json.RawMessage c.doRequest("POST", "/v1/accounts", body, &result) printJSON(result) }, } cmd.Flags().StringVar(&username, "username", "", "username (required)") cmd.Flags().StringVar(&accountType, "type", "human", "account type: human or system") return cmd } func accountGetCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "get", Short: "Get account details", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("account get: --id is required") } c := newController() var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+id, nil, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } func accountUpdateCmd() *cobra.Command { var id string var status string cmd := &cobra.Command{ Use: "update", Short: "Update an account", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("account update: --id is required") } if status == "" { fatalf("account update: --status is required") } c := newController() body := map[string]string{"status": status} c.doRequest("PATCH", "/v1/accounts/"+id, body, nil) fmt.Println("account updated") }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") cmd.Flags().StringVar(&status, "status", "", "new status: active or inactive") return cmd } func accountDeleteCmd() *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") } c := newController() c.doRequest("DELETE", "/v1/accounts/"+id, nil, nil) fmt.Println("account deleted") }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } // accountSetPasswordCmd resets a human account's password (admin operation). // No current password is required. All active sessions for the target account // are revoked by the server on success. // // Security: the new 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 lists. Terminal echo is disabled // (golang.org/x/term); the raw byte slice is zeroed after use. func accountSetPasswordCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "set-password", Short: "Reset a human account's password (admin)", Long: `Admin: reset a human account's password without requiring the current password. New password is always prompted interactively. Revokes all active sessions for the account.`, Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("account set-password: --id is required") } c := newController() // Security: always prompt interactively; never accept password as a flag. fmt.Fprint(os.Stderr, "New 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) for i := range raw { raw[i] = 0 } body := map[string]string{"new_password": passwd} c.doRequest("PUT", "/v1/accounts/"+id+"/password", body, nil) fmt.Println("password updated; all active sessions revoked") }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } // ---- role subcommands ---- func roleCmd() *cobra.Command { cmd := &cobra.Command{ Use: "role", Short: "Role management commands", } cmd.AddCommand(roleListCmd()) cmd.AddCommand(roleSetCmd()) cmd.AddCommand(roleGrantCmd()) cmd.AddCommand(roleRevokeCmd()) return cmd } func roleListCmd() *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") } c := newController() var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+id+"/roles", nil, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } func roleSetCmd() *cobra.Command { var id string var 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") } c := newController() roles := []string{} if rolesFlag != "" { for _, r := range strings.Split(rolesFlag, ",") { r = strings.TrimSpace(r) if r != "" { roles = append(roles, r) } } } body := map[string][]string{"roles": roles} c.doRequest("PUT", "/v1/accounts/"+id+"/roles", body, nil) fmt.Printf("roles set: %v\n", roles) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") cmd.Flags().StringVar(&rolesFlag, "roles", "", "comma-separated list of roles") return cmd } func roleGrantCmd() *cobra.Command { var id string var 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") } c := newController() body := map[string]string{"role": role} c.doRequest("POST", "/v1/accounts/"+id+"/roles", body, nil) fmt.Printf("role granted: %s\n", role) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") cmd.Flags().StringVar(&role, "role", "", "role name (required)") return cmd } func roleRevokeCmd() *cobra.Command { var id string var 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") } c := newController() c.doRequest("DELETE", "/v1/accounts/"+id+"/roles/"+role, nil, nil) 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 tokenCmd() *cobra.Command { cmd := &cobra.Command{ Use: "token", Short: "Token management commands", } cmd.AddCommand(tokenIssueCmd()) cmd.AddCommand(tokenRevokeCmd()) return cmd } func tokenIssueCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "issue", Short: "Issue a token for a system account", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("token issue: --id is required") } c := newController() body := map[string]string{"account_id": id} var result json.RawMessage c.doRequest("POST", "/v1/token/issue", body, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "system account UUID (required)") return cmd } func tokenRevokeCmd() *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") } c := newController() c.doRequest("DELETE", "/v1/token/"+jti, nil, nil) fmt.Println("token revoked") }, } cmd.Flags().StringVar(&jti, "jti", "", "JTI of the token to revoke (required)") return cmd } // ---- pgcreds subcommands ---- func pgcredsCmd() *cobra.Command { cmd := &cobra.Command{ Use: "pgcreds", Short: "Postgres credential management commands", } cmd.AddCommand(pgcredsListCmd()) cmd.AddCommand(pgcredsGetCmd()) cmd.AddCommand(pgcredsSetCmd()) return cmd } func pgcredsListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List all Postgres credentials", Run: func(cmd *cobra.Command, args []string) { c := newController() var result json.RawMessage c.doRequest("GET", "/v1/pgcreds", nil, &result) printJSON(result) }, } } func pgcredsGetCmd() *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") } c := newController() var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+id+"/pgcreds", nil, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } func pgcredsSetCmd() *cobra.Command { var id string var host string var port int var dbName string var username string var 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 == "" { fatalf("pgcreds set: --id, --host, --db, and --user are required") } c := newController() // Prompt for the Postgres password interactively if not supplied so it // stays out of shell history. // Security: terminal echo is disabled during entry; the raw byte slice is // zeroed after conversion to string. passwd := password if passwd == "" { fmt.Fprint(os.Stderr, "Postgres 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) for i := range raw { raw[i] = 0 } } body := map[string]interface{}{ "host": host, "port": port, "database": dbName, "username": username, "password": passwd, } c.doRequest("PUT", "/v1/accounts/"+id+"/pgcreds", body, nil) 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 (prompted if omitted)") return cmd } // ---- policy subcommands ---- func policyCmd() *cobra.Command { cmd := &cobra.Command{ Use: "policy", Short: "Policy rule management commands", } cmd.AddCommand(policyListCmd()) cmd.AddCommand(policyCreateCmd()) cmd.AddCommand(policyGetCmd()) cmd.AddCommand(policyUpdateCmd()) cmd.AddCommand(policyDeleteCmd()) return cmd } func policyListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List all policy rules", Run: func(cmd *cobra.Command, args []string) { c := newController() var result json.RawMessage c.doRequest("GET", "/v1/policy/rules", nil, &result) printJSON(result) }, } } func policyCreateCmd() *cobra.Command { var description string var jsonFile string var priority int var notBefore string var expiresAt string cmd := &cobra.Command{ Use: "create", Short: "Create a new 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)") } c := newController() // 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 json.RawMessage if err := json.Unmarshal(ruleBytes, &ruleBody); err != nil { fatalf("policy create: invalid JSON in %s: %v", jsonFile, err) } body := map[string]interface{}{ "description": description, "priority": priority, "rule": ruleBody, } if notBefore != "" { if _, err := time.Parse(time.RFC3339, notBefore); err != nil { fatalf("policy create: --not-before must be RFC3339: %v", err) } body["not_before"] = notBefore } if expiresAt != "" { if _, err := time.Parse(time.RFC3339, expiresAt); err != nil { fatalf("policy create: --expires-at must be RFC3339: %v", err) } body["expires_at"] = expiresAt } var result json.RawMessage c.doRequest("POST", "/v1/policy/rules", body, &result) printJSON(result) }, } 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 policyGetCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "get", Short: "Get a policy rule", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("policy get: --id is required") } c := newController() var result json.RawMessage c.doRequest("GET", "/v1/policy/rules/"+id, nil, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "rule ID (required)") return cmd } func policyUpdateCmd() *cobra.Command { var id string var priority int var enabled string var notBefore string var expiresAt string var clearNotBefore bool var clearExpiresAt bool cmd := &cobra.Command{ Use: "update", Short: "Update a policy rule", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("policy update: --id is required") } c := newController() body := map[string]interface{}{} if priority >= 0 { body["priority"] = priority } if enabled != "" { switch enabled { case "true": b := true body["enabled"] = b case "false": b := false body["enabled"] = b default: fatalf("policy update: --enabled must be true or false") } } if clearNotBefore { body["clear_not_before"] = true } else if notBefore != "" { if _, err := time.Parse(time.RFC3339, notBefore); err != nil { fatalf("policy update: --not-before must be RFC3339: %v", err) } body["not_before"] = notBefore } if clearExpiresAt { body["clear_expires_at"] = true } else if expiresAt != "" { if _, err := time.Parse(time.RFC3339, expiresAt); err != nil { fatalf("policy update: --expires-at must be RFC3339: %v", err) } body["expires_at"] = expiresAt } if len(body) == 0 { fatalf("policy update: at least one flag is required") } var result json.RawMessage c.doRequest("PATCH", "/v1/policy/rules/"+id, body, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "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 policyDeleteCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "delete", Short: "Delete a policy rule", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("policy delete: --id is required") } c := newController() c.doRequest("DELETE", "/v1/policy/rules/"+id, nil, nil) fmt.Println("policy rule deleted") }, } cmd.Flags().StringVar(&id, "id", "", "rule ID (required)") return cmd } // ---- tag subcommands ---- func tagCmd() *cobra.Command { cmd := &cobra.Command{ Use: "tag", Short: "Account tag management commands", } cmd.AddCommand(tagListCmd()) cmd.AddCommand(tagSetCmd()) return cmd } func tagListCmd() *cobra.Command { var id string cmd := &cobra.Command{ Use: "list", Short: "List tags for an account", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("tag list: --id is required") } c := newController() var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+id+"/tags", nil, &result) printJSON(result) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") return cmd } func tagSetCmd() *cobra.Command { var id string var tagsFlag string cmd := &cobra.Command{ Use: "set", Short: "Set tags for an account", Long: "Set tags for an account. Pass empty --tags \"\" to clear all tags.", Run: func(cmd *cobra.Command, args []string) { if id == "" { fatalf("tag set: --id is required") } c := newController() tags := []string{} if tagsFlag != "" { for _, t := range strings.Split(tagsFlag, ",") { t = strings.TrimSpace(t) if t != "" { tags = append(tags, t) } } } body := map[string][]string{"tags": tags} c.doRequest("PUT", "/v1/accounts/"+id+"/tags", body, nil) fmt.Printf("tags set: %v\n", tags) }, } cmd.Flags().StringVar(&id, "id", "", "account UUID (required)") cmd.Flags().StringVar(&tagsFlag, "tags", "", "comma-separated list of tags (empty string clears all tags)") return cmd } // ---- HTTP helpers ---- // doRequest performs an authenticated JSON HTTP request. If result is non-nil, // the response body is decoded into it. Exits on error. func (c *controller) doRequest(method, path string, body, result interface{}) { url := c.serverURL + path var bodyReader *strings.Reader if body != nil { b, err := json.Marshal(body) if err != nil { fatalf("marshal request body: %v", err) } bodyReader = strings.NewReader(string(b)) } else { bodyReader = strings.NewReader("") } req, err := http.NewRequest(method, url, bodyReader) if err != nil { fatalf("create request: %v", err) } req.Header.Set("Content-Type", "application/json") if c.token != "" { req.Header.Set("Authorization", "Bearer "+c.token) } resp, err := c.client.Do(req) if err != nil { fatalf("HTTP %s %s: %v", method, path, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 400 { var errBody map[string]string _ = json.NewDecoder(resp.Body).Decode(&errBody) msg := errBody["error"] if msg == "" { msg = resp.Status } fatalf("server returned %d: %s", resp.StatusCode, msg) } if result != nil && resp.StatusCode != http.StatusNoContent { if err := json.NewDecoder(resp.Body).Decode(result); err != nil { fatalf("decode response: %v", err) } } } // newHTTPClient builds an http.Client with optional custom CA certificate. // Security: TLS 1.2+ is required; the system CA pool is used by default. func newHTTPClient(caCertPath string) (*http.Client, error) { tlsCfg := &tls.Config{ MinVersion: tls.VersionTLS12, } if caCertPath != "" { // G304: path comes from a CLI flag supplied by the operator, not from // untrusted input. File inclusion is intentional. pemData, err := os.ReadFile(caCertPath) //nolint:gosec if err != nil { return nil, fmt.Errorf("read CA cert: %w", err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("no valid certificates found in %s", caCertPath) } tlsCfg.RootCAs = pool } return &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsCfg, }, Timeout: 30 * time.Second, }, nil } // printJSON pretty-prints a JSON value to stdout. func printJSON(v interface{}) { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") if err := enc.Encode(v); err != nil { fatalf("encode output: %v", err) } } // fatalf prints an error message and exits with code 1. func fatalf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "mciasctl: "+format+"\n", args...) os.Exit(1) }