// Command mciasgrpcctl is the MCIAS gRPC admin CLI. // // It connects to a running mciassrv gRPC listener and provides subcommands for // managing accounts, roles, tokens, and Postgres credentials via the gRPC API. // // Usage: // // mciasgrpcctl [global flags] [args] // // Global flags: // // -server gRPC server address (default: localhost:9443) // -token Bearer token for authentication (or set MCIAS_TOKEN env var) // -cacert Path to CA certificate for TLS verification (optional) // // Commands: // // health // pubkey // // 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 package main import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "flag" "fmt" "os" "strings" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1" ) func main() { // Global flags. serverAddr := flag.String("server", "localhost: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 "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 the shared gRPC connection and token for all subcommands. type controller struct { conn *grpc.ClientConn token string } // authCtx returns a context with the Bearer token injected as gRPC metadata. // Security: token is placed in the "authorization" key per the gRPC convention // that mirrors the HTTP Authorization header. Value is never logged. func (c *controller) authCtx() context.Context { ctx := context.Background() if c.token == "" { return ctx } // Security: metadata key "authorization" matches the server-side // extractBearerFromMD expectation; value is "Bearer ". return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+c.token) } // callCtx returns an authCtx with a 30-second deadline. func (c *controller) callCtx() (context.Context, context.CancelFunc) { return context.WithTimeout(c.authCtx(), 30*time.Second) } // ---- health / pubkey ---- func (c *controller) runHealth() { adminCl := mciasv1.NewAdminServiceClient(c.conn) ctx, cancel := c.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 (c *controller) runPubKey() { adminCl := mciasv1.NewAdminServiceClient(c.conn) ctx, cancel := c.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, }) } // ---- 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() { cl := mciasv1.NewAccountServiceClient(c.conn) ctx, cancel := c.callCtx() defer cancel() resp, err := cl.ListAccounts(ctx, &mciasv1.ListAccountsRequest{}) if err != nil { fatalf("account list: %v", err) } printJSON(resp.Accounts) } 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") } 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) } 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") } 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) } 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) if *id == "" { fatalf("account update: -id is required") } 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") } // ---- 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") } 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) } } } cl := mciasv1.NewAccountServiceClient(c.conn) ctx, cancel := c.callCtx() defer cancel() _, 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) } // ---- 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 (c *controller) tokenValidate(args []string) { fs := flag.NewFlagSet("token validate", flag.ExitOnError) tok := fs.String("token", "", "JWT to validate (required)") _ = fs.Parse(args) if *tok == "" { fatalf("token validate: -token is required") } 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, }) } 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") } 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}) } 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") } 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") } // ---- 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") } 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, }, }) if err != nil { fatalf("pgcreds set: %v", err) } fmt.Println("credentials stored") } // ---- gRPC connection ---- // newGRPCConn dials the gRPC server with TLS. // If caCertPath is empty, the system CA pool is used. // Security: TLS 1.2+ is enforced by the crypto/tls defaults on the client side. // The connection is insecure-skip-verify-free; operators can supply a custom CA // for self-signed certs without disabling certificate validation. func newGRPCConn(serverAddr, caCertPath string) (*grpc.ClientConn, 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 } creds := credentials.NewTLS(tlsCfg) conn, err := grpc.NewClient(serverAddr, grpc.WithTransportCredentials(creds)) if err != nil { return nil, fmt.Errorf("dial %s: %w", serverAddr, err) } return conn, nil } // ---- helpers ---- // printJSON pretty-prints a value as JSON 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 to stderr and exits with code 1. 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: localhost:9443) -token Bearer token (or set MCIAS_TOKEN env var) -cacert Path to CA certificate for TLS verification Commands: health pubkey 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 `) }