Implement Phase 7: gRPC dual-stack interface
- proto/mcias/v1/: AdminService, AuthService, TokenService, AccountService, CredentialService; generated Go stubs in gen/ - internal/grpcserver: full handler implementations sharing all business logic (auth, token, db, crypto) with REST server; interceptor chain: logging -> auth (JWT alg-first + revocation) -> rate-limit (token bucket, 10 req/s, burst 10, per-IP) - internal/config: optional grpc_addr field in [server] section - cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr when configured; graceful shutdown of both servers in 15s window - cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands (health, pubkey, account, role, token, pgcreds) using TLS with optional custom CA cert - internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering public RPCs, auth interceptor (no token, invalid, revoked -> 401), non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows, AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip, credential fields absent from all responses Security: JWT validation path identical to REST: alg header checked before signature, alg:none rejected, revocation table checked after sig. Authorization metadata value never logged by any interceptor. Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from all proto response messages — enforced by proto design and confirmed by test TestCredentialFieldsAbsentFromAccountResponse. Login dummy-Argon2 timing guard preserves timing uniformity for unknown users (same as REST handleLogin). TLS required at listener level; cmd/mciassrv uses credentials.NewServerTLSFromFile; no h2c offered. 137 tests pass, zero race conditions (go test -race ./...)
This commit is contained in:
602
cmd/mciasgrpcctl/main.go
Normal file
602
cmd/mciasgrpcctl/main.go
Normal file
@@ -0,0 +1,602 @@
|
||||
// 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] <command> [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 <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)
|
||||
}
|
||||
|
||||
// ---- 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),
|
||||
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] <command> [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
|
||||
`)
|
||||
}
|
||||
Reference in New Issue
Block a user