// Command mciasctl is the MCIAS admin CLI. // // It connects to a running mciassrv instance and provides subcommands for // managing accounts, roles, tokens, and Postgres credentials. // // Usage: // // mciasctl [global flags] [args] // // Global flags: // // -server URL of the mciassrv instance (default: https://localhost:8443) // -token Bearer token for authentication (or set MCIAS_TOKEN env var) // -cacert Path to CA certificate for TLS verification (optional) // // Commands: // // 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 issue -id UUID // token revoke -jti JTI // // pgcreds set -id UUID -host HOST -port PORT -db DB -user USER -password PASS // pgcreds get -id UUID package main import ( "crypto/tls" "crypto/x509" "encoding/json" "flag" "fmt" "net/http" "os" "strings" "time" ) func main() { // Global flags. serverURL := flag.String("server", "https://localhost:8443", "mciassrv base URL") 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 HTTP client. client, err := newHTTPClient(*caCert) if err != nil { fatalf("build HTTP client: %v", err) } ctl := &controller{ serverURL: strings.TrimRight(*serverURL, "/"), token: bearerToken, client: client, } command := args[0] subArgs := args[1:] switch command { case "account": ctl.runAccount(subArgs) case "role": ctl.runRole(subArgs) case "token": ctl.runToken(subArgs) case "pgcreds": ctl.runPGCreds(subArgs) default: fatalf("unknown command %q; run with no args to see usage", command) } } // controller holds shared state for all subcommands. type controller struct { client *http.Client serverURL string token string } // ---- account subcommands ---- func (c *controller) runAccount(args []string) { if len(args) == 0 { fatalf("account requires a subcommand: list, create, get, update, delete") } 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]) } } func (c *controller) accountList() { var result []json.RawMessage c.doRequest("GET", "/v1/accounts", nil, &result) printJSON(result) } 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) if *username == "" { fatalf("account create: -username is required") } body := map[string]string{ "username": *username, "account_type": *accountType, } if *password != "" { body["password"] = *password } var result json.RawMessage c.doRequest("POST", "/v1/accounts", body, &result) printJSON(result) } func (c *controller) accountGet(args []string) { fs := flag.NewFlagSet("account get", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("account get: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id, nil, &result) printJSON(result) } 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") _ = fs.Parse(args) if *id == "" { fatalf("account update: -id is required") } if *status == "" { fatalf("account update: -status is required") } body := map[string]string{"status": *status} c.doRequest("PATCH", "/v1/accounts/"+*id, body, nil) 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") } c.doRequest("DELETE", "/v1/accounts/"+*id, nil, nil) fmt.Println("account deleted") } // ---- role subcommands ---- func (c *controller) runRole(args []string) { if len(args) == 0 { fatalf("role requires a subcommand: list, set") } switch args[0] { case "list": c.roleList(args[1:]) case "set": c.roleSet(args[1:]) default: fatalf("unknown role subcommand %q", args[0]) } } 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") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id+"/roles", nil, &result) printJSON(result) } 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") } 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) } // ---- token subcommands ---- func (c *controller) runToken(args []string) { if len(args) == 0 { fatalf("token requires a subcommand: issue, revoke") } switch args[0] { case "issue": c.tokenIssue(args[1:]) case "revoke": c.tokenRevoke(args[1:]) default: fatalf("unknown token subcommand %q", args[0]) } } func (c *controller) tokenIssue(args []string) { fs := flag.NewFlagSet("token issue", flag.ExitOnError) id := fs.String("id", "", "system account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("token issue: -id is required") } body := map[string]string{"account_id": *id} var result json.RawMessage c.doRequest("POST", "/v1/token/issue", body, &result) printJSON(result) } 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) if *jti == "" { fatalf("token revoke: -jti is required") } c.doRequest("DELETE", "/v1/token/"+*jti, nil, nil) fmt.Println("token revoked") } // ---- 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 (c *controller) pgCredsGet(args []string) { fs := flag.NewFlagSet("pgcreds get", flag.ExitOnError) id := fs.String("id", "", "account UUID (required)") _ = fs.Parse(args) if *id == "" { fatalf("pgcreds get: -id is required") } var result json.RawMessage c.doRequest("GET", "/v1/accounts/"+*id+"/pgcreds", nil, &result) printJSON(result) } 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") } body := map[string]interface{}{ "host": *host, "port": *port, "database": *dbName, "username": *username, "password": *password, } c.doRequest("PUT", "/v1/accounts/"+*id+"/pgcreds", body, nil) fmt.Println("credentials stored") } // ---- 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) } func usage() { fmt.Fprintf(os.Stderr, `mciasctl - MCIAS admin CLI Usage: mciasctl [global flags] [args] Global flags: -server URL of the mciassrv instance (default: https://localhost:8443) -token Bearer token (or set MCIAS_TOKEN env var) -cacert Path to CA certificate for TLS verification Commands: 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 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 `) }