From 7ede54afb2e8b3b62d9acd6a001151a31bf22961 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 12 Mar 2026 20:51:10 -0700 Subject: [PATCH] grpcctl: add auth login and policy commands - Add auth/login and auth/logout to mciasgrpcctl, calling the existing AuthService.Login/Logout RPCs; password is always prompted interactively (term.ReadPassword), never accepted as a flag, raw bytes zeroed after use - Add proto/mcias/v1/policy.proto with PolicyService (List, Create, Get, Update, Delete policy rules) - Regenerate gen/mcias/v1/ stubs to include policy - Implement internal/grpcserver/policyservice.go delegating to the same db layer as the REST policy handlers - Register PolicyService in grpcserver.go - Add policy list/create/get/update/delete to mciasgrpcctl - Update mciasgrpcctl man page with new commands Security: auth login uses the same interactive password prompt pattern as mciasctl; password never appears in process args, shell history, or logs; raw bytes zeroed after string conversion (same as REST CLI and REST server). Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 4 + README.md | 10 +- cmd/mciasctl/main.go | 6 +- cmd/mciasgrpcctl/main.go | 326 ++++++++++- gen/mcias/v1/account.pb.go | 2 +- gen/mcias/v1/account_grpc.pb.go | 2 +- gen/mcias/v1/admin.pb.go | 2 +- gen/mcias/v1/admin_grpc.pb.go | 2 +- gen/mcias/v1/auth.pb.go | 2 +- gen/mcias/v1/auth_grpc.pb.go | 2 +- gen/mcias/v1/common.pb.go | 2 +- gen/mcias/v1/policy.pb.go | 779 +++++++++++++++++++++++++++ gen/mcias/v1/policy_grpc.pb.go | 299 ++++++++++ gen/mcias/v1/token.pb.go | 2 +- gen/mcias/v1/token_grpc.pb.go | 2 +- internal/grpcserver/grpcserver.go | 1 + internal/grpcserver/policyservice.go | 278 ++++++++++ man/man1/mciasctl.1 | 2 +- man/man1/mciasgrpcctl.1 | 31 +- proto/generate.go | 2 +- proto/mcias/v1/policy.proto | 104 ++++ 21 files changed, 1835 insertions(+), 25 deletions(-) create mode 100644 gen/mcias/v1/policy.pb.go create mode 100644 gen/mcias/v1/policy_grpc.pb.go create mode 100644 internal/grpcserver/policyservice.go create mode 100644 proto/mcias/v1/policy.proto diff --git a/Makefile b/Makefile index 3e34148..4565481 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,10 @@ dist: man docker: docker build -t mcias:$(VERSION) -t mcias:latest . +.PHONY: install-local +install-local: build + cp bin/* $(HOME)/.local/bin/ + # --------------------------------------------------------------------------- # Help # --------------------------------------------------------------------------- diff --git a/README.md b/README.md index 4c64405..996ac61 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ MCIAS_MASTER_PASSPHRASE=your-passphrase mciassrv -config /etc/mcias/mcias.conf ### 6. Verify ```sh -curl -k https://localhost:8443/v1/health +curl -k https://mcias.metacircular.net:8443/v1/health # {"status":"ok"} ``` @@ -173,11 +173,11 @@ make docker # build Docker image mcias: ## Admin CLI (mciasctl) ```sh -TOKEN=$(curl -sk https://localhost:8443/v1/auth/login \ +TOKEN=$(curl -sk https://mcias.metacircular.net:8443/v1/auth/login \ -d '{"username":"admin","password":"..."}' | jq -r .token) export MCIAS_TOKEN=$TOKEN -mciasctl -server https://localhost:8443 account list +mciasctl -server https://mcias.metacircular.net:8443 account list mciasctl account create -username alice # password prompted interactively mciasctl role set -id $UUID -roles admin mciasctl token issue -id $SYSTEM_UUID @@ -245,7 +245,7 @@ See `man mciasgrpcctl` and [ARCHITECTURE.md](ARCHITECTURE.md) §17. ## Web Management UI mciassrv includes a built-in web interface for day-to-day administration. -After starting the server, navigate to `https://localhost:8443/login` and +After starting the server, navigate to `https://mcias.metacircular.net:8443/login` and log in with an admin account. The UI provides: @@ -278,7 +278,7 @@ docker run -d \ -p 9443:9443 \ mcias:latest -curl -k https://localhost:8443/v1/health +curl -k https://mcias.metacircular.net:8443/v1/health ``` The container runs as uid 10001 (mcias) with no capabilities. diff --git a/cmd/mciasctl/main.go b/cmd/mciasctl/main.go index dd0eb60..f25e3fd 100644 --- a/cmd/mciasctl/main.go +++ b/cmd/mciasctl/main.go @@ -10,7 +10,7 @@ // // Global flags: // -// -server URL of the mciassrv instance (default: https://localhost:8443) +// -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) // @@ -61,7 +61,7 @@ import ( func main() { // Global flags. - serverURL := flag.String("server", "https://localhost:8443", "mciassrv base URL") + serverURL := flag.String("server", "https://mcias.metacircular.net: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 @@ -871,7 +871,7 @@ func usage() { Usage: mciasctl [global flags] [args] Global flags: - -server URL of the mciassrv instance (default: https://localhost:8443) + -server URL of the mciassrv instance (default: https://mcias.metacircular.net:8443) -token Bearer token (or set MCIAS_TOKEN env var) -cacert Path to CA certificate for TLS verification diff --git a/cmd/mciasgrpcctl/main.go b/cmd/mciasgrpcctl/main.go index c9aa3aa..71be05d 100644 --- a/cmd/mciasgrpcctl/main.go +++ b/cmd/mciasgrpcctl/main.go @@ -1,7 +1,8 @@ // 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. +// managing accounts, roles, tokens, Postgres credentials, and policy rules via +// the gRPC API. // // Usage: // @@ -9,7 +10,7 @@ // // Global flags: // -// -server gRPC server address (default: localhost:9443) +// -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) // @@ -18,6 +19,9 @@ // 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 @@ -33,6 +37,12 @@ // // 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 ( @@ -43,9 +53,11 @@ import ( "flag" "fmt" "os" + "strconv" "strings" "time" + "golang.org/x/term" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" @@ -55,7 +67,7 @@ import ( func main() { // Global flags. - serverAddr := flag.String("server", "localhost:9443", "gRPC server address (host:port)") + serverAddr := flag.String("server", "mcias.metacircular.net: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 @@ -93,6 +105,8 @@ func main() { ctl.runHealth() case "pubkey": ctl.runPubKey() + case "auth": + ctl.runAuth(subArgs) case "account": ctl.runAccount(subArgs) case "role": @@ -101,6 +115,8 @@ func main() { ctl.runToken(subArgs) case "pgcreds": ctl.runPGCreds(subArgs) + case "policy": + ctl.runPolicy(subArgs) default: fatalf("unknown command %q; run with no args to see usage", command) } @@ -162,6 +178,89 @@ func (c *controller) runPubKey() { }) } +// ---- auth subcommands ---- + +func (c *controller) runAuth(args []string) { + if len(args) == 0 { + fatalf("auth requires a subcommand: login, logout") + } + switch args[0] { + case "login": + c.authLogin(args[1:]) + case "logout": + c.authLogout() + default: + fatalf("unknown auth subcommand %q", args[0]) + } +} + +// authLogin 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 +// (golang.org/x/term.ReadPassword); the raw byte slice is zeroed after use. +func (c *controller) authLogin(args []string) { + fs := flag.NewFlagSet("auth login", flag.ExitOnError) + username := fs.String("username", "", "username (required)") + totpCode := fs.String("totp", "", "TOTP code (required if TOTP is enrolled)") + _ = fs.Parse(args) + + 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. + 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) + // Zero the raw byte slice once copied into the string. + for i := range raw { + raw[i] = 0 + } + + authCl := mciasv1.NewAuthServiceClient(c.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)) + } +} + +// authLogout revokes the caller's current JWT via the gRPC AuthService. +func (c *controller) authLogout() { + authCl := mciasv1.NewAuthServiceClient(c.conn) + ctx, cancel := c.callCtx() + defer cancel() + + if _, err := authCl.Logout(ctx, &mciasv1.LogoutRequest{}); err != nil { + fatalf("auth logout: %v", err) + } + fmt.Println("logged out") +} + // ---- account subcommands ---- func (c *controller) runAccount(args []string) { @@ -518,6 +617,208 @@ func (c *controller) pgCredsSet(args []string) { fmt.Println("credentials stored") } +// ---- policy subcommands ---- + +func (c *controller) runPolicy(args []string) { + if len(args) == 0 { + fatalf("policy requires a subcommand: list, create, get, update, delete") + } + switch args[0] { + case "list": + c.policyList() + case "create": + c.policyCreate(args[1:]) + case "get": + c.policyGet(args[1:]) + case "update": + c.policyUpdate(args[1:]) + case "delete": + c.policyDelete(args[1:]) + default: + fatalf("unknown policy subcommand %q", args[0]) + } +} + +func (c *controller) policyList() { + cl := mciasv1.NewPolicyServiceClient(c.conn) + ctx, cancel := c.callCtx() + defer cancel() + + resp, err := cl.ListPolicyRules(ctx, &mciasv1.ListPolicyRulesRequest{}) + if err != nil { + fatalf("policy list: %v", err) + } + printJSON(resp.Rules) +} + +func (c *controller) policyCreate(args []string) { + fs := flag.NewFlagSet("policy create", flag.ExitOnError) + description := fs.String("description", "", "rule description (required)") + jsonFile := fs.String("json", "", "path to JSON file containing the rule body (required)") + priority := fs.Int("priority", 100, "rule priority (lower = evaluated first)") + notBefore := fs.String("not-before", "", "earliest activation time (RFC3339, optional)") + expiresAt := fs.String("expires-at", "", "expiry time (RFC3339, optional)") + _ = fs.Parse(args) + + 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(c.conn) + ctx, cancel := c.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) +} + +func (c *controller) policyGet(args []string) { + fs := flag.NewFlagSet("policy get", flag.ExitOnError) + idStr := fs.String("id", "", "rule ID (required)") + _ = fs.Parse(args) + + 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(c.conn) + ctx, cancel := c.callCtx() + defer cancel() + + resp, err := cl.GetPolicyRule(ctx, &mciasv1.GetPolicyRuleRequest{Id: id}) + if err != nil { + fatalf("policy get: %v", err) + } + printJSON(resp.Rule) +} + +func (c *controller) policyUpdate(args []string) { + fs := flag.NewFlagSet("policy update", flag.ExitOnError) + idStr := fs.String("id", "", "rule ID (required)") + priority := fs.Int("priority", -1, "new priority (-1 = no change)") + enabled := fs.String("enabled", "", "true or false") + notBefore := fs.String("not-before", "", "earliest activation time (RFC3339)") + expiresAt := fs.String("expires-at", "", "expiry time (RFC3339)") + clearNotBefore := fs.Bool("clear-not-before", false, "remove not_before constraint") + clearExpiresAt := fs.Bool("clear-expires-at", false, "remove expires_at constraint") + _ = fs.Parse(args) + + 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(c.conn) + ctx, cancel := c.callCtx() + defer cancel() + + resp, err := cl.UpdatePolicyRule(ctx, req) + if err != nil { + fatalf("policy update: %v", err) + } + printJSON(resp.Rule) +} + +func (c *controller) policyDelete(args []string) { + fs := flag.NewFlagSet("policy delete", flag.ExitOnError) + idStr := fs.String("id", "", "rule ID (required)") + _ = fs.Parse(args) + + 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(c.conn) + ctx, cancel := c.callCtx() + defer cancel() + + if _, err := cl.DeletePolicyRule(ctx, &mciasv1.DeletePolicyRuleRequest{Id: id}); err != nil { + fatalf("policy delete: %v", err) + } + fmt.Println("policy rule deleted") +} + // ---- gRPC connection ---- // newGRPCConn dials the gRPC server with TLS. @@ -575,7 +876,7 @@ func usage() { Usage: mciasgrpcctl [global flags] [args] Global flags: - -server gRPC server address (default: localhost:9443) + -server gRPC server address (default: mcias.metacircular.net:9443) -token Bearer token (or set MCIAS_TOKEN env var) -cacert Path to CA certificate for TLS verification @@ -583,6 +884,12 @@ Commands: health pubkey + auth login -username NAME [-totp CODE] + Obtain a bearer token. Password is always prompted interactively. + Token is written to stdout; expiry to stderr. + Example: export MCIAS_TOKEN=$(mciasgrpcctl auth login -username alice) + auth logout Revoke the current bearer token. + account list account create -username NAME -password PASS [-type human|system] account get -id UUID @@ -598,5 +905,16 @@ Commands: 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] + FILE must contain a JSON rule body, e.g.: + {"effect":"allow","actions":["pgcreds:read"],"resource_type":"pgcreds","owner_matches_subject":true} + 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 `) } diff --git a/gen/mcias/v1/account.pb.go b/gen/mcias/v1/account.pb.go index deee4db..c696ece 100644 --- a/gen/mcias/v1/account.pb.go +++ b/gen/mcias/v1/account.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.4 +// protoc v3.20.3 // source: mcias/v1/account.proto package mciasv1 diff --git a/gen/mcias/v1/account_grpc.pb.go b/gen/mcias/v1/account_grpc.pb.go index 99a8b0f..2127f1e 100644 --- a/gen/mcias/v1/account_grpc.pb.go +++ b/gen/mcias/v1/account_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v6.33.4 +// - protoc v3.20.3 // source: mcias/v1/account.proto package mciasv1 diff --git a/gen/mcias/v1/admin.pb.go b/gen/mcias/v1/admin.pb.go index 3f2da7b..4e7f377 100644 --- a/gen/mcias/v1/admin.pb.go +++ b/gen/mcias/v1/admin.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.4 +// protoc v3.20.3 // source: mcias/v1/admin.proto package mciasv1 diff --git a/gen/mcias/v1/admin_grpc.pb.go b/gen/mcias/v1/admin_grpc.pb.go index ddeabba..8d5ab3b 100644 --- a/gen/mcias/v1/admin_grpc.pb.go +++ b/gen/mcias/v1/admin_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v6.33.4 +// - protoc v3.20.3 // source: mcias/v1/admin.proto package mciasv1 diff --git a/gen/mcias/v1/auth.pb.go b/gen/mcias/v1/auth.pb.go index 1bdd755..9600107 100644 --- a/gen/mcias/v1/auth.pb.go +++ b/gen/mcias/v1/auth.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.4 +// protoc v3.20.3 // source: mcias/v1/auth.proto package mciasv1 diff --git a/gen/mcias/v1/auth_grpc.pb.go b/gen/mcias/v1/auth_grpc.pb.go index eda8857..7d5089f 100644 --- a/gen/mcias/v1/auth_grpc.pb.go +++ b/gen/mcias/v1/auth_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v6.33.4 +// - protoc v3.20.3 // source: mcias/v1/auth.proto package mciasv1 diff --git a/gen/mcias/v1/common.pb.go b/gen/mcias/v1/common.pb.go index 4880699..6fadd94 100644 --- a/gen/mcias/v1/common.pb.go +++ b/gen/mcias/v1/common.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.4 +// protoc v3.20.3 // source: mcias/v1/common.proto package mciasv1 diff --git a/gen/mcias/v1/policy.pb.go b/gen/mcias/v1/policy.pb.go new file mode 100644 index 0000000..56e4ced --- /dev/null +++ b/gen/mcias/v1/policy.pb.go @@ -0,0 +1,779 @@ +// PolicyService: CRUD management of policy rules. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.20.3 +// source: mcias/v1/policy.proto + +package mciasv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// PolicyRule is the wire representation of a policy rule record. +type PolicyRule struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Priority int32 `protobuf:"varint,3,opt,name=priority,proto3" json:"priority,omitempty"` + Enabled bool `protobuf:"varint,4,opt,name=enabled,proto3" json:"enabled,omitempty"` + RuleJson string `protobuf:"bytes,5,opt,name=rule_json,json=ruleJson,proto3" json:"rule_json,omitempty"` // JSON-encoded RuleBody + CreatedAt string `protobuf:"bytes,6,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // RFC3339 + UpdatedAt string `protobuf:"bytes,7,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` // RFC3339 + NotBefore string `protobuf:"bytes,8,opt,name=not_before,json=notBefore,proto3" json:"not_before,omitempty"` // RFC3339; empty if unset + ExpiresAt string `protobuf:"bytes,9,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // RFC3339; empty if unset + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyRule) Reset() { + *x = PolicyRule{} + mi := &file_mcias_v1_policy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyRule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyRule) ProtoMessage() {} + +func (x *PolicyRule) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyRule.ProtoReflect.Descriptor instead. +func (*PolicyRule) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{0} +} + +func (x *PolicyRule) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *PolicyRule) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PolicyRule) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *PolicyRule) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +func (x *PolicyRule) GetRuleJson() string { + if x != nil { + return x.RuleJson + } + return "" +} + +func (x *PolicyRule) GetCreatedAt() string { + if x != nil { + return x.CreatedAt + } + return "" +} + +func (x *PolicyRule) GetUpdatedAt() string { + if x != nil { + return x.UpdatedAt + } + return "" +} + +func (x *PolicyRule) GetNotBefore() string { + if x != nil { + return x.NotBefore + } + return "" +} + +func (x *PolicyRule) GetExpiresAt() string { + if x != nil { + return x.ExpiresAt + } + return "" +} + +type ListPolicyRulesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPolicyRulesRequest) Reset() { + *x = ListPolicyRulesRequest{} + mi := &file_mcias_v1_policy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPolicyRulesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPolicyRulesRequest) ProtoMessage() {} + +func (x *ListPolicyRulesRequest) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPolicyRulesRequest.ProtoReflect.Descriptor instead. +func (*ListPolicyRulesRequest) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{1} +} + +type ListPolicyRulesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rules []*PolicyRule `protobuf:"bytes,1,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPolicyRulesResponse) Reset() { + *x = ListPolicyRulesResponse{} + mi := &file_mcias_v1_policy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPolicyRulesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPolicyRulesResponse) ProtoMessage() {} + +func (x *ListPolicyRulesResponse) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPolicyRulesResponse.ProtoReflect.Descriptor instead. +func (*ListPolicyRulesResponse) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{2} +} + +func (x *ListPolicyRulesResponse) GetRules() []*PolicyRule { + if x != nil { + return x.Rules + } + return nil +} + +type CreatePolicyRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` // required + RuleJson string `protobuf:"bytes,2,opt,name=rule_json,json=ruleJson,proto3" json:"rule_json,omitempty"` // required; JSON-encoded RuleBody + Priority int32 `protobuf:"varint,3,opt,name=priority,proto3" json:"priority,omitempty"` // default 100 when zero + NotBefore string `protobuf:"bytes,4,opt,name=not_before,json=notBefore,proto3" json:"not_before,omitempty"` // RFC3339; optional + ExpiresAt string `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // RFC3339; optional + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePolicyRuleRequest) Reset() { + *x = CreatePolicyRuleRequest{} + mi := &file_mcias_v1_policy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePolicyRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePolicyRuleRequest) ProtoMessage() {} + +func (x *CreatePolicyRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePolicyRuleRequest.ProtoReflect.Descriptor instead. +func (*CreatePolicyRuleRequest) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{3} +} + +func (x *CreatePolicyRuleRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *CreatePolicyRuleRequest) GetRuleJson() string { + if x != nil { + return x.RuleJson + } + return "" +} + +func (x *CreatePolicyRuleRequest) GetPriority() int32 { + if x != nil { + return x.Priority + } + return 0 +} + +func (x *CreatePolicyRuleRequest) GetNotBefore() string { + if x != nil { + return x.NotBefore + } + return "" +} + +func (x *CreatePolicyRuleRequest) GetExpiresAt() string { + if x != nil { + return x.ExpiresAt + } + return "" +} + +type CreatePolicyRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *PolicyRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePolicyRuleResponse) Reset() { + *x = CreatePolicyRuleResponse{} + mi := &file_mcias_v1_policy_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePolicyRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePolicyRuleResponse) ProtoMessage() {} + +func (x *CreatePolicyRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePolicyRuleResponse.ProtoReflect.Descriptor instead. +func (*CreatePolicyRuleResponse) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{4} +} + +func (x *CreatePolicyRuleResponse) GetRule() *PolicyRule { + if x != nil { + return x.Rule + } + return nil +} + +type GetPolicyRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPolicyRuleRequest) Reset() { + *x = GetPolicyRuleRequest{} + mi := &file_mcias_v1_policy_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPolicyRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyRuleRequest) ProtoMessage() {} + +func (x *GetPolicyRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPolicyRuleRequest.ProtoReflect.Descriptor instead. +func (*GetPolicyRuleRequest) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{5} +} + +func (x *GetPolicyRuleRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type GetPolicyRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *PolicyRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPolicyRuleResponse) Reset() { + *x = GetPolicyRuleResponse{} + mi := &file_mcias_v1_policy_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPolicyRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyRuleResponse) ProtoMessage() {} + +func (x *GetPolicyRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPolicyRuleResponse.ProtoReflect.Descriptor instead. +func (*GetPolicyRuleResponse) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{6} +} + +func (x *GetPolicyRuleResponse) GetRule() *PolicyRule { + if x != nil { + return x.Rule + } + return nil +} + +// UpdatePolicyRuleRequest carries partial updates. +// Fields left at their zero value are not changed on the server, except: +// - clear_not_before=true removes the not_before constraint +// - clear_expires_at=true removes the expires_at constraint +// +// has_priority / has_enabled use proto3 optional (field presence) so the +// server can distinguish "not supplied" from "set to zero/false". +type UpdatePolicyRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Priority *int32 `protobuf:"varint,2,opt,name=priority,proto3,oneof" json:"priority,omitempty"` // omit to leave unchanged + Enabled *bool `protobuf:"varint,3,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` // omit to leave unchanged + NotBefore string `protobuf:"bytes,4,opt,name=not_before,json=notBefore,proto3" json:"not_before,omitempty"` // RFC3339; ignored when clear_not_before=true + ExpiresAt string `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // RFC3339; ignored when clear_expires_at=true + ClearNotBefore bool `protobuf:"varint,6,opt,name=clear_not_before,json=clearNotBefore,proto3" json:"clear_not_before,omitempty"` + ClearExpiresAt bool `protobuf:"varint,7,opt,name=clear_expires_at,json=clearExpiresAt,proto3" json:"clear_expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdatePolicyRuleRequest) Reset() { + *x = UpdatePolicyRuleRequest{} + mi := &file_mcias_v1_policy_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdatePolicyRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePolicyRuleRequest) ProtoMessage() {} + +func (x *UpdatePolicyRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePolicyRuleRequest.ProtoReflect.Descriptor instead. +func (*UpdatePolicyRuleRequest) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdatePolicyRuleRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *UpdatePolicyRuleRequest) GetPriority() int32 { + if x != nil && x.Priority != nil { + return *x.Priority + } + return 0 +} + +func (x *UpdatePolicyRuleRequest) GetEnabled() bool { + if x != nil && x.Enabled != nil { + return *x.Enabled + } + return false +} + +func (x *UpdatePolicyRuleRequest) GetNotBefore() string { + if x != nil { + return x.NotBefore + } + return "" +} + +func (x *UpdatePolicyRuleRequest) GetExpiresAt() string { + if x != nil { + return x.ExpiresAt + } + return "" +} + +func (x *UpdatePolicyRuleRequest) GetClearNotBefore() bool { + if x != nil { + return x.ClearNotBefore + } + return false +} + +func (x *UpdatePolicyRuleRequest) GetClearExpiresAt() bool { + if x != nil { + return x.ClearExpiresAt + } + return false +} + +type UpdatePolicyRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Rule *PolicyRule `protobuf:"bytes,1,opt,name=rule,proto3" json:"rule,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdatePolicyRuleResponse) Reset() { + *x = UpdatePolicyRuleResponse{} + mi := &file_mcias_v1_policy_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdatePolicyRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePolicyRuleResponse) ProtoMessage() {} + +func (x *UpdatePolicyRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePolicyRuleResponse.ProtoReflect.Descriptor instead. +func (*UpdatePolicyRuleResponse) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdatePolicyRuleResponse) GetRule() *PolicyRule { + if x != nil { + return x.Rule + } + return nil +} + +type DeletePolicyRuleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePolicyRuleRequest) Reset() { + *x = DeletePolicyRuleRequest{} + mi := &file_mcias_v1_policy_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePolicyRuleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePolicyRuleRequest) ProtoMessage() {} + +func (x *DeletePolicyRuleRequest) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletePolicyRuleRequest.ProtoReflect.Descriptor instead. +func (*DeletePolicyRuleRequest) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{9} +} + +func (x *DeletePolicyRuleRequest) GetId() int64 { + if x != nil { + return x.Id + } + return 0 +} + +type DeletePolicyRuleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePolicyRuleResponse) Reset() { + *x = DeletePolicyRuleResponse{} + mi := &file_mcias_v1_policy_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePolicyRuleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePolicyRuleResponse) ProtoMessage() {} + +func (x *DeletePolicyRuleResponse) ProtoReflect() protoreflect.Message { + mi := &file_mcias_v1_policy_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletePolicyRuleResponse.ProtoReflect.Descriptor instead. +func (*DeletePolicyRuleResponse) Descriptor() ([]byte, []int) { + return file_mcias_v1_policy_proto_rawDescGZIP(), []int{10} +} + +var File_mcias_v1_policy_proto protoreflect.FileDescriptor + +const file_mcias_v1_policy_proto_rawDesc = "" + + "\n" + + "\x15mcias/v1/policy.proto\x12\bmcias.v1\"\x8d\x02\n" + + "\n" + + "PolicyRule\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + + "\bpriority\x18\x03 \x01(\x05R\bpriority\x12\x18\n" + + "\aenabled\x18\x04 \x01(\bR\aenabled\x12\x1b\n" + + "\trule_json\x18\x05 \x01(\tR\bruleJson\x12\x1d\n" + + "\n" + + "created_at\x18\x06 \x01(\tR\tcreatedAt\x12\x1d\n" + + "\n" + + "updated_at\x18\a \x01(\tR\tupdatedAt\x12\x1d\n" + + "\n" + + "not_before\x18\b \x01(\tR\tnotBefore\x12\x1d\n" + + "\n" + + "expires_at\x18\t \x01(\tR\texpiresAt\"\x18\n" + + "\x16ListPolicyRulesRequest\"E\n" + + "\x17ListPolicyRulesResponse\x12*\n" + + "\x05rules\x18\x01 \x03(\v2\x14.mcias.v1.PolicyRuleR\x05rules\"\xb2\x01\n" + + "\x17CreatePolicyRuleRequest\x12 \n" + + "\vdescription\x18\x01 \x01(\tR\vdescription\x12\x1b\n" + + "\trule_json\x18\x02 \x01(\tR\bruleJson\x12\x1a\n" + + "\bpriority\x18\x03 \x01(\x05R\bpriority\x12\x1d\n" + + "\n" + + "not_before\x18\x04 \x01(\tR\tnotBefore\x12\x1d\n" + + "\n" + + "expires_at\x18\x05 \x01(\tR\texpiresAt\"D\n" + + "\x18CreatePolicyRuleResponse\x12(\n" + + "\x04rule\x18\x01 \x01(\v2\x14.mcias.v1.PolicyRuleR\x04rule\"&\n" + + "\x14GetPolicyRuleRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"A\n" + + "\x15GetPolicyRuleResponse\x12(\n" + + "\x04rule\x18\x01 \x01(\v2\x14.mcias.v1.PolicyRuleR\x04rule\"\x94\x02\n" + + "\x17UpdatePolicyRuleRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\x12\x1f\n" + + "\bpriority\x18\x02 \x01(\x05H\x00R\bpriority\x88\x01\x01\x12\x1d\n" + + "\aenabled\x18\x03 \x01(\bH\x01R\aenabled\x88\x01\x01\x12\x1d\n" + + "\n" + + "not_before\x18\x04 \x01(\tR\tnotBefore\x12\x1d\n" + + "\n" + + "expires_at\x18\x05 \x01(\tR\texpiresAt\x12(\n" + + "\x10clear_not_before\x18\x06 \x01(\bR\x0eclearNotBefore\x12(\n" + + "\x10clear_expires_at\x18\a \x01(\bR\x0eclearExpiresAtB\v\n" + + "\t_priorityB\n" + + "\n" + + "\b_enabled\"D\n" + + "\x18UpdatePolicyRuleResponse\x12(\n" + + "\x04rule\x18\x01 \x01(\v2\x14.mcias.v1.PolicyRuleR\x04rule\")\n" + + "\x17DeletePolicyRuleRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x03R\x02id\"\x1a\n" + + "\x18DeletePolicyRuleResponse2\xca\x03\n" + + "\rPolicyService\x12V\n" + + "\x0fListPolicyRules\x12 .mcias.v1.ListPolicyRulesRequest\x1a!.mcias.v1.ListPolicyRulesResponse\x12Y\n" + + "\x10CreatePolicyRule\x12!.mcias.v1.CreatePolicyRuleRequest\x1a\".mcias.v1.CreatePolicyRuleResponse\x12P\n" + + "\rGetPolicyRule\x12\x1e.mcias.v1.GetPolicyRuleRequest\x1a\x1f.mcias.v1.GetPolicyRuleResponse\x12Y\n" + + "\x10UpdatePolicyRule\x12!.mcias.v1.UpdatePolicyRuleRequest\x1a\".mcias.v1.UpdatePolicyRuleResponse\x12Y\n" + + "\x10DeletePolicyRule\x12!.mcias.v1.DeletePolicyRuleRequest\x1a\".mcias.v1.DeletePolicyRuleResponseB2Z0git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1b\x06proto3" + +var ( + file_mcias_v1_policy_proto_rawDescOnce sync.Once + file_mcias_v1_policy_proto_rawDescData []byte +) + +func file_mcias_v1_policy_proto_rawDescGZIP() []byte { + file_mcias_v1_policy_proto_rawDescOnce.Do(func() { + file_mcias_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mcias_v1_policy_proto_rawDesc), len(file_mcias_v1_policy_proto_rawDesc))) + }) + return file_mcias_v1_policy_proto_rawDescData +} + +var file_mcias_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_mcias_v1_policy_proto_goTypes = []any{ + (*PolicyRule)(nil), // 0: mcias.v1.PolicyRule + (*ListPolicyRulesRequest)(nil), // 1: mcias.v1.ListPolicyRulesRequest + (*ListPolicyRulesResponse)(nil), // 2: mcias.v1.ListPolicyRulesResponse + (*CreatePolicyRuleRequest)(nil), // 3: mcias.v1.CreatePolicyRuleRequest + (*CreatePolicyRuleResponse)(nil), // 4: mcias.v1.CreatePolicyRuleResponse + (*GetPolicyRuleRequest)(nil), // 5: mcias.v1.GetPolicyRuleRequest + (*GetPolicyRuleResponse)(nil), // 6: mcias.v1.GetPolicyRuleResponse + (*UpdatePolicyRuleRequest)(nil), // 7: mcias.v1.UpdatePolicyRuleRequest + (*UpdatePolicyRuleResponse)(nil), // 8: mcias.v1.UpdatePolicyRuleResponse + (*DeletePolicyRuleRequest)(nil), // 9: mcias.v1.DeletePolicyRuleRequest + (*DeletePolicyRuleResponse)(nil), // 10: mcias.v1.DeletePolicyRuleResponse +} +var file_mcias_v1_policy_proto_depIdxs = []int32{ + 0, // 0: mcias.v1.ListPolicyRulesResponse.rules:type_name -> mcias.v1.PolicyRule + 0, // 1: mcias.v1.CreatePolicyRuleResponse.rule:type_name -> mcias.v1.PolicyRule + 0, // 2: mcias.v1.GetPolicyRuleResponse.rule:type_name -> mcias.v1.PolicyRule + 0, // 3: mcias.v1.UpdatePolicyRuleResponse.rule:type_name -> mcias.v1.PolicyRule + 1, // 4: mcias.v1.PolicyService.ListPolicyRules:input_type -> mcias.v1.ListPolicyRulesRequest + 3, // 5: mcias.v1.PolicyService.CreatePolicyRule:input_type -> mcias.v1.CreatePolicyRuleRequest + 5, // 6: mcias.v1.PolicyService.GetPolicyRule:input_type -> mcias.v1.GetPolicyRuleRequest + 7, // 7: mcias.v1.PolicyService.UpdatePolicyRule:input_type -> mcias.v1.UpdatePolicyRuleRequest + 9, // 8: mcias.v1.PolicyService.DeletePolicyRule:input_type -> mcias.v1.DeletePolicyRuleRequest + 2, // 9: mcias.v1.PolicyService.ListPolicyRules:output_type -> mcias.v1.ListPolicyRulesResponse + 4, // 10: mcias.v1.PolicyService.CreatePolicyRule:output_type -> mcias.v1.CreatePolicyRuleResponse + 6, // 11: mcias.v1.PolicyService.GetPolicyRule:output_type -> mcias.v1.GetPolicyRuleResponse + 8, // 12: mcias.v1.PolicyService.UpdatePolicyRule:output_type -> mcias.v1.UpdatePolicyRuleResponse + 10, // 13: mcias.v1.PolicyService.DeletePolicyRule:output_type -> mcias.v1.DeletePolicyRuleResponse + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_mcias_v1_policy_proto_init() } +func file_mcias_v1_policy_proto_init() { + if File_mcias_v1_policy_proto != nil { + return + } + file_mcias_v1_policy_proto_msgTypes[7].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_mcias_v1_policy_proto_rawDesc), len(file_mcias_v1_policy_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_mcias_v1_policy_proto_goTypes, + DependencyIndexes: file_mcias_v1_policy_proto_depIdxs, + MessageInfos: file_mcias_v1_policy_proto_msgTypes, + }.Build() + File_mcias_v1_policy_proto = out.File + file_mcias_v1_policy_proto_goTypes = nil + file_mcias_v1_policy_proto_depIdxs = nil +} diff --git a/gen/mcias/v1/policy_grpc.pb.go b/gen/mcias/v1/policy_grpc.pb.go new file mode 100644 index 0000000..22f98ee --- /dev/null +++ b/gen/mcias/v1/policy_grpc.pb.go @@ -0,0 +1,299 @@ +// PolicyService: CRUD management of policy rules. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc v3.20.3 +// source: mcias/v1/policy.proto + +package mciasv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + PolicyService_ListPolicyRules_FullMethodName = "/mcias.v1.PolicyService/ListPolicyRules" + PolicyService_CreatePolicyRule_FullMethodName = "/mcias.v1.PolicyService/CreatePolicyRule" + PolicyService_GetPolicyRule_FullMethodName = "/mcias.v1.PolicyService/GetPolicyRule" + PolicyService_UpdatePolicyRule_FullMethodName = "/mcias.v1.PolicyService/UpdatePolicyRule" + PolicyService_DeletePolicyRule_FullMethodName = "/mcias.v1.PolicyService/DeletePolicyRule" +) + +// PolicyServiceClient is the client API for PolicyService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// PolicyService manages policy rules (admin only). +type PolicyServiceClient interface { + // ListPolicyRules returns all policy rules. + // Requires: admin JWT. + ListPolicyRules(ctx context.Context, in *ListPolicyRulesRequest, opts ...grpc.CallOption) (*ListPolicyRulesResponse, error) + // CreatePolicyRule creates a new policy rule. + // Requires: admin JWT. + CreatePolicyRule(ctx context.Context, in *CreatePolicyRuleRequest, opts ...grpc.CallOption) (*CreatePolicyRuleResponse, error) + // GetPolicyRule returns a single policy rule by ID. + // Requires: admin JWT. + GetPolicyRule(ctx context.Context, in *GetPolicyRuleRequest, opts ...grpc.CallOption) (*GetPolicyRuleResponse, error) + // UpdatePolicyRule applies a partial update to a policy rule. + // Requires: admin JWT. + UpdatePolicyRule(ctx context.Context, in *UpdatePolicyRuleRequest, opts ...grpc.CallOption) (*UpdatePolicyRuleResponse, error) + // DeletePolicyRule permanently removes a policy rule. + // Requires: admin JWT. + DeletePolicyRule(ctx context.Context, in *DeletePolicyRuleRequest, opts ...grpc.CallOption) (*DeletePolicyRuleResponse, error) +} + +type policyServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPolicyServiceClient(cc grpc.ClientConnInterface) PolicyServiceClient { + return &policyServiceClient{cc} +} + +func (c *policyServiceClient) ListPolicyRules(ctx context.Context, in *ListPolicyRulesRequest, opts ...grpc.CallOption) (*ListPolicyRulesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListPolicyRulesResponse) + err := c.cc.Invoke(ctx, PolicyService_ListPolicyRules_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) CreatePolicyRule(ctx context.Context, in *CreatePolicyRuleRequest, opts ...grpc.CallOption) (*CreatePolicyRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreatePolicyRuleResponse) + err := c.cc.Invoke(ctx, PolicyService_CreatePolicyRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) GetPolicyRule(ctx context.Context, in *GetPolicyRuleRequest, opts ...grpc.CallOption) (*GetPolicyRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetPolicyRuleResponse) + err := c.cc.Invoke(ctx, PolicyService_GetPolicyRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) UpdatePolicyRule(ctx context.Context, in *UpdatePolicyRuleRequest, opts ...grpc.CallOption) (*UpdatePolicyRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdatePolicyRuleResponse) + err := c.cc.Invoke(ctx, PolicyService_UpdatePolicyRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) DeletePolicyRule(ctx context.Context, in *DeletePolicyRuleRequest, opts ...grpc.CallOption) (*DeletePolicyRuleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeletePolicyRuleResponse) + err := c.cc.Invoke(ctx, PolicyService_DeletePolicyRule_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PolicyServiceServer is the server API for PolicyService service. +// All implementations must embed UnimplementedPolicyServiceServer +// for forward compatibility. +// +// PolicyService manages policy rules (admin only). +type PolicyServiceServer interface { + // ListPolicyRules returns all policy rules. + // Requires: admin JWT. + ListPolicyRules(context.Context, *ListPolicyRulesRequest) (*ListPolicyRulesResponse, error) + // CreatePolicyRule creates a new policy rule. + // Requires: admin JWT. + CreatePolicyRule(context.Context, *CreatePolicyRuleRequest) (*CreatePolicyRuleResponse, error) + // GetPolicyRule returns a single policy rule by ID. + // Requires: admin JWT. + GetPolicyRule(context.Context, *GetPolicyRuleRequest) (*GetPolicyRuleResponse, error) + // UpdatePolicyRule applies a partial update to a policy rule. + // Requires: admin JWT. + UpdatePolicyRule(context.Context, *UpdatePolicyRuleRequest) (*UpdatePolicyRuleResponse, error) + // DeletePolicyRule permanently removes a policy rule. + // Requires: admin JWT. + DeletePolicyRule(context.Context, *DeletePolicyRuleRequest) (*DeletePolicyRuleResponse, error) + mustEmbedUnimplementedPolicyServiceServer() +} + +// UnimplementedPolicyServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPolicyServiceServer struct{} + +func (UnimplementedPolicyServiceServer) ListPolicyRules(context.Context, *ListPolicyRulesRequest) (*ListPolicyRulesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListPolicyRules not implemented") +} +func (UnimplementedPolicyServiceServer) CreatePolicyRule(context.Context, *CreatePolicyRuleRequest) (*CreatePolicyRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreatePolicyRule not implemented") +} +func (UnimplementedPolicyServiceServer) GetPolicyRule(context.Context, *GetPolicyRuleRequest) (*GetPolicyRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPolicyRule not implemented") +} +func (UnimplementedPolicyServiceServer) UpdatePolicyRule(context.Context, *UpdatePolicyRuleRequest) (*UpdatePolicyRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdatePolicyRule not implemented") +} +func (UnimplementedPolicyServiceServer) DeletePolicyRule(context.Context, *DeletePolicyRuleRequest) (*DeletePolicyRuleResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeletePolicyRule not implemented") +} +func (UnimplementedPolicyServiceServer) mustEmbedUnimplementedPolicyServiceServer() {} +func (UnimplementedPolicyServiceServer) testEmbeddedByValue() {} + +// UnsafePolicyServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PolicyServiceServer will +// result in compilation errors. +type UnsafePolicyServiceServer interface { + mustEmbedUnimplementedPolicyServiceServer() +} + +func RegisterPolicyServiceServer(s grpc.ServiceRegistrar, srv PolicyServiceServer) { + // If the following call panics, it indicates UnimplementedPolicyServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PolicyService_ServiceDesc, srv) +} + +func _PolicyService_ListPolicyRules_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListPolicyRulesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).ListPolicyRules(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_ListPolicyRules_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).ListPolicyRules(ctx, req.(*ListPolicyRulesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_CreatePolicyRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreatePolicyRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).CreatePolicyRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_CreatePolicyRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).CreatePolicyRule(ctx, req.(*CreatePolicyRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_GetPolicyRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPolicyRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).GetPolicyRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_GetPolicyRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).GetPolicyRule(ctx, req.(*GetPolicyRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_UpdatePolicyRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdatePolicyRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).UpdatePolicyRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_UpdatePolicyRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).UpdatePolicyRule(ctx, req.(*UpdatePolicyRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_DeletePolicyRule_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeletePolicyRuleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).DeletePolicyRule(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_DeletePolicyRule_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).DeletePolicyRule(ctx, req.(*DeletePolicyRuleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PolicyService_ServiceDesc is the grpc.ServiceDesc for PolicyService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PolicyService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "mcias.v1.PolicyService", + HandlerType: (*PolicyServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListPolicyRules", + Handler: _PolicyService_ListPolicyRules_Handler, + }, + { + MethodName: "CreatePolicyRule", + Handler: _PolicyService_CreatePolicyRule_Handler, + }, + { + MethodName: "GetPolicyRule", + Handler: _PolicyService_GetPolicyRule_Handler, + }, + { + MethodName: "UpdatePolicyRule", + Handler: _PolicyService_UpdatePolicyRule_Handler, + }, + { + MethodName: "DeletePolicyRule", + Handler: _PolicyService_DeletePolicyRule_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "mcias/v1/policy.proto", +} diff --git a/gen/mcias/v1/token.pb.go b/gen/mcias/v1/token.pb.go index 4fee42e..e9385d3 100644 --- a/gen/mcias/v1/token.pb.go +++ b/gen/mcias/v1/token.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v6.33.4 +// protoc v3.20.3 // source: mcias/v1/token.proto package mciasv1 diff --git a/gen/mcias/v1/token_grpc.pb.go b/gen/mcias/v1/token_grpc.pb.go index a292b48..b197c6f 100644 --- a/gen/mcias/v1/token_grpc.pb.go +++ b/gen/mcias/v1/token_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.6.1 -// - protoc v6.33.4 +// - protoc v3.20.3 // source: mcias/v1/token.proto package mciasv1 diff --git a/internal/grpcserver/grpcserver.go b/internal/grpcserver/grpcserver.go index 9692d35..449a1fc 100644 --- a/internal/grpcserver/grpcserver.go +++ b/internal/grpcserver/grpcserver.go @@ -120,6 +120,7 @@ func (s *Server) buildServer(extra ...grpc.ServerOption) *grpc.Server { mciasv1.RegisterTokenServiceServer(srv, &tokenServiceServer{s: s}) mciasv1.RegisterAccountServiceServer(srv, &accountServiceServer{s: s}) mciasv1.RegisterCredentialServiceServer(srv, &credentialServiceServer{s: s}) + mciasv1.RegisterPolicyServiceServer(srv, &policyServiceServer{s: s}) return srv } diff --git a/internal/grpcserver/policyservice.go b/internal/grpcserver/policyservice.go new file mode 100644 index 0000000..18d311f --- /dev/null +++ b/internal/grpcserver/policyservice.go @@ -0,0 +1,278 @@ +// policyServiceServer implements mciasv1.PolicyServiceServer. +// All handlers are admin-only and delegate to the same db package used by +// the REST policy handlers in internal/server/handlers_policy.go. +package grpcserver + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + mciasv1 "git.wntrmute.dev/kyle/mcias/gen/mcias/v1" + "git.wntrmute.dev/kyle/mcias/internal/db" + "git.wntrmute.dev/kyle/mcias/internal/model" + "git.wntrmute.dev/kyle/mcias/internal/policy" +) + +type policyServiceServer struct { + mciasv1.UnimplementedPolicyServiceServer + s *Server +} + +// policyRuleToProto converts a model.PolicyRuleRecord to the wire representation. +func policyRuleToProto(rec *model.PolicyRuleRecord) *mciasv1.PolicyRule { + r := &mciasv1.PolicyRule{ + Id: rec.ID, + Description: rec.Description, + Priority: int32(rec.Priority), //nolint:gosec // priority is a small positive integer + Enabled: rec.Enabled, + RuleJson: rec.RuleJSON, + CreatedAt: rec.CreatedAt.UTC().Format(time.RFC3339), + UpdatedAt: rec.UpdatedAt.UTC().Format(time.RFC3339), + } + if rec.NotBefore != nil { + r.NotBefore = rec.NotBefore.UTC().Format(time.RFC3339) + } + if rec.ExpiresAt != nil { + r.ExpiresAt = rec.ExpiresAt.UTC().Format(time.RFC3339) + } + return r +} + +// validateRuleJSON ensures the JSON string is valid and contains a recognised +// effect. It mirrors the validation in the REST handleCreatePolicyRule handler. +func validateRuleJSON(ruleJSON string) error { + var body policy.RuleBody + if err := json.Unmarshal([]byte(ruleJSON), &body); err != nil { + return fmt.Errorf("rule_json is not valid JSON: %w", err) + } + if body.Effect != policy.Allow && body.Effect != policy.Deny { + return fmt.Errorf("rule.effect must be %q or %q", policy.Allow, policy.Deny) + } + return nil +} + +// ListPolicyRules returns all policy rules. Admin only. +func (p *policyServiceServer) ListPolicyRules(ctx context.Context, _ *mciasv1.ListPolicyRulesRequest) (*mciasv1.ListPolicyRulesResponse, error) { + if err := p.s.requireAdmin(ctx); err != nil { + return nil, err + } + + rules, err := p.s.db.ListPolicyRules(false) + if err != nil { + p.s.logger.Error("list policy rules", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + resp := &mciasv1.ListPolicyRulesResponse{ + Rules: make([]*mciasv1.PolicyRule, 0, len(rules)), + } + for _, rec := range rules { + resp.Rules = append(resp.Rules, policyRuleToProto(rec)) + } + return resp, nil +} + +// CreatePolicyRule creates a new policy rule. Admin only. +func (p *policyServiceServer) CreatePolicyRule(ctx context.Context, req *mciasv1.CreatePolicyRuleRequest) (*mciasv1.CreatePolicyRuleResponse, error) { + if err := p.s.requireAdmin(ctx); err != nil { + return nil, err + } + + if req.Description == "" { + return nil, status.Error(codes.InvalidArgument, "description is required") + } + if req.RuleJson == "" { + return nil, status.Error(codes.InvalidArgument, "rule_json is required") + } + if err := validateRuleJSON(req.RuleJson); err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + priority := int(req.Priority) + if priority == 0 { + priority = 100 // default, matching REST handler + } + + var notBefore, expiresAt *time.Time + if req.NotBefore != "" { + t, err := time.Parse(time.RFC3339, req.NotBefore) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "not_before must be RFC3339") + } + notBefore = &t + } + if req.ExpiresAt != "" { + t, err := time.Parse(time.RFC3339, req.ExpiresAt) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "expires_at must be RFC3339") + } + expiresAt = &t + } + if notBefore != nil && expiresAt != nil && !expiresAt.After(*notBefore) { + return nil, status.Error(codes.InvalidArgument, "expires_at must be after not_before") + } + + claims := claimsFromContext(ctx) + var createdBy *int64 + if claims != nil { + if actor, err := p.s.db.GetAccountByUUID(claims.Subject); err == nil { + createdBy = &actor.ID + } + } + + rec, err := p.s.db.CreatePolicyRule(req.Description, priority, req.RuleJson, createdBy, notBefore, expiresAt) + if err != nil { + p.s.logger.Error("create policy rule", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + p.s.db.WriteAuditEvent(model.EventPolicyRuleCreated, createdBy, nil, peerIP(ctx), //nolint:errcheck + fmt.Sprintf(`{"rule_id":%d,"description":%q}`, rec.ID, rec.Description)) + + return &mciasv1.CreatePolicyRuleResponse{Rule: policyRuleToProto(rec)}, nil +} + +// GetPolicyRule returns a single policy rule by ID. Admin only. +func (p *policyServiceServer) GetPolicyRule(ctx context.Context, req *mciasv1.GetPolicyRuleRequest) (*mciasv1.GetPolicyRuleResponse, error) { + if err := p.s.requireAdmin(ctx); err != nil { + return nil, err + } + if req.Id == 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + rec, err := p.s.db.GetPolicyRule(req.Id) + if err != nil { + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "policy rule not found") + } + p.s.logger.Error("get policy rule", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + return &mciasv1.GetPolicyRuleResponse{Rule: policyRuleToProto(rec)}, nil +} + +// UpdatePolicyRule applies a partial update to a policy rule. Admin only. +func (p *policyServiceServer) UpdatePolicyRule(ctx context.Context, req *mciasv1.UpdatePolicyRuleRequest) (*mciasv1.UpdatePolicyRuleResponse, error) { + if err := p.s.requireAdmin(ctx); err != nil { + return nil, err + } + if req.Id == 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + // Verify the rule exists before applying updates. + if _, err := p.s.db.GetPolicyRule(req.Id); err != nil { + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "policy rule not found") + } + p.s.logger.Error("get policy rule for update", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + // Build optional update fields — nil means "do not change". + var priority *int + if req.Priority != nil { + v := int(req.GetPriority()) + priority = &v + } + + // Double-pointer semantics for time fields: nil outer = no change; + // non-nil outer with nil inner = set to NULL; non-nil both = set value. + var notBefore, expiresAt **time.Time + if req.ClearNotBefore { + var nilTime *time.Time + notBefore = &nilTime + } else if req.NotBefore != "" { + t, err := time.Parse(time.RFC3339, req.NotBefore) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "not_before must be RFC3339") + } + tp := &t + notBefore = &tp + } + if req.ClearExpiresAt { + var nilTime *time.Time + expiresAt = &nilTime + } else if req.ExpiresAt != "" { + t, err := time.Parse(time.RFC3339, req.ExpiresAt) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "expires_at must be RFC3339") + } + tp := &t + expiresAt = &tp + } + + if err := p.s.db.UpdatePolicyRule(req.Id, nil, priority, nil, notBefore, expiresAt); err != nil { + p.s.logger.Error("update policy rule", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + if req.Enabled != nil { + if err := p.s.db.SetPolicyRuleEnabled(req.Id, req.GetEnabled()); err != nil { + p.s.logger.Error("set policy rule enabled", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + } + + claims := claimsFromContext(ctx) + var actorID *int64 + if claims != nil { + if actor, err := p.s.db.GetAccountByUUID(claims.Subject); err == nil { + actorID = &actor.ID + } + } + p.s.db.WriteAuditEvent(model.EventPolicyRuleUpdated, actorID, nil, peerIP(ctx), //nolint:errcheck + fmt.Sprintf(`{"rule_id":%d}`, req.Id)) + + updated, err := p.s.db.GetPolicyRule(req.Id) + if err != nil { + p.s.logger.Error("get updated policy rule", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + return &mciasv1.UpdatePolicyRuleResponse{Rule: policyRuleToProto(updated)}, nil +} + +// DeletePolicyRule permanently removes a policy rule. Admin only. +func (p *policyServiceServer) DeletePolicyRule(ctx context.Context, req *mciasv1.DeletePolicyRuleRequest) (*mciasv1.DeletePolicyRuleResponse, error) { + if err := p.s.requireAdmin(ctx); err != nil { + return nil, err + } + if req.Id == 0 { + return nil, status.Error(codes.InvalidArgument, "id is required") + } + + rec, err := p.s.db.GetPolicyRule(req.Id) + if err != nil { + if errors.Is(err, db.ErrNotFound) { + return nil, status.Error(codes.NotFound, "policy rule not found") + } + p.s.logger.Error("get policy rule for delete", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + if err := p.s.db.DeletePolicyRule(req.Id); err != nil { + p.s.logger.Error("delete policy rule", "error", err) + return nil, status.Error(codes.Internal, "internal error") + } + + claims := claimsFromContext(ctx) + var actorID *int64 + if claims != nil { + if actor, err := p.s.db.GetAccountByUUID(claims.Subject); err == nil { + actorID = &actor.ID + } + } + p.s.db.WriteAuditEvent(model.EventPolicyRuleDeleted, actorID, nil, peerIP(ctx), //nolint:errcheck + fmt.Sprintf(`{"rule_id":%d,"description":%q}`, rec.ID, rec.Description)) + + return &mciasv1.DeletePolicyRuleResponse{}, nil +} diff --git a/man/man1/mciasctl.1 b/man/man1/mciasctl.1 index 859c6e7..c2855ce 100644 --- a/man/man1/mciasctl.1 +++ b/man/man1/mciasctl.1 @@ -34,7 +34,7 @@ environment variable. .It Fl server Ar url Base URL of the mciassrv instance. Default: -.Qq https://localhost:8443 . +.Qq https://mcias.metacircular.net:8443 . Can also be set with the .Ev MCIAS_SERVER environment variable. diff --git a/man/man1/mciasgrpcctl.1 b/man/man1/mciasgrpcctl.1 index bb46618..64bd4dd 100644 --- a/man/man1/mciasgrpcctl.1 +++ b/man/man1/mciasgrpcctl.1 @@ -1,4 +1,4 @@ -.Dd March 11, 2026 +.Dd March 12, 2026 .Dt MCIASGRPCCTL 1 .Os .Sh NAME @@ -37,7 +37,7 @@ gRPC server address in .Ar host:port format. Default: -.Qq localhost:9443 . +.Qq mcias.metacircular.net:9443 . .It Fl token Ar jwt Bearer token for authentication. Can also be set with the @@ -58,6 +58,18 @@ and exits 0 if the server is healthy. .It Nm Ic pubkey Returns the server's Ed25519 public key as a JWK. .El +.Ss auth +.Bl -tag -width Ds +.It Nm Ic auth Ic login Fl username Ar name Op Fl totp Ar code +Authenticates with the server and prints the bearer token to stdout. +The password is always prompted interactively. +Suitable for use in scripts: +.Bd -literal -offset indent +export MCIAS_TOKEN=$(mciasgrpcctl auth login -username alice) +.Ed +.It Nm Ic auth Ic logout +Revokes the current bearer token. +.El .Ss account .Bl -tag -width Ds .It Nm Ic account Ic list @@ -94,6 +106,21 @@ Returns the Postgres credentials for the account. .It Nm Ic pgcreds Ic set Fl id Ar uuid Fl host Ar host Op Fl port Ar port Fl db Ar db Fl user Ar user Fl password Ar pass Sets Postgres credentials for the account. .El +.Ss policy +.Bl -tag -width Ds +.It Nm Ic policy Ic list +Lists all policy rules. +.It Nm Ic policy Ic create Fl description Ar str Fl json Ar file Op Fl priority Ar n Op Fl not-before Ar rfc3339 Op Fl expires-at Ar rfc3339 +Creates a new policy rule. +.Ar file +must be a path to a file containing a JSON rule body. +.It Nm Ic policy Ic get Fl id Ar id +Returns the policy rule with the given ID. +.It Nm Ic policy Ic update Fl id Ar id Op Fl priority Ar n Op Fl enabled Ar true|false Op Fl not-before Ar rfc3339 Op Fl expires-at Ar rfc3339 Op Fl clear-not-before Op Fl clear-expires-at +Applies a partial update to a policy rule. +.It Nm Ic policy Ic delete Fl id Ar id +Permanently removes a policy rule. +.El .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev MCIAS_TOKEN diff --git a/proto/generate.go b/proto/generate.go index 8b1ffb3..3715314 100644 --- a/proto/generate.go +++ b/proto/generate.go @@ -6,5 +6,5 @@ // // Prerequisites: protoc, protoc-gen-go, protoc-gen-go-grpc must be in PATH. // -//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto +//go:generate protoc --proto_path=../proto --go_out=../gen --go_opt=paths=source_relative --go-grpc_out=../gen --go-grpc_opt=paths=source_relative mcias/v1/common.proto mcias/v1/admin.proto mcias/v1/auth.proto mcias/v1/token.proto mcias/v1/account.proto mcias/v1/policy.proto package proto diff --git a/proto/mcias/v1/policy.proto b/proto/mcias/v1/policy.proto new file mode 100644 index 0000000..483767d --- /dev/null +++ b/proto/mcias/v1/policy.proto @@ -0,0 +1,104 @@ +// PolicyService: CRUD management of policy rules. +syntax = "proto3"; + +package mcias.v1; + +option go_package = "git.wntrmute.dev/kyle/mcias/gen/mcias/v1;mciasv1"; + +// PolicyRule is the wire representation of a policy rule record. +message PolicyRule { + int64 id = 1; + string description = 2; + int32 priority = 3; + bool enabled = 4; + string rule_json = 5; // JSON-encoded RuleBody + string created_at = 6; // RFC3339 + string updated_at = 7; // RFC3339 + string not_before = 8; // RFC3339; empty if unset + string expires_at = 9; // RFC3339; empty if unset +} + +// --- List --- + +message ListPolicyRulesRequest {} + +message ListPolicyRulesResponse { + repeated PolicyRule rules = 1; +} + +// --- Create --- + +message CreatePolicyRuleRequest { + string description = 1; // required + string rule_json = 2; // required; JSON-encoded RuleBody + int32 priority = 3; // default 100 when zero + string not_before = 4; // RFC3339; optional + string expires_at = 5; // RFC3339; optional +} + +message CreatePolicyRuleResponse { + PolicyRule rule = 1; +} + +// --- Get --- + +message GetPolicyRuleRequest { + int64 id = 1; +} + +message GetPolicyRuleResponse { + PolicyRule rule = 1; +} + +// --- Update --- + +// UpdatePolicyRuleRequest carries partial updates. +// Fields left at their zero value are not changed on the server, except: +// - clear_not_before=true removes the not_before constraint +// - clear_expires_at=true removes the expires_at constraint +// has_priority / has_enabled use proto3 optional (field presence) so the +// server can distinguish "not supplied" from "set to zero/false". +message UpdatePolicyRuleRequest { + int64 id = 1; + optional int32 priority = 2; // omit to leave unchanged + optional bool enabled = 3; // omit to leave unchanged + string not_before = 4; // RFC3339; ignored when clear_not_before=true + string expires_at = 5; // RFC3339; ignored when clear_expires_at=true + bool clear_not_before = 6; + bool clear_expires_at = 7; +} + +message UpdatePolicyRuleResponse { + PolicyRule rule = 1; +} + +// --- Delete --- + +message DeletePolicyRuleRequest { + int64 id = 1; +} + +message DeletePolicyRuleResponse {} + +// PolicyService manages policy rules (admin only). +service PolicyService { + // ListPolicyRules returns all policy rules. + // Requires: admin JWT. + rpc ListPolicyRules(ListPolicyRulesRequest) returns (ListPolicyRulesResponse); + + // CreatePolicyRule creates a new policy rule. + // Requires: admin JWT. + rpc CreatePolicyRule(CreatePolicyRuleRequest) returns (CreatePolicyRuleResponse); + + // GetPolicyRule returns a single policy rule by ID. + // Requires: admin JWT. + rpc GetPolicyRule(GetPolicyRuleRequest) returns (GetPolicyRuleResponse); + + // UpdatePolicyRule applies a partial update to a policy rule. + // Requires: admin JWT. + rpc UpdatePolicyRule(UpdatePolicyRuleRequest) returns (UpdatePolicyRuleResponse); + + // DeletePolicyRule permanently removes a policy rule. + // Requires: admin JWT. + rpc DeletePolicyRule(DeletePolicyRuleRequest) returns (DeletePolicyRuleResponse); +}