Files
mcias/cmd/mciasgrpcctl/main.go
Kyle Isom 5b5e1a7ed6 Use mcdsl/terminal for all password prompts
Replace direct golang.org/x/term calls with mcdsl/terminal.ReadPassword
across mciasctl (6 sites), mciasgrpcctl (1 site), and mciasdb (1 site).
Aligns with the new CLI security standard in engineering-standards.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 11:40:11 -07:00

1047 lines
28 KiB
Go

// Command mciasgrpcctl is the MCIAS gRPC admin CLI.
//
// It connects to a running mciassrv gRPC listener and provides subcommands for
// managing accounts, roles, tokens, Postgres credentials, and policy rules via
// the gRPC API.
//
// Usage:
//
// mciasgrpcctl [global flags] <command> [args]
//
// 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)
//
// Commands:
//
// health
// pubkey
//
// 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
//
// 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
//
// 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
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"git.wntrmute.dev/mc/mcdsl/terminal"
mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1"
)
// 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 <token>".
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)
}
func main() {
var (
serverAddr string
tokenFlag string
caCert string
)
ctl := &controller{}
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)
}
}
// ---- health / pubkey ----
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,
})
},
}
}
// ---- auth subcommands ----
func authCmd(ctl *controller) *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Authentication commands",
}
cmd.AddCommand(authLoginCmd(ctl))
cmd.AddCommand(authLogoutCmd(ctl))
return cmd
}
// 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
// lists.
//
// Security: terminal echo is disabled during password entry
// (mcdsl/terminal.ReadPassword).
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")
}
// 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.
passwd, err := terminal.ReadPassword("Password: ")
if err != nil {
fatalf("read password: %v", err)
}
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()
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))
}
},
}
cmd.Flags().StringVar(&username, "username", "", "username (required)")
cmd.Flags().StringVar(&totpCode, "totp", "", "TOTP code (required if TOTP is enrolled)")
return cmd
}
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)
}
fmt.Println("logged out")
},
}
}
// ---- account subcommands ----
func accountCmd(ctl *controller) *cobra.Command {
cmd := &cobra.Command{
Use: "account",
Short: "Account management commands",
}
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 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")
}
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)
},
}
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 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")
}
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)
},
}
cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
return cmd
}
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")
}
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")
},
}
cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
cmd.Flags().StringVar(&status, "status", "", "new status: active or inactive (required)")
return cmd
}
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")
}
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")
},
}
cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
return cmd
}
// ---- role subcommands ----
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 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(ctl.conn)
ctx, cancel := ctl.callCtx()
defer cancel()
resp, err := cl.GetRoles(ctx, &mciasv1.GetRolesRequest{Id: id})
if err != nil {
fatalf("role list: %v", err)
}
printJSON(resp.Roles)
},
}
cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
return cmd
}
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")
}
var roles []string
if rolesFlag != "" {
for _, r := range strings.Split(rolesFlag, ",") {
r = strings.TrimSpace(r)
if r != "" {
roles = append(roles, r)
}
}
}
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)
}
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(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")
}
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)
}
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(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 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 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")
}
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,
})
},
}
cmd.Flags().StringVar(&tok, "token", "", "JWT to validate (required)")
return cmd
}
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")
}
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})
},
}
cmd.Flags().StringVar(&id, "id", "", "system account UUID (required)")
return cmd
}
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")
}
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")
},
}
cmd.Flags().StringVar(&jti, "jti", "", "JTI of the token to revoke (required)")
return cmd
}
// ---- pgcreds subcommands ----
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 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")
}
cl := mciasv1.NewCredentialServiceClient(ctl.conn)
ctx, cancel := ctl.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,
})
},
}
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 policyCmd(ctl *controller) *cobra.Command {
cmd := &cobra.Command{
Use: "policy",
Short: "Policy rule management commands",
}
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 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)")
}
// 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)
},
}
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(&notBefore, "not-before", "", "earliest activation time (RFC3339, optional)")
cmd.Flags().StringVar(&expiresAt, "expires-at", "", "expiry time (RFC3339, optional)")
return cmd
}
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")
}
cl := mciasv1.NewPolicyServiceClient(ctl.conn)
ctx, cancel := ctl.callCtx()
defer cancel()
resp, err := cl.GetPolicyRule(ctx, &mciasv1.GetPolicyRuleRequest{Id: id})
if err != nil {
fatalf("policy get: %v", err)
}
printJSON(resp.Rule)
},
}
cmd.Flags().StringVar(&idStr, "id", "", "rule ID (required)")
return cmd
}
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")
}
req := &mciasv1.UpdatePolicyRuleRequest{
Id: id,
ClearNotBefore: clearNotBefore,
ClearExpiresAt: clearExpiresAt,
}
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(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)
},
}
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(&notBefore, "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(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")
}
cl := mciasv1.NewPolicyServiceClient(ctl.conn)
ctx, cancel := ctl.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 ----
// 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)
}