From bf249fc783623202d35075aa758e64a944ba27ab Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 17 Oct 2026 01:01:48 -0400 Subject: [PATCH 1/2] feat(iamd): enforce IAM policies with typed CEL filters Add policy storage, admin management APIs, CLI templates, and per-call RPC enforcement. Propagate policy snapshots and admin identity through STS and both signature verifiers. Use cel.dev/cel-go, preserve DENY precedence and conditional filters, and fail closed when attached documents are unavailable. Cover policy evaluation, admin recovery, and cached metadata propagation with regression tests. Signed-off-by: Xe Iaso --- HANDOFF.md | 83 + cmd/iam/cmd/policies.go | 280 ++ cmd/iam/cmd/policies_test.go | 126 + cmd/iam/cmd/policy_templates.go | 92 + cmd/iam/cmd/policy_templates_test.go | 65 + cmd/iam/examples/examples.go | 10 + cmd/iam/examples/own-keys.json | 15 + cmd/iam/examples/sts-sigv4-verifier.json | 12 + cmd/iam/examples/sts-sigv4a-verifier.json | 9 + cmd/iam/examples/users-read-only.json | 10 + cmd/iamd/authz.go | 105 + cmd/iamd/integration_test.go | 13 +- cmd/iamd/main.go | 29 +- cmd/iamd/models/dao.go | 18 +- cmd/iamd/models/policies.go | 301 +++ cmd/iamd/models/policies_test.go | 259 ++ cmd/iamd/policy_integration_test.go | 256 ++ cmd/iamd/pub/iam/iamclient.go | 11 +- cmd/iamd/services/iam/policies/policies.go | 255 ++ .../services/iam/sts/policy_metadata_test.go | 77 + cmd/iamd/services/iam/sts/publickey.go | 14 +- cmd/iamd/services/iam/sts/signingkey.go | 36 +- .../logging/expressions/environment.go | 8 +- docs/README.md | 5 + docs/iam-policies.md | 284 ++ docs/iam-service-integration.md | 403 +++ docs/reviews/sigv4a-implementation.md | 140 + gen/within/website/x/iam/sts/v1/sts.pb.go | 80 +- gen/within/website/x/iam/sts/v1/sts.twirp.go | 90 +- .../x/iam/v1/iamv1connect/policy.connect.go | 365 +++ gen/within/website/x/iam/v1/policy.pb.go | 990 +++++++ gen/within/website/x/iam/v1/policy.twirp.go | 2304 +++++++++++++++++ gen/within/website/x/iam/v1/policy_grpc.pb.go | 442 ++++ go.mod | 5 +- go.sum | 48 +- iam/authz/authz.go | 251 ++ iam/authz/authz_test.go | 139 + pb/within/website/x/iam/sts/v1/sts.proto | 15 +- pb/within/website/x/iam/v1/policy.proto | 209 ++ web/middleware/authctx/authctx.go | 23 +- web/middleware/sigv4/iamsts/iamsts.go | 30 +- web/middleware/sigv4/iamsts/iamsts_test.go | 57 + web/middleware/sigv4/iamsts/integration.md | 6 + web/middleware/sigv4a/iamsts/iamsts.go | 29 +- web/middleware/sigv4a/iamsts/iamsts_test.go | 57 + web/middleware/sigv4a/iamsts/integration.md | 6 + 46 files changed, 7908 insertions(+), 154 deletions(-) create mode 100644 HANDOFF.md create mode 100644 cmd/iam/cmd/policies.go create mode 100644 cmd/iam/cmd/policies_test.go create mode 100644 cmd/iam/cmd/policy_templates.go create mode 100644 cmd/iam/cmd/policy_templates_test.go create mode 100644 cmd/iam/examples/examples.go create mode 100644 cmd/iam/examples/own-keys.json create mode 100644 cmd/iam/examples/sts-sigv4-verifier.json create mode 100644 cmd/iam/examples/sts-sigv4a-verifier.json create mode 100644 cmd/iam/examples/users-read-only.json create mode 100644 cmd/iamd/authz.go create mode 100644 cmd/iamd/models/policies.go create mode 100644 cmd/iamd/models/policies_test.go create mode 100644 cmd/iamd/policy_integration_test.go create mode 100644 cmd/iamd/services/iam/policies/policies.go create mode 100644 cmd/iamd/services/iam/sts/policy_metadata_test.go create mode 100644 docs/iam-policies.md create mode 100644 docs/iam-service-integration.md create mode 100644 docs/reviews/sigv4a-implementation.md create mode 100644 gen/within/website/x/iam/v1/iamv1connect/policy.connect.go create mode 100644 gen/within/website/x/iam/v1/policy.pb.go create mode 100644 gen/within/website/x/iam/v1/policy.twirp.go create mode 100644 gen/within/website/x/iam/v1/policy_grpc.pb.go create mode 100644 iam/authz/authz.go create mode 100644 iam/authz/authz_test.go create mode 100644 pb/within/website/x/iam/v1/policy.proto diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 00000000..7bef9c9e --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,83 @@ +# Handoff: IAM policy enforcement + +Branch: `feat/iam-policies`. + +## Failure and repair + +The previous commit failed because the evaluator source directory, `iam/`, +was deleted as a presumed build artifact. That directory contains the real +`within.website/x/iam/authz` package. The reconstructed evaluator also lost +parts of the original implementation and its documentation no longer +matched the public API. + +The failed reconstruction left these defects: + +- Policy validation declared `principal` as dynamic. Unknown principal + fields therefore passed the write-time check. +- Request environments did not reliably declare the concrete protobuf + request type. Valid field filters failed during authorization. +- Boolean filter results did not receive a consistent write-time check. +- DENY filters were ignored, and an early ALLOW skipped later DENY rules. +- Trailing wildcard matches failed when `*` matched an empty suffix. +- The evaluator lost its documented metrics. +- Corrupt policy documents blocked admin repairs. Missing attached documents + could silently remove a DENY rule. Attach and delete checks were not atomic. +- The service guide used the old variadic constructor and absent adapters. +- The handoff contained incorrect CEL API claims. Dynamic types permit field + access, but cannot check fields against a concrete protobuf schema. + +The repair uses `cel.dev/cel-go`. `TryCompile` registers the concrete +`TokenIdentity` type and requires a boolean result. Request fields remain +dynamic during policy creation because policies can target external services. +Each method's enforcement environment registers its concrete request type. + +`authz.New` accepts `map[string]interface{}`. Each entry maps a full method +name to a sample protobuf request. The service guide uses this signature +and supplies its own context helper and Twirp interceptor. It calls the +existing `Evaluator.Check` API, preserves admin bypass, and rejects missing +verified callers. + +Filters compile and evaluate per call. There is no compiled-program cache. +The metrics cover authorization decisions, filter duration, and filter errors. + +## Feature scope + +- Policy protobuf API, generated bindings, SQLite models, and admin-only + policy management handlers. +- Offline policy validation, bundled CLI templates, and signed management + commands. +- CEL authorization for iamd's mounted RPC services. +- Policy sets and admin identity in STS responses and verifier context. +- Documentation for downstream services that install their own enforcement. + +Only iamd installs policy enforcement in this repository. Policy attachments +alone do not protect another application's routes. + +## Required semantics + +- Policies match full names: `./`. +- Glob patterns match the entire name. `*` spans `/`, and `?` matches one character. +- Admin callers bypass policy evaluation. +- Any applicable DENY overrides every ALLOW. No applicable ALLOW means denial. +- A filter error makes an ALLOW inapplicable and a DENY applicable. +- iamd reads attachments per call. Downstream verifiers retain policy snapshots + until the server-advised key-cache TTL expires. +- Policy management handlers require an admin. Policies cannot delegate administration. +- Handlers retain responsibility for request validation and resource ownership. + +## Validation and delivery + +`npm test` passed after the repair. This command regenerates protobuf and Go +outputs, formats the repository, and runs `go test ./...`. Focused IAM, STS, +and SigV4/SigV4A tests also passed. Race checks passed for `iam/authz`, +`cmd/iamd/...`, and both iamsts verifier packages. The service guide examples compile with +existing IAM bindings substituted for the illustrative widget service. + +Regression tests cover DENY precedence and conditions, typed fields, boolean +filters, wildcard boundaries, unavailable documents, admin recovery, and +policy/admin metadata on both fresh and cached verification requests. + +Application imports use `cel.dev/cel-go v0.32.0`. The old CEL module remains +an indirect dependency of Protovalidate and the OpenAPI generation tool. + +The user authorized a signed Conventional Commit, branch push, and pull request. diff --git a/cmd/iam/cmd/policies.go b/cmd/iam/cmd/policies.go new file mode 100644 index 00000000..d06fde80 --- /dev/null +++ b/cmd/iam/cmd/policies.go @@ -0,0 +1,280 @@ +package cmd + +import ( + "fmt" + "io" + "os" + "text/tabwriter" + "time" + + "buf.build/go/protovalidate" + "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "within.website/x/cmd/iamd/pub/iam" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/iam/authz" +) + +func init() { + rootCmd.AddCommand(newPoliciesCmd()) +} + +func newPoliciesCmd() *cobra.Command { + policies := &cobra.Command{ + Use: "policies ", + Short: "Manage IAM policies and user attachments", + Long: "Manage immutable IAM policies. Files contain a Policy document in protobuf JSON, not a CreatePolicy request. Remote operations require admin credentials.", + } + + var validateFile string + validate := &cobra.Command{ + Use: "validate --file ", + Short: "Validate a policy file offline, without credentials", + Long: "Validate protobuf JSON, policy field rules, and CEL compilation without contacting iamd. Request-specific CEL fields are checked by the enforcing service, not by this command.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if _, err := loadPolicyFile(cmd, validateFile); err != nil { + return err + } + _, err := fmt.Fprintln(cmd.OutOrStdout(), "policy valid") + return err + }, + } + validate.Flags().StringVarP(&validateFile, "file", "f", "", "Policy JSON file, or - for stdin") + validate.MarkFlagRequired("file") + + var createFile string + create := &cobra.Command{ + Use: "create --file ", + Short: "Create a policy from a JSON file; print its ID", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + doc, err := loadPolicyFile(cmd, createFile) + if err != nil { + return err + } + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + resp, err := cli.CreatePolicy(cmd.Context(), &iamv1.CreatePolicyReq{Policy: doc}) + if err != nil { + return fmt.Errorf("creating policy: %w", err) + } + _, err = fmt.Fprintln(cmd.OutOrStdout(), resp.GetPolicy().GetId()) + return err + }, + } + create.Flags().StringVarP(&createFile, "file", "f", "", "Policy JSON file, or - for stdin") + create.MarkFlagRequired("file") + + var getJSON bool + get := &cobra.Command{ + Use: "get [--json]", + Short: "Show a policy; --json exports a reusable Policy document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + resp, err := cli.GetPolicy(cmd.Context(), &iamv1.GetPolicyReq{PolicyId: args[0]}) + if err != nil { + return fmt.Errorf("getting policy: %w", err) + } + if getJSON { + return writePolicyJSON(cmd, resp.GetPolicy()) + } + return printPolicy(cmd.OutOrStdout(), resp.GetPolicy()) + }, + } + get.Flags().BoolVarP(&getJSON, "json", "j", false, "Export the Policy as protobuf JSON") + + var listCount, listPage int32 + var listJSON bool + var listUserID string + list := &cobra.Command{ + Use: "list [--user-id ] [--json]", + Short: "List policy documents, optionally restricted to a user's attachments", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if listUserID == "" && (listCount <= 0 || listPage < 0) { + return fmt.Errorf("--count must be positive and --page must be non-negative") + } + if listUserID != "" && (cmd.Flags().Changed("count") || cmd.Flags().Changed("page")) { + return fmt.Errorf("--count and --page cannot be combined with --user-id; user attachments are not paginated") + } + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + var docs []*iamv1.Policy + if listUserID != "" { + resp, err := cli.ListPoliciesForUser(cmd.Context(), &iamv1.ListPoliciesForUserReq{UserId: listUserID}) + if err != nil { + return fmt.Errorf("listing policies for user: %w", err) + } + docs = resp.GetPolicies() + } else { + resp, err := cli.ListPolicies(cmd.Context(), &iamv1.ListPoliciesReq{Count: listCount, Page: listPage}) + if err != nil { + return fmt.Errorf("listing policies: %w", err) + } + docs = resp.GetPolicies() + } + if listJSON { + return writePolicyJSON(cmd, &iamv1.ListPoliciesResp{Policies: docs}) + } + tw := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 3, ' ', tabwriter.StripEscape|tabwriter.TabIndent) + fmt.Fprintln(tw, "Policy ID\tName\tStatements\tCreated\t") + for _, p := range docs { + fmt.Fprintf(tw, "%s\t%s\t%d\t%s\t\n", p.GetId(), p.GetName(), len(p.GetStatements()), p.GetCreatedAt().AsTime().Format(time.RFC3339)) + } + return tw.Flush() + }, + } + list.Flags().Int32VarP(&listCount, "count", "c", 40, "Maximum policies per page (global list only)") + list.Flags().Int32VarP(&listPage, "page", "p", 0, "Zero-based page (global list only)") + list.Flags().BoolVarP(&listJSON, "json", "j", false, "Format the policy list as protobuf JSON") + list.Flags().StringVar(&listUserID, "user-id", "", "List all policies attached to this user") + + var attachUserID string + attach := &cobra.Command{ + Use: "attach --user-id ", + Short: "Attach a policy to a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + if _, err := cli.AttachPolicy(cmd.Context(), &iamv1.AttachPolicyReq{PolicyId: args[0], UserId: attachUserID}); err != nil { + return fmt.Errorf("attaching policy: %w", err) + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), "policy %s attached to user %s\n", args[0], attachUserID) + return err + }, + } + attach.Flags().StringVar(&attachUserID, "user-id", "", "User to attach the policy to") + attach.MarkFlagRequired("user-id") + + var detachUserID string + detach := &cobra.Command{ + Use: "detach --user-id ", + Short: "Detach a policy from a user", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + if _, err := cli.DetachPolicy(cmd.Context(), &iamv1.DetachPolicyReq{PolicyId: args[0], UserId: detachUserID}); err != nil { + return fmt.Errorf("detaching policy: %w", err) + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), "policy %s detached from user %s\n", args[0], detachUserID) + return err + }, + } + detach.Flags().StringVar(&detachUserID, "user-id", "", "User to detach the policy from") + detach.MarkFlagRequired("user-id") + + delete := &cobra.Command{ + Use: "delete ", + Short: "Delete an unattached policy document", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cli, err := newPolicyClient(cmd) + if err != nil { + return err + } + if _, err := cli.DeletePolicy(cmd.Context(), &iamv1.DeletePolicyReq{PolicyId: args[0]}); err != nil { + return fmt.Errorf("deleting policy: %w", err) + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), "policy %s deleted\n", args[0]) + return err + }, + } + + policies.AddCommand(validate, create, get, list, attach, detach, delete, newPolicyTemplatesCmd()) + return policies +} + +// loadPolicyFile accepts the same document get --json exports. Server-owned +// metadata may be present; CreatePolicy assigns a new ID and timestamps. +func loadPolicyFile(cmd *cobra.Command, path string) (*iamv1.Policy, error) { + var data []byte + var err error + if path == "-" { + data, err = io.ReadAll(cmd.InOrStdin()) + } else { + data, err = os.ReadFile(path) + } + if err != nil { + return nil, fmt.Errorf("reading policy %q: %w", path, err) + } + var doc iamv1.Policy + // Reject unknown and duplicate fields rather than silently discarding a + // misspelled rule. A file is a Policy, never a CreatePolicyReq wrapper. + if err := protojson.Unmarshal(data, &doc); err != nil { + return nil, fmt.Errorf("decoding policy %q: %w", path, err) + } + if err := validatePolicy(&doc, path); err != nil { + return nil, err + } + return &doc, nil +} + +func validatePolicy(doc *iamv1.Policy, source string) error { + if err := protovalidate.Validate(doc); err != nil { + return fmt.Errorf("validating policy %q: %w", source, err) + } + for i, st := range doc.GetStatements() { + if err := authz.TryCompile(st.GetFilter()); err != nil { + return fmt.Errorf("policy %q statement %d filter: %w", source, i+1, err) + } + } + return nil +} + +func newPolicyClient(cmd *cobra.Command) (iamv1.PolicyService, error) { + cfg, err := loadConfig(cfgFile) + if err != nil { + return nil, fmt.Errorf("loading config from %s: %w", cfgFile, err) + } + cli, err := iam.New(cmd.Context(), cfg.Endpoint, cfg.Region, cfg.AccessKeyID, cfg.SecretAccessKey) + if err != nil { + return nil, fmt.Errorf("creating IAM client: %w", err) + } + return cli.Policies, nil +} + +func writePolicyJSON(cmd *cobra.Command, msg proto.Message) error { + data, err := (protojson.MarshalOptions{Indent: " ", UseProtoNames: true}).Marshal(msg) + if err != nil { + return fmt.Errorf("encoding policy JSON: %w", err) + } + _, err = fmt.Fprintln(cmd.OutOrStdout(), string(data)) + return err +} + +func printPolicy(w io.Writer, p *iamv1.Policy) error { + if _, err := fmt.Fprintf(w, "Policy ID: %s\nName: %s\nCreated: %s\nUpdated: %s\n", + p.GetId(), p.GetName(), p.GetCreatedAt().AsTime().Format(time.RFC3339), p.GetUpdatedAt().AsTime().Format(time.RFC3339)); err != nil { + return err + } + for i, st := range p.GetStatements() { + if _, err := fmt.Fprintf(w, "\nStatement %d: %s\n", i+1, st.GetEffect()); err != nil { + return err + } + for _, method := range st.GetMethods() { + if _, err := fmt.Fprintf(w, " Method: %s\n", method); err != nil { + return err + } + } + if _, err := fmt.Fprintf(w, " Filter: %s\n", st.GetFilter()); err != nil { + return err + } + } + return nil +} diff --git a/cmd/iam/cmd/policies_test.go b/cmd/iam/cmd/policies_test.go new file mode 100644 index 00000000..058e14b3 --- /dev/null +++ b/cmd/iam/cmd/policies_test.go @@ -0,0 +1,126 @@ +package cmd + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/spf13/cobra" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +func TestPolicyValidateRejectsInvalidDocuments(t *testing.T) { + cases := []struct { + name string + input string + }{ + { + name: "unknown rule field", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","method":["*"]}]}`, + }, + { + name: "unknown enum value", + input: `{"name":"example","statements":[{"effect":99,"methods":["*"]}]}`, + }, + { + name: "unspecified effect", + input: `{"name":"example","statements":[{"methods":["*"]}]}`, + }, + { + name: "empty name", + input: `{"statements":[{"effect":"EFFECT_ALLOW","methods":["*"]}]}`, + }, + { + name: "empty statements", + input: `{"name":"example","statements":[]}`, + }, + { + name: "empty methods", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW"}]}`, + }, + { + name: "empty method pattern", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":[""]}]}`, + }, + { + name: "duplicate method patterns", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":["*","*"]}]}`, + }, + { + name: "invalid CEL syntax", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":["*"],"filter":"req.user_id =="}]}`, + }, + { + name: "unknown principal field", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":["*"],"filter":"principal.misspelled == 'x'"}]}`, + }, + { + name: "RPC wrapper instead of document", + input: `{"policy":{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":["*"]}]}}`, + }, + { + name: "duplicate document fields", + input: `{"name":"first","name":"second","statements":[{"effect":"EFFECT_ALLOW","methods":["*"]}]}`, + }, + { + name: "multiple JSON documents", + input: `{"name":"example","statements":[{"effect":"EFFECT_ALLOW","methods":["*"]}]} {}`, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + cmd := newPoliciesCmd() + cmd.SetIn(strings.NewReader(tt.input)) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + cmd.SetArgs([]string{"validate", "--file", "-"}) + if err := cmd.Execute(); err == nil { + t.Error("validate accepted an invalid document") + } + }) + } +} + +func TestPolicyExportImportRoundTrip(t *testing.T) { + original := &iamv1.Policy{ + Id: "019d0000-0000-7000-8000-000000000001", + Name: "own-keys", + Statements: []*iamv1.PolicyStatement{ + { + Effect: iamv1.Effect_EFFECT_ALLOW, + Methods: []string{"within.website.x.iam.v1.KeyService/ListKeys"}, + Filter: `req.user_id == "" || req.user_id == principal.principal_id`, + }, + { + Effect: iamv1.Effect_EFFECT_DENY, + Methods: []string{"*/CreateKey"}, + Filter: `req.user_id != principal.principal_id`, + }, + }, + CreatedAt: timestamppb.New(time.Date(2026, 10, 16, 12, 0, 0, 0, time.UTC)), + UpdatedAt: timestamppb.New(time.Date(2026, 10, 16, 13, 0, 0, 0, time.UTC)), + } + var out bytes.Buffer + cmd := &cobra.Command{} + cmd.SetOut(&out) + if err := writePolicyJSON(cmd, original); err != nil { + t.Fatal(err) + } + path := filepath.Join(t.TempDir(), "exported.json") + if err := os.WriteFile(path, out.Bytes(), 0600); err != nil { + t.Fatal(err) + } + decoded, err := loadPolicyFile(cmd, path) + if err != nil { + t.Fatalf("importing exported policy: %v", err) + } + if !proto.Equal(original, decoded) { + t.Errorf("export/import changed the policy:\nwant %v\ngot %v", original, decoded) + } +} diff --git a/cmd/iam/cmd/policy_templates.go b/cmd/iam/cmd/policy_templates.go new file mode 100644 index 00000000..b35017fd --- /dev/null +++ b/cmd/iam/cmd/policy_templates.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "fmt" + "strconv" + "strings" + + "github.com/spf13/cobra" + "google.golang.org/protobuf/encoding/protojson" + "within.website/x/cmd/iam/examples" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +func newPolicyTemplatesCmd() *cobra.Command { + templates := &cobra.Command{ + Use: "templates ", + Short: "List and render built-in policy templates offline", + Long: "List and render the policy examples bundled with this binary. No credentials, server connection, or source checkout are required. Rendering does not create or attach a policy.", + } + list := &cobra.Command{ + Use: "list", + Short: "List available template names", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + entries, err := examples.Policies.ReadDir(".") + if err != nil { + return fmt.Errorf("listing policy templates: %w", err) + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + if _, err := fmt.Fprintln(cmd.OutOrStdout(), strings.TrimSuffix(entry.Name(), ".json")); err != nil { + return err + } + } + return nil + }, + } + + var name, region, service string + render := &cobra.Command{ + Use: "render ", + Short: "Write a policy document as protobuf JSON to stdout", + Long: "Render a built-in template, optionally changing the policy name. Save stdout to a file or pipe it to policies create --file -. " + + "For sts-sigv4-verifier only, --region and --service together replace the example's yow/iam scope. " + + "That template grants forgery-capable symmetric signing keys; prefer sts-sigv4a-verifier for verification-only access.", + Example: " iam policies templates render own-keys --name alice-keys > alice-keys.json\n" + + " iam policies templates render sts-sigv4-verifier --region us-east-1 --service mi --name mi-verifier", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + template := args[0] + data, err := examples.Policies.ReadFile(template + ".json") + if err != nil { + return fmt.Errorf("loading policy template %q (see policies templates list): %w", template, err) + } + var doc iamv1.Policy + if err := protojson.Unmarshal(data, &doc); err != nil { + return fmt.Errorf("decoding policy template %q: %w", template, err) + } + if cmd.Flags().Changed("region") || cmd.Flags().Changed("service") { + if template != "sts-sigv4-verifier" { + return fmt.Errorf("--region and --service apply only to sts-sigv4-verifier") + } + if region == "" || service == "" { + return fmt.Errorf("--region and --service must be non-empty") + } + if len(doc.GetStatements()) != 1 { + return fmt.Errorf("sts-sigv4-verifier must contain exactly one scoped statement") + } + // Quote scope values as CEL string literals so CLI arguments + // cannot inject expressions or widen the template's grant. + doc.Statements[0].Filter = fmt.Sprintf("req.region == %s && req.service == %s", strconv.Quote(region), strconv.Quote(service)) + doc.Name = fmt.Sprintf("sts-sigv4-verifier-%s-%s", region, service) + } + if cmd.Flags().Changed("name") { + doc.Name = name + } + if err := validatePolicy(&doc, "template "+template); err != nil { + return err + } + return writePolicyJSON(cmd, &doc) + }, + } + render.Flags().StringVar(&name, "name", "", "Override the policy document's name") + render.Flags().StringVar(®ion, "region", "", "Override the classic SigV4 region; requires --service") + render.Flags().StringVar(&service, "service", "", "Override the classic SigV4 service; requires --region") + render.MarkFlagsRequiredTogether("region", "service") + + templates.AddCommand(list, render) + return templates +} diff --git a/cmd/iam/cmd/policy_templates_test.go b/cmd/iam/cmd/policy_templates_test.go new file mode 100644 index 00000000..1c77d4c0 --- /dev/null +++ b/cmd/iam/cmd/policy_templates_test.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "bytes" + "testing" + + "google.golang.org/protobuf/encoding/protojson" + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/iam/authz" +) + +// Scope overrides must remain literal comparisons, including when argument +// values contain CEL syntax. A quoting mistake here could turn a restricted +// signing-key policy into an allow-all grant. +func TestTemplateScopeIsolation(t *testing.T) { + eval, err := authz.New(map[string]interface{}{ + "within.website.x.iam.sts.v1.SigningKeyService/GetSigningKey": &stsv1.GetSigningKeyRequest{}, + }) + if err != nil { + t.Fatal(err) + } + cases := []struct { + name string + region string + service string + }{ + {name: "ordinary scope", region: "us-east-1", service: "mi"}, + {name: "single quotes", region: "yow' || true || '", service: "iam"}, + {name: "double quotes", region: "yow", service: `iam" || true || "`}, + {name: "escaped characters", region: "region\\name", service: "service\nname"}, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + cmd := newPoliciesCmd() + var stdout, stderr bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"templates", "render", "sts-sigv4-verifier", "--region", tt.region, "--service", tt.service, "--name", "restricted-verifier"}) + if err := cmd.Execute(); err != nil { + t.Fatalf("render: %v (%s)", err, &stderr) + } + var doc iamv1.Policy + if err := protojson.Unmarshal(stdout.Bytes(), &doc); err != nil { + t.Fatal(err) + } + method := "within.website.x.iam.sts.v1.SigningKeyService/GetSigningKey" + principal := &stsv1.TokenIdentity{PrincipalId: "verifier"} + for _, request := range []struct { + region string + service string + allow bool + }{ + {region: tt.region, service: tt.service, allow: true}, + {region: "other-region", service: tt.service, allow: false}, + {region: tt.region, service: "other-service", allow: false}, + } { + decision := eval.Check([]*iamv1.Policy{&doc}, method, &stsv1.GetSigningKeyRequest{Region: request.region, Service: request.service}, principal) + if decision.Allowed != request.allow { + t.Errorf("region=%q service=%q: got allow=%v (%s), want %v", request.region, request.service, decision.Allowed, decision.Reason, request.allow) + } + } + }) + } +} diff --git a/cmd/iam/examples/examples.go b/cmd/iam/examples/examples.go new file mode 100644 index 00000000..58cf6b34 --- /dev/null +++ b/cmd/iam/examples/examples.go @@ -0,0 +1,10 @@ +// Package examples contains the source documents for the IAM CLI's built-in +// policy templates. Editing an example updates its template on the next build. +package examples + +import "embed" + +// Policies contains the example policy documents, keyed by filename. +// +//go:embed *.json +var Policies embed.FS diff --git a/cmd/iam/examples/own-keys.json b/cmd/iam/examples/own-keys.json new file mode 100644 index 00000000..cf4b4c46 --- /dev/null +++ b/cmd/iam/examples/own-keys.json @@ -0,0 +1,15 @@ +{ + "name": "own-keys", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": ["within.website.x.iam.v1.KeyService/ListKeys"], + "filter": "req.user_id == '' || req.user_id == principal.principal_id" + }, + { + "effect": "EFFECT_ALLOW", + "methods": ["within.website.x.iam.v1.KeyService/CreateKey"], + "filter": "req.user_id == principal.principal_id" + } + ] +} diff --git a/cmd/iam/examples/sts-sigv4-verifier.json b/cmd/iam/examples/sts-sigv4-verifier.json new file mode 100644 index 00000000..f52932f8 --- /dev/null +++ b/cmd/iam/examples/sts-sigv4-verifier.json @@ -0,0 +1,12 @@ +{ + "name": "sts-sigv4-verifier-yow-iam", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": [ + "within.website.x.iam.sts.v1.SigningKeyService/GetSigningKey" + ], + "filter": "req.region == 'yow' && req.service == 'iam'" + } + ] +} diff --git a/cmd/iam/examples/sts-sigv4a-verifier.json b/cmd/iam/examples/sts-sigv4a-verifier.json new file mode 100644 index 00000000..067637f0 --- /dev/null +++ b/cmd/iam/examples/sts-sigv4a-verifier.json @@ -0,0 +1,9 @@ +{ + "name": "sts-sigv4a-verifier", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": ["within.website.x.iam.sts.v1.SigningKeyService/GetPublicKey"] + } + ] +} diff --git a/cmd/iam/examples/users-read-only.json b/cmd/iam/examples/users-read-only.json new file mode 100644 index 00000000..12662bb1 --- /dev/null +++ b/cmd/iam/examples/users-read-only.json @@ -0,0 +1,10 @@ +{ + "name": "users-read-only", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": ["within.website.x.iam.v1.UserService/ListUsers"], + "filter": "req.count > 0 && req.count <= 50 && req.page >= 0" + } + ] +} diff --git a/cmd/iamd/authz.go b/cmd/iamd/authz.go new file mode 100644 index 00000000..a6df775e --- /dev/null +++ b/cmd/iamd/authz.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + + "github.com/twitchtv/twirp" + "google.golang.org/protobuf/proto" + "within.website/x/cmd/iamd/models" + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/iam/authz" + "within.website/x/web/middleware/authctx" +) + +// newAuthorizer builds the CEL evaluator over every mounted method. methodTypes +// maps a full method name to a sample request message so the CEL environment +// can resolve req fields. +func newAuthorizer() (*authz.Evaluator, error) { + methodTypes := map[string]interface{}{ + authz.MethodName(iamv1.UserService_ServiceDesc.ServiceName, "CreateUser"): &iamv1.CreateUserReq{}, + authz.MethodName(iamv1.UserService_ServiceDesc.ServiceName, "DisableUser"): &iamv1.DisableUserReq{}, + authz.MethodName(iamv1.UserService_ServiceDesc.ServiceName, "ListUsers"): &iamv1.ListUsersReq{}, + authz.MethodName(iamv1.KeyService_ServiceDesc.ServiceName, "CreateKey"): &iamv1.CreateKeyReq{}, + authz.MethodName(iamv1.KeyService_ServiceDesc.ServiceName, "DisableKey"): &iamv1.DisableKeyReq{}, + authz.MethodName(iamv1.KeyService_ServiceDesc.ServiceName, "ListKeys"): &iamv1.ListKeysReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "CreatePolicy"): &iamv1.CreatePolicyReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "DeletePolicy"): &iamv1.DeletePolicyReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "GetPolicy"): &iamv1.GetPolicyReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "ListPolicies"): &iamv1.ListPoliciesReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "AttachPolicy"): &iamv1.AttachPolicyReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "DetachPolicy"): &iamv1.DetachPolicyReq{}, + authz.MethodName(iamv1.PolicyService_ServiceDesc.ServiceName, "ListPoliciesForUser"): &iamv1.ListPoliciesForUserReq{}, + authz.MethodName(stsv1.SigningKeyService_ServiceDesc.ServiceName, "GetSigningKey"): &stsv1.GetSigningKeyRequest{}, + authz.MethodName(stsv1.SigningKeyService_ServiceDesc.ServiceName, "GetPublicKey"): &stsv1.GetPublicKeyRequest{}, + } + eval, err := authz.New(methodTypes) + if err != nil { + return nil, fmt.Errorf("can't build policy evaluator: %w", err) + } + return eval, nil +} + +// fullServiceNames maps the short service names Twirp puts in ctx to the +// full proto names policy statements match against. Values come from the +// generated gRPC ServiceDescs, so they cannot drift from the protos. +var fullServiceNames = map[string]string{ + "UserService": iamv1.UserService_ServiceDesc.ServiceName, + "KeyService": iamv1.KeyService_ServiceDesc.ServiceName, + "PolicyService": iamv1.PolicyService_ServiceDesc.ServiceName, + "SigningKeyService": stsv1.SigningKeyService_ServiceDesc.ServiceName, +} + +// policyInterceptor authorizes every Twirp call after the route middleware +// has authenticated the caller and resolved its user. It runs policy +// evaluation (see within.website/x/iam/authz) with the decoded request +// message; PolicyService handlers additionally gate on is_admin inside +// themselves, so administration can never be reached through a policy +// document. Policies are read per call so attaches and detaches take effect +// on the next request. +func policyInterceptor(lg *slog.Logger, dao *models.DAO, eval *authz.Evaluator) twirp.Interceptor { + return func(next twirp.Method) twirp.Method { + return func(ctx context.Context, req interface{}) (interface{}, error) { + u, ok := authctx.User(ctx) + if !ok { + return nil, twirp.NewError(twirp.Unauthenticated, "no verified caller in context") + } + // Admin access must survive missing or corrupt policy documents so + // the caller can repair policy attachments through PolicyService. + if u.GetIsAdmin() { + eval.Check(nil, "", nil, authz.PrincipalFromUser("", u)) + return next(ctx, req) + } + + msg, ok := req.(proto.Message) + if !ok { + // Every mounted route takes a proto request; anything else is + // a wiring fault. Fail closed. + return nil, twirp.InternalErrorf("authz: request is not a proto message") + } + + svc, _ := twirp.ServiceName(ctx) + mth, _ := twirp.MethodName(ctx) + method := authz.MethodName(fullServiceNames[svc], mth) + keyID, _ := authctx.KeyID(ctx) + principal := authz.PrincipalFromUser(keyID, u) + + policies, err := dao.PoliciesForUser(ctx, u.GetId()) + if err != nil { + lg.ErrorContext(ctx, "authz: can't load policies", "err", err, "user", u.GetId()) + return nil, twirp.InternalErrorf("authz: can't load policies") + } + + d := eval.Check(policies, method, msg, principal) + if !d.Allowed { + lg.WarnContext(ctx, "authz: denied", "method", method, "user", u.GetId(), "reason", d.Reason) + return nil, twirp.NewError(twirp.PermissionDenied, d.Reason) + } + lg.DebugContext(ctx, "authz: allowed", "method", method, "user", u.GetId(), "reason", d.Reason) + + return next(ctx, req) + } + } +} diff --git a/cmd/iamd/integration_test.go b/cmd/iamd/integration_test.go index 88d0986d..c656991d 100644 --- a/cmd/iamd/integration_test.go +++ b/cmd/iamd/integration_test.go @@ -121,7 +121,11 @@ func TestIntegration_SignedTwirpRoundTrip(t *testing.T) { } authMW := newDualVerifier(dao, intRegion, intService, 1<<20) - mux := newMux(quietLogger(), dao, authMW, 5*time.Minute) + eval, err := newAuthorizer() + if err != nil { + t.Fatalf("newAuthorizer: %v", err) + } + mux := newMux(quietLogger(), dao, authMW, eval, 5*time.Minute) srv := httptest.NewServer(mux) defer srv.Close() @@ -252,9 +256,12 @@ func TestIntegration_SigningKeyVerifierChain(t *testing.T) { } authMW := newDualVerifier(dao, intRegion, intService, 1<<20) - iamd := httptest.NewServer(newMux(quietLogger(), dao, authMW, 5*time.Minute)) + eval, err := newAuthorizer() + if err != nil { + t.Fatalf("newAuthorizer: %v", err) + } + iamd := httptest.NewServer(newMux(quietLogger(), dao, authMW, eval, 5*time.Minute)) defer iamd.Close() - // The downstream service: fetches signing keys from iamd (signing those // fetches with its own credential) and verifies end-user requests locally. downstreamVerifier := iamsts.New(iamsts.Config{ diff --git a/cmd/iamd/main.go b/cmd/iamd/main.go index feade828..8036890f 100644 --- a/cmd/iamd/main.go +++ b/cmd/iamd/main.go @@ -15,10 +15,12 @@ import ( "within.website/x" "within.website/x/cmd/iamd/models" "within.website/x/cmd/iamd/services/iam/keys" + "within.website/x/cmd/iamd/services/iam/policies" "within.website/x/cmd/iamd/services/iam/sts" "within.website/x/cmd/iamd/services/iam/users" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/iam/authz" "within.website/x/internal" "within.website/x/twirp/twirpslog" ) @@ -56,21 +58,33 @@ func main() { // authMW per request), then resolve the caller to its DAO user (available // downstream via sigv4a.User). The SigningKeyService route's callers are // downstream verifiers authenticating with their own IAM credential. -func newMux(lg *slog.Logger, dao *models.DAO, authMW func(http.Handler) http.Handler, signingKeyCacheTTL time.Duration) *http.ServeMux { +func newMux(lg *slog.Logger, dao *models.DAO, authMW func(http.Handler) http.Handler, eval *authz.Evaluator, signingKeyCacheTTL time.Duration) *http.ServeMux { mux := http.NewServeMux() // Verify the signature, then annotate the request with the caller's user // (read downstream via sigv4a.User). stack := chain(authMW, UserMiddleware(dao)) + // Authorization runs inside each Twirp server as an interceptor, where + // the decoded request message is available: policy filters evaluate + // over (method, req, principal). The logging interceptor is outermost + // so denials are logged with their outcome. + withAuthz := twirp.WithServerInterceptors( + twirpslog.Interceptor(lg), + policyInterceptor(lg, dao, eval), + ) + us := users.New(dao) - mux.Handle(iamv1.UserServicePathPrefix, stack(iamv1.NewUserServiceServer(us, twirp.WithServerInterceptors(twirpslog.Interceptor(lg))))) + mux.Handle(iamv1.UserServicePathPrefix, stack(iamv1.NewUserServiceServer(us, withAuthz))) ks := keys.New(dao) - mux.Handle(iamv1.KeyServicePathPrefix, stack(iamv1.NewKeyServiceServer(ks, twirp.WithServerInterceptors(twirpslog.Interceptor(lg))))) + mux.Handle(iamv1.KeyServicePathPrefix, stack(iamv1.NewKeyServiceServer(ks, withAuthz))) + + ps := policies.New(dao) + mux.Handle(iamv1.PolicyServicePathPrefix, stack(iamv1.NewPolicyServiceServer(ps, withAuthz))) sk := sts.NewSigningKeys(dao, signingKeyCacheTTL) - mux.Handle(stsv1.SigningKeyServicePathPrefix, stack(stsv1.NewSigningKeyServiceServer(sk, twirp.WithServerInterceptors(twirpslog.Interceptor(lg))))) + mux.Handle(stsv1.SigningKeyServicePathPrefix, stack(stsv1.NewSigningKeyServiceServer(sk, withAuthz))) return mux } @@ -111,7 +125,12 @@ func run(ctx context.Context, lg *slog.Logger) error { // The route middleware is the dual verifier's only consumer: it // authenticates every caller to iamd, including SigningKeyService. authMW := newDualVerifier(dao, *region, *service, *maxBodySize) - mux := newMux(lg, dao, authMW, *signingKeyCacheTTL) + + eval, err := newAuthorizer() + if err != nil { + return fmt.Errorf("can't build policy evaluator: %w", err) + } + mux := newMux(lg, dao, authMW, eval, *signingKeyCacheTTL) g, ctx := errgroup.WithContext(ctx) diff --git a/cmd/iamd/models/dao.go b/cmd/iamd/models/dao.go index d8a9c3bf..e12bc893 100644 --- a/cmd/iamd/models/dao.go +++ b/cmd/iamd/models/dao.go @@ -12,9 +12,11 @@ import ( ) type DAO struct { - db *gorm.DB - keys gorm.Interface[Key] - users gorm.Interface[User] + db *gorm.DB + keys gorm.Interface[Key] + users gorm.Interface[User] + policies gorm.Interface[Policy] + userPolicies gorm.Interface[UserPolicy] } func (d *DAO) DB() *gorm.DB { @@ -63,13 +65,17 @@ func Open(dbLoc string) (*DAO, error) { if err := db.AutoMigrate( &User{}, &Key{}, + &Policy{}, + &UserPolicy{}, ); err != nil { return nil, fmt.Errorf("failed to migrate schema: %w", err) } return &DAO{ - db: db, - keys: gorm.G[Key](db), - users: gorm.G[User](db), + db: db, + keys: gorm.G[Key](db), + users: gorm.G[User](db), + policies: gorm.G[Policy](db), + userPolicies: gorm.G[UserPolicy](db), }, nil } diff --git a/cmd/iamd/models/policies.go b/cmd/iamd/models/policies.go new file mode 100644 index 00000000..eb637234 --- /dev/null +++ b/cmd/iamd/models/policies.go @@ -0,0 +1,301 @@ +package models + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/google/uuid" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" + "gorm.io/gorm" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +// ErrPolicyAttached is returned by DeletePolicy while at least one user still +// has the policy attached; detach first so no enforcement snapshot can +// reference a deleted document. +var ErrPolicyAttached = errors.New("policy attached to at least one user") + +// ErrDuplicatePolicyName is returned by CreatePolicy when the name is +// already taken. +var ErrDuplicatePolicyName = errors.New("policy name already in use") + +// ErrAlreadyAttached is returned by AttachPolicy when the exact (user, +// policy) edge already exists. +var ErrAlreadyAttached = errors.New("policy already attached to user") + +// Policy is a named authorization document. Statements are stored as a JSON +// array of protojson-encoded iamv1.PolicyStatement values — proto field +// names, not Go ones — so the wire format is stable across regenerations. +type Policy struct { + gorm.Model // adds CreatedAt, UpdatedAt, DeletedAt + + UUID string `gorm:"uniqueIndex"` // uuidv7 + Name string `gorm:"uniqueIndex"` // human-readable, unique + + Statements string +} + +// UserPolicy is the attachment edge between a user and a policy. Rows are +// hard-deleted on detach: a soft-deleted row would keep occupying the unique +// (user_id, policy_id) index and block re-attaching. +type UserPolicy struct { + gorm.Model + + UserID uint `gorm:"uniqueIndex:idx_user_policy"` + PolicyID uint `gorm:"uniqueIndex:idx_user_policy"` +} + +// statementsDoc is the JSON envelope persisted in Policy.Statements. +type statementsDoc struct { + Statements []json.RawMessage `json:"statements"` +} + +func encodeStatements(stmts []*iamv1.PolicyStatement) (string, error) { + doc := statementsDoc{Statements: make([]json.RawMessage, 0, len(stmts))} + for _, s := range stmts { + b, err := protojson.Marshal(s) + if err != nil { + return "", fmt.Errorf("encode statement: %w", err) + } + doc.Statements = append(doc.Statements, b) + } + b, err := json.Marshal(doc) + if err != nil { + return "", fmt.Errorf("encode statements: %w", err) + } + return string(b), nil +} + +func decodeStatements(s string) ([]*iamv1.PolicyStatement, error) { + var doc statementsDoc + if err := json.Unmarshal([]byte(s), &doc); err != nil { + return nil, fmt.Errorf("decode statements: %w", err) + } + stmts := make([]*iamv1.PolicyStatement, 0, len(doc.Statements)) + for _, raw := range doc.Statements { + st := &iamv1.PolicyStatement{} + if err := protojson.Unmarshal(raw, st); err != nil { + return nil, fmt.Errorf("decode statement: %w", err) + } + stmts = append(stmts, st) + } + return stmts, nil +} + +// AsProto converts the stored row to its proto form. It errors on corrupted +// statement payloads rather than silently dropping statements: a policy that +// half-loads must not half-apply. +func (p *Policy) AsProto() (*iamv1.Policy, error) { + stmts, err := decodeStatements(p.Statements) + if err != nil { + return nil, fmt.Errorf("policy %s (%s): %w", p.UUID, p.Name, err) + } + return &iamv1.Policy{ + Id: p.UUID, + Name: p.Name, + Statements: stmts, + CreatedAt: timestamppb.New(p.CreatedAt), + UpdatedAt: timestamppb.New(p.UpdatedAt), + }, nil +} + +func asProtos(ps []Policy) ([]*iamv1.Policy, error) { + out := make([]*iamv1.Policy, 0, len(ps)) + for i := range ps { + p, err := ps[i].AsProto() + if err != nil { + return nil, err + } + out = append(out, p) + } + return out, nil +} + +// isUniqueViolation reports whether err is a unique-index violation from the +// SQLite driver. gorm's TranslateError is off in this deployment, so the +// constraint surfaces as a driver error string. +func isUniqueViolation(err error) bool { + for ; err != nil; err = errors.Unwrap(err) { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + return true + } + } + return false +} + +// CreatePolicy stores a new document with a fresh uuidv7 id. A duplicate +// name returns an error for which isUniqueViolation is true. +func (d *DAO) CreatePolicy(ctx context.Context, name string, stmts []*iamv1.PolicyStatement) (*iamv1.Policy, error) { + id, err := uuid.NewV7() + if err != nil { + return nil, err + } + + encoded, err := encodeStatements(stmts) + if err != nil { + return nil, err + } + + p := Policy{ + UUID: id.String(), + Name: name, + Statements: encoded, + } + if err := d.policies.Create(ctx, &p); err != nil { + if isUniqueViolation(err) { + return nil, ErrDuplicatePolicyName + } + return nil, err + } + + return p.AsProto() +} + +// GetPolicy returns the policy with the given public id (UUID), as proto. +// It returns an error wrapping gorm.ErrRecordNotFound if no such policy +// exists (or it is soft-deleted). +func (d *DAO) GetPolicy(ctx context.Context, policyID string) (*iamv1.Policy, error) { + p, err := d.policies.Where("uuid = ?", policyID).First(ctx) + if err != nil { + return nil, err + } + return p.AsProto() +} + +// ListPolicies enumerates documents, newest first, paginated. +func (d *DAO) ListPolicies(ctx context.Context, count, page int) ([]*iamv1.Policy, error) { + ps, err := d.policies. + Order("created_at DESC"). + Limit(count). + Offset(count * page). + Find(ctx) + if err != nil { + return nil, err + } + return asProtos(ps) +} + +// DeletePolicy soft-deletes the document with the given public id. It +// refuses with ErrPolicyAttached while any live attachment exists. +func (d *DAO) DeletePolicy(ctx context.Context, policyID string) error { + return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + policies := gorm.G[Policy](tx) + userPolicies := gorm.G[UserPolicy](tx) + p, err := policies.Where("uuid = ?", policyID).First(ctx) + if err != nil { + return err + } + + edges, err := userPolicies.Where("policy_id = ?", p.Model.ID).Find(ctx) + if err != nil { + return err + } + if len(edges) > 0 { + return ErrPolicyAttached + } + + if _, err := policies.Where("uuid = ?", policyID).Delete(ctx); err != nil { + return err + } + return nil + }) +} + +// AttachPolicy binds a policy to a user, both by public ids. It returns an +// error wrapping gorm.ErrRecordNotFound when either side is missing, and +// ErrAlreadyAttached when the edge exists. +func (d *DAO) AttachPolicy(ctx context.Context, policyID, userID string) error { + return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + policies := gorm.G[Policy](tx) + userPolicies := gorm.G[UserPolicy](tx) + users := gorm.G[User](tx) + p, err := policies.Where("uuid = ?", policyID).First(ctx) + if err != nil { + return err + } + u, err := users.Where("uuid = ?", userID).First(ctx) + if err != nil { + return err + } + + existing, err := userPolicies.Where("user_id = ? AND policy_id = ?", u.Model.ID, p.Model.ID).Find(ctx) + if err != nil { + return err + } + if len(existing) > 0 { + return ErrAlreadyAttached + } + + edge := UserPolicy{UserID: u.Model.ID, PolicyID: p.Model.ID} + if err := userPolicies.Create(ctx, &edge); err != nil { + // A concurrent attach raced us onto the unique index. + if isUniqueViolation(err) { + return ErrAlreadyAttached + } + return err + } + return nil + }) +} + +// DetachPolicy removes the (policy, user) edge. The edge row is +// hard-deleted so re-attaching later is not blocked by the unique index. +// It returns an error wrapping gorm.ErrRecordNotFound when the edge does +// not exist. +func (d *DAO) DetachPolicy(ctx context.Context, policyID, userID string) error { + p, err := d.policies.Where("uuid = ?", policyID).First(ctx) + if err != nil { + return err + } + u, err := d.users.Where("uuid = ?", userID).First(ctx) + if err != nil { + return err + } + + edges := gorm.G[UserPolicy](d.db.Unscoped()) + n, err := edges.Where("user_id = ? AND policy_id = ?", u.Model.ID, p.Model.ID).Delete(ctx) + if err != nil { + return err + } + if n == 0 { + return gorm.ErrRecordNotFound + } + return nil +} + +// PoliciesForUser returns every live policy attached to the user with the +// given public id, as protos ready for evaluation. Attachments to missing +// rows fail closed rather than silently omitting a possible DENY statement; +// the user resolves or the whole call fails with gorm.ErrRecordNotFound. +func (d *DAO) PoliciesForUser(ctx context.Context, userID string) ([]*iamv1.Policy, error) { + u, err := d.users.Where("uuid = ?", userID).First(ctx) + if err != nil { + return nil, err + } + + edges, err := d.userPolicies.Where("user_id = ?", u.Model.ID).Find(ctx) + if err != nil { + return nil, err + } + if len(edges) == 0 { + return nil, nil + } + + ids := make([]uint, 0, len(edges)) + for _, e := range edges { + ids = append(ids, e.PolicyID) + } + + ps, err := d.policies.Where("id IN ?", ids).Order("created_at DESC").Find(ctx) + if err != nil { + return nil, err + } + if len(ps) != len(edges) { + return nil, fmt.Errorf("user %s has an attachment to an unavailable policy", userID) + } + return asProtos(ps) +} diff --git a/cmd/iamd/models/policies_test.go b/cmd/iamd/models/policies_test.go new file mode 100644 index 00000000..5f2ccffc --- /dev/null +++ b/cmd/iamd/models/policies_test.go @@ -0,0 +1,259 @@ +package models + +import ( + "context" + "errors" + "testing" + + "gorm.io/gorm" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +func allowPolicyStmt(pattern string) []*iamv1.PolicyStatement { + return []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_ALLOW, + Methods: []string{pattern}, + Filter: `req.user_id == principal.principal_id`, + }} +} + +func mustCreatePolicy(t *testing.T, d *DAO, name string, stmts []*iamv1.PolicyStatement) *iamv1.Policy { + t.Helper() + p, err := d.CreatePolicy(context.Background(), name, stmts) + if err != nil { + t.Fatalf("CreatePolicy(%q): %v", name, err) + } + return p +} + +func TestCreatePolicyRoundTrip(t *testing.T) { + d := openTestDAO(t) + + p := mustCreatePolicy(t, d, "own-keys", allowPolicyStmt("within.website.x.iam.v1.KeyService/*")) + + if p.GetId() == "" { + t.Error("CreatePolicy left id empty") + } + if len(p.GetStatements()) != 1 { + t.Fatalf("stored statements = %d, want 1", len(p.GetStatements())) + } + st := p.GetStatements()[0] + if st.GetEffect() != iamv1.Effect_EFFECT_ALLOW { + t.Errorf("effect = %v, want ALLOW", st.GetEffect()) + } + if got := st.GetMethods(); len(got) != 1 || got[0] != "within.website.x.iam.v1.KeyService/*" { + t.Errorf("methods = %v, want [within.website.x.iam.v1.KeyService/*]", got) + } + if st.GetFilter() != `req.user_id == principal.principal_id` { + t.Errorf("filter = %q, changed in storage", st.GetFilter()) + } + + got, err := d.GetPolicy(context.Background(), p.GetId()) + if err != nil { + t.Fatalf("GetPolicy: %v", err) + } + if got.GetName() != "own-keys" { + t.Errorf("name = %q, want own-keys", got.GetName()) + } + if len(got.GetStatements()) != 1 { + t.Errorf("round-trip statements = %d, want 1", len(got.GetStatements())) + } +} + +func TestCreatePolicyDuplicateName(t *testing.T) { + d := openTestDAO(t) + + mustCreatePolicy(t, d, "dup", allowPolicyStmt("*")) + + _, err := d.CreatePolicy(context.Background(), "dup", allowPolicyStmt("*")) + if !errors.Is(err, ErrDuplicatePolicyName) { + t.Errorf("duplicate name error = %v, want ErrDuplicatePolicyName", err) + } +} + +func TestGetPolicyMissing(t *testing.T) { + d := openTestDAO(t) + + _, err := d.GetPolicy(context.Background(), "no-such-id") + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("GetPolicy(missing) error = %v, want ErrRecordNotFound", err) + } +} + +func TestListPolicies(t *testing.T) { + d := openTestDAO(t) + + mustCreatePolicy(t, d, "a", allowPolicyStmt("*")) + mustCreatePolicy(t, d, "b", allowPolicyStmt("*")) + mustCreatePolicy(t, d, "c", allowPolicyStmt("*")) + + got, err := d.ListPolicies(context.Background(), 2, 0) + if err != nil { + t.Fatalf("ListPolicies: %v", err) + } + if len(got) != 2 { + t.Fatalf("page 0 count = %d, want 2", len(got)) + } + if got[0].GetName() != "c" || got[1].GetName() != "b" { + t.Errorf("page 0 = [%s %s], want newest first [c b]", got[0].GetName(), got[1].GetName()) + } + + got, err = d.ListPolicies(context.Background(), 2, 1) + if err != nil { + t.Fatalf("ListPolicies page 1: %v", err) + } + if len(got) != 1 || got[0].GetName() != "a" { + t.Errorf("page 1 = %v, want [a]", got) + } +} + +func TestDeletePolicyAttachmentGuard(t *testing.T) { + d := openTestDAO(t) + ctx := context.Background() + + p := mustCreatePolicy(t, d, "guarded", allowPolicyStmt("*")) + u := mustCreateUser(t, d, "alice") + if err := d.AttachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("AttachPolicy: %v", err) + } + + err := d.DeletePolicy(ctx, p.GetId()) + if !errors.Is(err, ErrPolicyAttached) { + t.Fatalf("DeletePolicy while attached error = %v, want ErrPolicyAttached", err) + } + + if err := d.DetachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("DetachPolicy: %v", err) + } + if err := d.DeletePolicy(ctx, p.GetId()); err != nil { + t.Fatalf("DeletePolicy after detach: %v", err) + } + if _, err := d.GetPolicy(ctx, p.GetId()); !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("GetPolicy after delete error = %v, want ErrRecordNotFound", err) + } +} + +func TestAttachDetachLifecycle(t *testing.T) { + d := openTestDAO(t) + ctx := context.Background() + + p := mustCreatePolicy(t, d, "lifecycle", allowPolicyStmt("*")) + u := mustCreateUser(t, d, "bob") + + ps, err := d.PoliciesForUser(ctx, u.UUID) + if err != nil { + t.Fatalf("PoliciesForUser empty: %v", err) + } + if len(ps) != 0 { + t.Fatalf("PoliciesForUser before attach = %d, want 0", len(ps)) + } + + if err := d.AttachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("AttachPolicy: %v", err) + } + + err = d.AttachPolicy(ctx, p.GetId(), u.UUID) + if !errors.Is(err, ErrAlreadyAttached) { + t.Errorf("second attach error = %v, want ErrAlreadyAttached", err) + } + + ps, err = d.PoliciesForUser(ctx, u.UUID) + if err != nil { + t.Fatalf("PoliciesForUser: %v", err) + } + if len(ps) != 1 || ps[0].GetName() != "lifecycle" { + t.Fatalf("PoliciesForUser = %v, want [lifecycle]", ps) + } + + // Detach is hard-delete: re-attach must succeed on the same edge. + if err := d.DetachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("DetachPolicy: %v", err) + } + if err := d.AttachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("re-attach after detach: %v", err) + } + + err = d.DetachPolicy(ctx, p.GetId(), u.UUID) + if err != nil { + t.Fatalf("final detach: %v", err) + } + err = d.DetachPolicy(ctx, p.GetId(), u.UUID) + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("detach missing edge error = %v, want ErrRecordNotFound", err) + } +} + +func TestAttachPolicyMissingSides(t *testing.T) { + d := openTestDAO(t) + ctx := context.Background() + + u := mustCreateUser(t, d, "carol") + + if err := d.AttachPolicy(ctx, "no-such-policy", u.UUID); !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("attach with missing policy error = %v, want ErrRecordNotFound", err) + } + + p := mustCreatePolicy(t, d, "orphan", allowPolicyStmt("*")) + if err := d.AttachPolicy(ctx, p.GetId(), "no-such-user"); !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("attach with missing user error = %v, want ErrRecordNotFound", err) + } +} + +func TestPoliciesForUserDisabledUser(t *testing.T) { + d := openTestDAO(t) + ctx := context.Background() + + u := mustCreateUser(t, d, "dave") + p := mustCreatePolicy(t, d, "attached-then-disabled", allowPolicyStmt("*")) + if err := d.AttachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatalf("AttachPolicy: %v", err) + } + + if err := d.DisableUser(ctx, u.UUID, "test done"); err != nil { + t.Fatalf("DisableUser: %v", err) + } + + // The user no longer resolves, so their policy set is unreachable — + // authentication fails before authorization is ever consulted. + if _, err := d.PoliciesForUser(ctx, u.UUID); !errors.Is(err, gorm.ErrRecordNotFound) { + t.Errorf("PoliciesForUser(disabled) error = %v, want ErrRecordNotFound", err) + } +} + +func TestPoliciesForUserRejectsUnavailableDocuments(t *testing.T) { + for _, tt := range []struct { + name string + breakPolicy func(*gorm.DB, string) error + }{ + {name: "deleted", breakPolicy: func(db *gorm.DB, id string) error { + return db.Where("uuid = ?", id).Delete(&Policy{}).Error + }}, + {name: "missing", breakPolicy: func(db *gorm.DB, id string) error { + return db.Unscoped().Where("uuid = ?", id).Delete(&Policy{}).Error + }}, + {name: "corrupt", breakPolicy: func(db *gorm.DB, id string) error { + return db.Model(&Policy{}).Where("uuid = ?", id).Update("statements", "invalid JSON").Error + }}, + } { + t.Run(tt.name, func(t *testing.T) { + d := openTestDAO(t) + ctx := context.Background() + u := mustCreateUser(t, d, "alice") + allow := mustCreatePolicy(t, d, "allow", allowPolicyStmt("*")) + deny := mustCreatePolicy(t, d, "deny", []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_DENY, Methods: []string{"*"}, + }}) + for _, p := range []*iamv1.Policy{allow, deny} { + if err := d.AttachPolicy(ctx, p.GetId(), u.UUID); err != nil { + t.Fatal(err) + } + } + if err := tt.breakPolicy(d.db, deny.GetId()); err != nil { + t.Fatal(err) + } + if ps, err := d.PoliciesForUser(ctx, u.UUID); err == nil { + t.Fatalf("PoliciesForUser returned %v without its deny policy", ps) + } + }) + } +} diff --git a/cmd/iamd/policy_integration_test.go b/cmd/iamd/policy_integration_test.go new file mode 100644 index 00000000..33e50ca3 --- /dev/null +++ b/cmd/iamd/policy_integration_test.go @@ -0,0 +1,256 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/twitchtv/twirp" + "within.website/x/cmd/iamd/models" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/web/middleware/authctx" + "within.website/x/web/middleware/sigv4/iamsts" +) + +// TestIntegration_PolicyEnforcement drives the full authorization path over +// signed RPCs: an admin manages policies through PolicyService, a non-admin +// user is allowed exactly what its attached document grants, everything else +// is denied, and the admin bypass still works. +func TestIntegration_PolicyEnforcement(t *testing.T) { + ctx := context.Background() + dao := newDAO(t) + adminAKID, adminSecret := bootstrapCreds(t, dao) + + eval, err := newAuthorizer() + if err != nil { + t.Fatalf("newAuthorizer: %v", err) + } + authMW := newDualVerifier(dao, intRegion, intService, 1<<20) + srv := httptest.NewServer(newMux(quietLogger(), dao, authMW, eval, 5*time.Minute)) + defer srv.Close() + + hc := &http.Client{Transport: signedTransport(t, adminAKID, adminSecret)} + usersAPI := iamv1.NewUserServiceProtobufClient(srv.URL, hc) + policiesAPI := iamv1.NewPolicyServiceProtobufClient(srv.URL, hc) + + created, err := usersAPI.CreateUser(ctx, &iamv1.CreateUserReq{Name: "alice"}) + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + aliceUUID := created.GetUser().GetId() + aliceKeys := iamv1.NewKeyServiceProtobufClient(srv.URL, &http.Client{Transport: signedTransport(t, created.GetAccessKeyId(), created.GetSecretAccessKey())}) + aliceUsers := iamv1.NewUserServiceProtobufClient(srv.URL, &http.Client{Transport: signedTransport(t, created.GetAccessKeyId(), created.GetSecretAccessKey())}) + + // Before any policy: alice is implicitly denied everything non-self. + // (ListKeys for her own user_id is still handler-clamped to herself, but + // policy evaluation runs first and must deny.) + if _, err := aliceKeys.ListKeys(ctx, &iamv1.ListKeysReq{Count: 10, Page: 1, UserId: aliceUUID}); err == nil { + t.Fatal("ListKeys before any policy: allowed, want implicit deny") + } else if !isPermissionDenied(err) { + t.Fatalf("ListKeys before any policy: %v, want PermissionDenied", err) + } + + // Admin writes a policy: alice may manage only her own keys, and may + // never mint a key for anyone else (explicit deny). + created2, err := policiesAPI.CreatePolicy(ctx, &iamv1.CreatePolicyReq{ + Policy: &iamv1.Policy{ + Name: "own-keys-only", + Statements: []*iamv1.PolicyStatement{ + { + Effect: iamv1.Effect_EFFECT_DENY, + Methods: []string{ + "within.website.x.iam.v1.KeyService/CreateKey", + }, + Filter: `req.user_id != principal.principal_id`, + }, + { + Effect: iamv1.Effect_EFFECT_ALLOW, + Methods: []string{ + "within.website.x.iam.v1.KeyService/*", + }, + Filter: `req.user_id == "" || req.user_id == principal.principal_id`, + }, + }, + }, + }) + if err != nil { + t.Fatalf("CreatePolicy: %v", err) + } + policyID := created2.GetPolicy().GetId() + + if _, err := policiesAPI.AttachPolicy(ctx, &iamv1.AttachPolicyReq{PolicyId: policyID, UserId: aliceUUID}); err != nil { + t.Fatalf("AttachPolicy: %v", err) + } + + t.Run("allowed call passes the policy filter", func(t *testing.T) { + if _, err := aliceKeys.ListKeys(ctx, &iamv1.ListKeysReq{Count: 10, Page: 1, UserId: aliceUUID}); err != nil { + t.Fatalf("ListKeys own: %v", err) + } + }) + + t.Run("call outside the policy surface is implicitly denied", func(t *testing.T) { + if _, err := aliceUsers.ListUsers(ctx, &iamv1.ListUsersReq{Count: 10, Page: 1}); err == nil { + t.Fatal("ListUsers: allowed, want implicit deny") + } else if !isPermissionDenied(err) { + t.Fatalf("ListUsers: %v, want PermissionDenied", err) + } + }) + + t.Run("explicit deny beats allow", func(t *testing.T) { + other, err := usersAPI.CreateUser(ctx, &iamv1.CreateUserReq{Name: "bob"}) + if err != nil { + t.Fatalf("CreateUser(bob): %v", err) + } + _, err = aliceKeys.CreateKey(ctx, &iamv1.CreateKeyReq{Comment: "for bob", UserId: other.GetUser().GetId()}) + if err == nil { + t.Fatal("CreateKey for another user: allowed, want explicit deny") + } + if !isPermissionDenied(err) { + t.Fatalf("CreateKey for another user: %v, want PermissionDenied", err) + } + // The denial message names the denying policy document. + if msg := err.Error(); !strings.Contains(msg, "deny by policy") { + t.Errorf("denial message = %q, want it to name the deny statement", msg) + } + }) + + t.Run("policy administration is admin-only", func(t *testing.T) { + grant, err := policiesAPI.CreatePolicy(ctx, &iamv1.CreatePolicyReq{Policy: &iamv1.Policy{ + Name: "attempt-policy-administration", + Statements: []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_ALLOW, Methods: []string{"within.website.x.iam.v1.PolicyService/*"}, + }}, + }}) + if err != nil { + t.Fatal(err) + } + if _, err := policiesAPI.AttachPolicy(ctx, &iamv1.AttachPolicyReq{PolicyId: grant.GetPolicy().GetId(), UserId: aliceUUID}); err != nil { + t.Fatal(err) + } + alicePolicies := iamv1.NewPolicyServiceProtobufClient(srv.URL, &http.Client{Transport: signedTransport(t, created.GetAccessKeyId(), created.GetSecretAccessKey())}) + if _, err := alicePolicies.ListPolicies(ctx, &iamv1.ListPoliciesReq{Count: 10, Page: 1}); err == nil { + t.Fatal("ListPolicies by non-admin: allowed, want deny") + } else if !isPermissionDenied(err) { + t.Fatalf("ListPolicies by non-admin: %v, want PermissionDenied", err) + } + }) + + t.Run("admin bypasses policies", func(t *testing.T) { + admin, err := dao.GetKeyWithUser(ctx, adminAKID) + if err != nil { + t.Fatal(err) + } + broken, err := dao.CreatePolicy(ctx, "broken-admin-policy", []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_DENY, Methods: []string{"*"}, + }}) + if err != nil { + t.Fatal(err) + } + if err := dao.AttachPolicy(ctx, broken.GetId(), admin.User.UUID); err != nil { + t.Fatal(err) + } + if err := dao.DB().Model(&models.Policy{}).Where("uuid = ?", broken.GetId()).Update("statements", "invalid JSON").Error; err != nil { + t.Fatal(err) + } + if _, err := usersAPI.ListUsers(ctx, &iamv1.ListUsersReq{Count: 10, Page: 1}); err != nil { + t.Fatalf("admin ListUsers: %v", err) + } + // Admins must retain the management access needed to repair an + // attachment, even when its document cannot be decoded. + if _, err := policiesAPI.DetachPolicy(ctx, &iamv1.DetachPolicyReq{PolicyId: broken.GetId(), UserId: admin.User.UUID}); err != nil { + t.Fatalf("admin DetachPolicy: %v", err) + } + }) + + t.Run("detach removes the grant", func(t *testing.T) { + if _, err := policiesAPI.DetachPolicy(ctx, &iamv1.DetachPolicyReq{PolicyId: policyID, UserId: aliceUUID}); err != nil { + t.Fatalf("DetachPolicy: %v", err) + } + if _, err := aliceKeys.ListKeys(ctx, &iamv1.ListKeysReq{Count: 10, Page: 1, UserId: aliceUUID}); err == nil { + t.Fatal("ListKeys after detach: allowed, want implicit deny") + } + }) +} + +// TestIntegration_STSPropagatesPolicies checks that the signing-key service +// hands downstream verifiers the caller's policy set and admin flag, and +// that the verifier middleware surfaces both in the request context. +func TestIntegration_STSPropagatesPolicies(t *testing.T) { + ctx := context.Background() + dao := newDAO(t) + verifierAKID, verifierSecret := bootstrapCreds(t, dao) + + eval, err := newAuthorizer() + if err != nil { + t.Fatalf("newAuthorizer: %v", err) + } + authMW := newDualVerifier(dao, intRegion, intService, 1<<20) + iamd := httptest.NewServer(newMux(quietLogger(), dao, authMW, eval, 5*time.Minute)) + defer iamd.Close() + + endUser, err := dao.CreateUser(ctx, "end-user") + if err != nil { + t.Fatalf("CreateUser: %v", err) + } + endKey, err := dao.CreateKey(ctx, endUser, "end user key") + if err != nil { + t.Fatalf("CreateKey: %v", err) + } + + // Attach a recognizable policy document directly through the DAO; the + // admin client path is covered by TestIntegration_PolicyEnforcement. + policy, err := dao.CreatePolicy(ctx, "downstream-grant", []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_ALLOW, + Methods: []string{"example.v1.Things/*"}, + }}) + if err != nil { + t.Fatalf("CreatePolicy: %v", err) + } + if err := dao.AttachPolicy(ctx, policy.GetId(), endUser.UUID); err != nil { + t.Fatalf("AttachPolicy: %v", err) + } + + verifier := iamsts.New(iamsts.Config{ + BaseURL: iamd.URL, + HTTPClient: &http.Client{Transport: signedTransport(t, verifierAKID, verifierSecret)}, + Region: intRegion, + Service: intService, + MaxBodySize: 1 << 20, + }) + + var gotPolicies, gotAdmin = -1, true + app := httptest.NewServer(verifier.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if ps, ok := authctx.PrincipalPolicies(r.Context()); ok { + gotPolicies = len(ps) + } + if c, ok := iamsts.Caller(r.Context()); ok { + gotAdmin = c.IsAdmin + } + w.WriteHeader(http.StatusNoContent) + }))) + defer app.Close() + + endUserClient := &http.Client{Transport: classicSignedTransport(t, endKey.AccessKeyID, endKey.SecretAccessKey)} + resp, err := endUserClient.Get(app.URL + "/thing") + if err != nil { + t.Fatalf("GET: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusNoContent { + t.Fatalf("status = %d, want 204", resp.StatusCode) + } + if gotPolicies != 1 { + t.Errorf("downstream saw %d policies, want 1", gotPolicies) + } + if gotAdmin { + t.Error("end user reported as admin downstream") + } +} + +func isPermissionDenied(err error) bool { + te, ok := err.(twirp.Error) + return ok && te.Code() == twirp.PermissionDenied +} diff --git a/cmd/iamd/pub/iam/iamclient.go b/cmd/iamd/pub/iam/iamclient.go index 3fc51d5f..77366021 100644 --- a/cmd/iamd/pub/iam/iamclient.go +++ b/cmd/iamd/pub/iam/iamclient.go @@ -10,8 +10,9 @@ import ( ) type Client struct { - Keys iamv1.KeyService - Users iamv1.UserService + Keys iamv1.KeyService + Users iamv1.UserService + Policies iamv1.PolicyService } func New(ctx context.Context, endpoint, region, accessKeyID, secretAccessKey string) (*Client, error) { @@ -33,9 +34,11 @@ func New(ctx context.Context, endpoint, region, accessKeyID, secretAccessKey str keys := iamv1.NewKeyServiceProtobufClient(endpoint, hc) users := iamv1.NewUserServiceProtobufClient(endpoint, hc) + policies := iamv1.NewPolicyServiceProtobufClient(endpoint, hc) return &Client{ - Keys: keys, - Users: users, + Keys: keys, + Users: users, + Policies: policies, }, nil } diff --git a/cmd/iamd/services/iam/policies/policies.go b/cmd/iamd/services/iam/policies/policies.go new file mode 100644 index 00000000..78a26681 --- /dev/null +++ b/cmd/iamd/services/iam/policies/policies.go @@ -0,0 +1,255 @@ +package policies + +import ( + "context" + "errors" + "log/slog" + + "buf.build/go/protovalidate" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/twitchtv/twirp" + "gorm.io/gorm" + "within.website/x/cmd/iamd/models" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/iam/authz" + "within.website/x/web/middleware/authctx" +) + +type Server struct { + dao *models.DAO + + iamv1.UnimplementedPolicyServiceServer +} + +var ( + policiesCreated = promauto.NewCounter(prometheus.CounterOpts{ + Name: "iam_policy_documents_created_total", + Help: "Number of policy documents created.", + }) + + policiesDeleted = promauto.NewCounter(prometheus.CounterOpts{ + Name: "iam_policy_documents_deleted_total", + Help: "Number of policy documents deleted.", + }) + + policiesAttached = promauto.NewCounter(prometheus.CounterOpts{ + Name: "iam_policy_attachments_created_total", + Help: "Number of user-policy attachments created.", + }) + + policiesDetached = promauto.NewCounter(prometheus.CounterOpts{ + Name: "iam_policy_attachments_removed_total", + Help: "Number of user-policy attachments removed.", + }) + + policyErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "iam_policy_service_errors_total", + Help: "Errors in policy service calls, by call and failing step.", + }, []string{"call", "step"}) +) + +func New(dao *models.DAO) *Server { + return &Server{dao: dao} +} + +// adminCaller resolves the authenticated caller and requires is_admin. +// Policy administration is gated by the admin flag, never by policies, so +// no policy document can lock an admin out of the management surface. +func adminCaller(ctx context.Context) (*iamv1.User, error) { + u, ok := authctx.User(ctx) + if !ok { + return nil, twirp.NewError(twirp.Unauthenticated, "no verified caller in context") + } + if !u.GetIsAdmin() { + return nil, twirp.NewError(twirp.PermissionDenied, "policy administration requires an admin caller") + } + return u, nil +} + +func (s *Server) CreatePolicy(ctx context.Context, req *iamv1.CreatePolicyReq) (*iamv1.CreatePolicyResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + doc := req.GetPolicy() + if doc == nil || doc.GetName() == "" { + return nil, twirp.InvalidArgumentError("policy.name", "must be set") + } + if len(doc.GetStatements()) == 0 { + return nil, twirp.InvalidArgumentError("policy.statements", "at least one statement is required") + } + for _, st := range doc.GetStatements() { + if st.GetEffect() == iamv1.Effect_EFFECT_UNSPECIFIED { + return nil, twirp.InvalidArgumentError("policy.statements", "statement {effect} must be ALLOW or DENY") + } + if len(st.GetMethods()) == 0 { + return nil, twirp.InvalidArgumentError("policy.statements", "statement must list at least one method pattern") + } + // Syntax-check filters at write time so typos are rejected here + // instead of failing closed at evaluation. Full type-checking needs + // the owning service's request types and still happens there. + if err := authz.TryCompile(st.GetFilter()); err != nil { + return nil, twirp.InvalidArgumentError("policy.statements", err.Error()) + } + } + + p, err := s.dao.CreatePolicy(ctx, doc.GetName(), doc.GetStatements()) + if err != nil { + slog.ErrorContext(ctx, "can't create policy", "err", err) + policyErrors.WithLabelValues("CreatePolicy", "create").Inc() + switch { + case errors.Is(err, models.ErrDuplicatePolicyName): + return nil, twirp.NewError(twirp.AlreadyExists, "policy name already in use") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + policiesCreated.Inc() + + return &iamv1.CreatePolicyResp{Policy: p}, nil +} + +func (s *Server) GetPolicy(ctx context.Context, req *iamv1.GetPolicyReq) (*iamv1.GetPolicyResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + p, err := s.dao.GetPolicy(ctx, req.GetPolicyId()) + if err != nil { + slog.ErrorContext(ctx, "can't get policy", "err", err) + policyErrors.WithLabelValues("GetPolicy", "select").Inc() + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, twirp.NotFoundError("policy not found") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + return &iamv1.GetPolicyResp{Policy: p}, nil +} + +func (s *Server) ListPolicies(ctx context.Context, req *iamv1.ListPoliciesReq) (*iamv1.ListPoliciesResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + ps, err := s.dao.ListPolicies(ctx, int(req.GetCount()), int(req.GetPage())) + if err != nil { + slog.ErrorContext(ctx, "can't list policies", "err", err) + policyErrors.WithLabelValues("ListPolicies", "select").Inc() + return nil, twirp.InternalErrorWith(err) + } + + return &iamv1.ListPoliciesResp{Policies: ps}, nil +} + +func (s *Server) DeletePolicy(ctx context.Context, req *iamv1.DeletePolicyReq) (*iamv1.DeletePolicyResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + if err := s.dao.DeletePolicy(ctx, req.GetPolicyId()); err != nil { + slog.ErrorContext(ctx, "can't delete policy", "err", err) + policyErrors.WithLabelValues("DeletePolicy", "delete").Inc() + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, twirp.NotFoundError("policy not found") + case errors.Is(err, models.ErrPolicyAttached): + return nil, twirp.NewError(twirp.FailedPrecondition, "policy is still attached to at least one user") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + policiesDeleted.Inc() + + return &iamv1.DeletePolicyResp{}, nil +} + +func (s *Server) AttachPolicy(ctx context.Context, req *iamv1.AttachPolicyReq) (*iamv1.AttachPolicyResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + if err := s.dao.AttachPolicy(ctx, req.GetPolicyId(), req.GetUserId()); err != nil { + slog.ErrorContext(ctx, "can't attach policy", "err", err) + policyErrors.WithLabelValues("AttachPolicy", "attach").Inc() + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, twirp.NotFoundError("policy or user not found") + case errors.Is(err, models.ErrAlreadyAttached): + return nil, twirp.NewError(twirp.AlreadyExists, "policy already attached to user") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + policiesAttached.Inc() + + return &iamv1.AttachPolicyResp{}, nil +} + +func (s *Server) DetachPolicy(ctx context.Context, req *iamv1.DetachPolicyReq) (*iamv1.DetachPolicyResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + if err := s.dao.DetachPolicy(ctx, req.GetPolicyId(), req.GetUserId()); err != nil { + slog.ErrorContext(ctx, "can't detach policy", "err", err) + policyErrors.WithLabelValues("DetachPolicy", "detach").Inc() + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, twirp.NotFoundError("no such attachment") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + policiesDetached.Inc() + + return &iamv1.DetachPolicyResp{}, nil +} + +func (s *Server) ListPoliciesForUser(ctx context.Context, req *iamv1.ListPoliciesForUserReq) (*iamv1.ListPoliciesForUserResp, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, twirp.NewError(twirp.InvalidArgument, err.Error()) + } + if _, err := adminCaller(ctx); err != nil { + return nil, err + } + + ps, err := s.dao.PoliciesForUser(ctx, req.GetUserId()) + if err != nil { + slog.ErrorContext(ctx, "can't list policies for user", "err", err) + policyErrors.WithLabelValues("ListPoliciesForUser", "select").Inc() + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return nil, twirp.NotFoundError("user not found") + default: + return nil, twirp.InternalErrorWith(err) + } + } + + return &iamv1.ListPoliciesForUserResp{Policies: ps}, nil +} diff --git a/cmd/iamd/services/iam/sts/policy_metadata_test.go b/cmd/iamd/services/iam/sts/policy_metadata_test.go new file mode 100644 index 00000000..2eaf367b --- /dev/null +++ b/cmd/iamd/services/iam/sts/policy_metadata_test.go @@ -0,0 +1,77 @@ +package sts + +import ( + "context" + "testing" + + "google.golang.org/protobuf/proto" + "within.website/x/cmd/iamd/models" + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +func TestPolicyMetadata(t *testing.T) { + for _, tt := range []struct { + name string + admin bool + }{ + {name: "policy"}, + {name: "admin", admin: true}, + } { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + s, dao, akid, _ := newSigningKeysTest(t) + key, err := dao.GetKeyWithUser(ctx, akid) + if err != nil { + t.Fatal(err) + } + p, err := dao.CreatePolicy(ctx, "downstream-access", []*iamv1.PolicyStatement{{ + Effect: iamv1.Effect_EFFECT_ALLOW, Methods: []string{"example.Things/Get"}, + Filter: "req.id == principal.principal_id", + }}) + if err != nil { + t.Fatal(err) + } + if err := dao.AttachPolicy(ctx, p.GetId(), key.User.UUID); err != nil { + t.Fatal(err) + } + if tt.admin { + if err := dao.DB().Model(&models.User{}).Where("uuid = ?", key.User.UUID).Update("is_admin", true).Error; err != nil { + t.Fatal(err) + } + // Admin responses must not load irrelevant policy documents. + if err := dao.DB().Model(&models.Policy{}).Where("uuid = ?", p.GetId()).Update("statements", "invalid JSON").Error; err != nil { + t.Fatal(err) + } + } + + check := func(t *testing.T, id *stsv1.TokenIdentity, ps []*iamv1.Policy) { + t.Helper() + if id.GetPrincipalId() != key.User.UUID || id.GetIsAdmin() != tt.admin { + t.Fatalf("identity = %v, want principal %s and admin %v", id, key.User.UUID, tt.admin) + } + if tt.admin { + if len(ps) != 0 { + t.Fatalf("admin policies = %v, want none", ps) + } + } else if len(ps) != 1 || !proto.Equal(ps[0], p) { + t.Fatalf("policies = %v, want [%v]", ps, p) + } + } + t.Run("SigV4", func(t *testing.T) { + resp, err := s.GetSigningKey(ctx, validReq(akid)) + if err != nil { + t.Fatal(err) + } + check(t, resp.GetIdentity(), resp.GetPolicies()) + }) + t.Run("SigV4A", func(t *testing.T) { + resp, err := s.GetPublicKey(ctx, &stsv1.GetPublicKeyRequest{AccessKeyId: akid}) + if err != nil { + t.Fatal(err) + } + check(t, resp.GetIdentity(), resp.GetPolicies()) + }) + }) + } +} diff --git a/cmd/iamd/services/iam/sts/publickey.go b/cmd/iamd/services/iam/sts/publickey.go index 19a5d2c0..0634c3e7 100644 --- a/cmd/iamd/services/iam/sts/publickey.go +++ b/cmd/iamd/services/iam/sts/publickey.go @@ -49,13 +49,15 @@ func (s *SigningKeys) GetPublicKey(ctx context.Context, req *stsv1.GetPublicKeyR return nil, twirp.InternalErrorWith(err) } + id, policies, err := s.identityAndPolicies(ctx, k) + if err != nil { + return nil, twirp.InternalErrorf("can't load identity policies") + } + return &stsv1.GetPublicKeyResponse{ - PublicKey: der, - Identity: &stsv1.TokenIdentity{ - AccessKeyId: k.AccessKeyID, - PrincipalId: k.User.UUID, - DisplayName: k.User.Name, - }, + PublicKey: der, + Identity: id, + Policies: policies, CacheUntil: timestamppb.New(s.now().Add(s.cacheTTL)), }, nil } diff --git a/cmd/iamd/services/iam/sts/signingkey.go b/cmd/iamd/services/iam/sts/signingkey.go index 62f5c3d0..e5bb9c5c 100644 --- a/cmd/iamd/services/iam/sts/signingkey.go +++ b/cmd/iamd/services/iam/sts/signingkey.go @@ -15,6 +15,7 @@ import ( "within.website/x/cmd/iamd/models" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" "within.website/x/web/middleware/sigv4" ) @@ -96,18 +97,41 @@ func (s *SigningKeys) GetSigningKey(ctx context.Context, req *stsv1.GetSigningKe cacheUntil = notValidAfter } + id, policies, err := s.identityAndPolicies(ctx, k) + if err != nil { + return nil, twirp.InternalErrorf("can't load identity policies") + } + return &stsv1.GetSigningKeyResponse{ - SigningKey: sigv4.DeriveSigningKey(k.SecretAccessKey, req.GetDate(), req.GetRegion(), req.GetService()), - Identity: &stsv1.TokenIdentity{ - AccessKeyId: k.AccessKeyID, - PrincipalId: k.User.UUID, - DisplayName: k.User.Name, - }, + SigningKey: sigv4.DeriveSigningKey(k.SecretAccessKey, req.GetDate(), req.GetRegion(), req.GetService()), + Identity: id, + Policies: policies, NotValidAfter: timestamppb.New(notValidAfter), CacheUntil: timestamppb.New(cacheUntil), }, nil } +// identityAndPolicies builds the TokenIdentity for the key's owning user and +// that user's effective policy set. Admin identities carry an empty policy +// list: they bypass evaluation, so there is nothing for a verifier to fetch. +// The key is pre-checked (user present, not disabled) by both callers. +func (s *SigningKeys) identityAndPolicies(ctx context.Context, k *models.Key) (*stsv1.TokenIdentity, []*iamv1.Policy, error) { + id := &stsv1.TokenIdentity{ + AccessKeyId: k.AccessKeyID, + PrincipalId: k.User.UUID, + DisplayName: k.User.Name, + IsAdmin: k.User.IsAdmin, + } + if k.User.IsAdmin { + return id, nil, nil + } + ps, err := s.dao.PoliciesForUser(ctx, k.User.UUID) + if err != nil { + return nil, nil, err + } + return id, ps, nil +} + func (s *SigningKeys) now() time.Time { if s.Now != nil { return s.Now() diff --git a/cmd/sakurajima/internal/logging/expressions/environment.go b/cmd/sakurajima/internal/logging/expressions/environment.go index a2aae2d9..8c19a2ea 100644 --- a/cmd/sakurajima/internal/logging/expressions/environment.go +++ b/cmd/sakurajima/internal/logging/expressions/environment.go @@ -7,11 +7,11 @@ import ( "math/rand/v2" "time" + "cel.dev/cel-go/cel" + "cel.dev/cel-go/common/types" + "cel.dev/cel-go/common/types/ref" + "cel.dev/cel-go/ext" "github.com/golang/protobuf/ptypes/timestamp" - "github.com/google/cel-go/cel" - "github.com/google/cel-go/common/types" - "github.com/google/cel-go/common/types/ref" - "github.com/google/cel-go/ext" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) diff --git a/docs/README.md b/docs/README.md index b066efc3..0950335f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,10 @@ # x +## Service authorization + +- [Integrating a service with iamd policies](iam-service-integration.md) — service credentials, signed clients, Twirp authentication and policy enforcement, and resource-ownership boundaries. +- [IAM policies](iam-policies.md) — policy semantics, CLI management, and built-in templates. + ## Commands - almarid - a mastodon bot mocking IRC spambots. diff --git a/docs/iam-policies.md b/docs/iam-policies.md new file mode 100644 index 00000000..b5e98bcb --- /dev/null +++ b/docs/iam-policies.md @@ -0,0 +1,284 @@ +# IAM policies (iamd) + +iamd authorizes RPC calls with AWS-IAM-style policy documents whose +conditions are CEL filters evaluated over the method name and the decoded +request message. This page documents the model, the evaluation semantics, +and how downstream services enforce the same policies locally. + +- Proto: `pb/within/website/x/iam/v1/policy.proto` +- Evaluator: `within.website/x/iam/authz` +- Management API: `iamd` `PolicyService` (admin-only) +- Storage: `cmd/iamd/models/policies.go` (SQLite via gorm) + +## Model + +A **policy** is a named document (UUIDv7 id, unique name) holding a list of +**statements**: + +| Field | Meaning | +| --------- | --------------------------------------------------------------------- | +| `effect` | `EFFECT_ALLOW` or `EFFECT_DENY` | +| `methods` | anchored glob patterns over full method names (below) | +| `filter` | optional CEL expression over `(method, req, principal)`; empty = true | + +Method names are `./`, e.g. +`within.website.x.iam.v1.KeyService/ListKeys`. Patterns match the whole +string; `*` spans any characters including `/`, `?` matches one character: + +``` +* # every method +within.website.x.iam.v1.KeyService/* # one whole service +within.website.x.iam.v1.*/List* # listers across services +``` + +Filters bind three names: + +- `method` — string, the full method name +- `req` — the decoded request proto for that method (typed at the enforcing service) +- `principal` — `within.website.x.iam.sts.v1.TokenIdentity`; `principal.principal_id` is the user UUID + +Examples: + +```cel +"" // no condition +req.user_id == principal.principal_id // own resources only +req.user_id == "" || req.user_id == principal.principal_id +req.comment.startsWith("ci-") +``` + +## Evaluation + +Per call, over every policy attached to the caller, in order: + +1. An admin caller (`is_admin`) bypasses everything: allow. +2. A statement **applies** when a `methods` pattern matches and its filter + is true. +3. Any applying `DENY` denies the call — explicit deny wins. +4. Otherwise any applying `ALLOW` allows the call. +5. Otherwise the call is implicitly denied. + +Filter errors fail closed. These errors include compilation failures against +the method's request type and evaluation failures. A failed `ALLOW` filter +grants nothing. A failed `DENY` filter denies the matching method. + +Failures increment `iam_authz_filter_errors_total`. If filter errors cause +a denial, the denial reason includes the cause. Another valid `ALLOW` can +still grant access after a failed `ALLOW` filter. + +Response scoping (e.g. non-admin `ListKeys` being pinned to the caller's own +user) is deliberately **not** policy machinery; it stays in the handlers, +which already clamp requests before the DAO. + +## Management + +`PolicyService` (CreatePolicy, GetPolicy, ListPolicies, DeletePolicy, +AttachPolicy, DetachPolicy, ListPoliciesForUser) is gated on `is_admin` +inside its handlers — never on policies — so no document can lock an admin +out of the management surface. `CreatePolicy` checks every filter with +`authz.TryCompile`. This check requires a boolean result and checks the +concrete principal type. Request fields remain dynamic until enforcement, +where the evaluator uses the owning service's request types. + +Policies are immutable after creation: +edit = create new + reattach + delete old, so an enforcing service's fetched +snapshot always compiles the same. `DeletePolicy` refuses while attachments +exist (`FAILED_PRECONDITION`). + +`iamd`'s bootstrap user is an admin and bypasses all checks. + +### CLI and policy files + +Use `iam policies` to manage documents through the signed API. Remote +commands use the existing `iam --config ` configuration and require +admin credentials. `validate` runs offline and does not load credentials or +contact iamd. + +Files contain a bare `Policy` document in protobuf JSON, not a +`{"policy": ...}` RPC request wrapper. For example, `own-keys.json`: + +```json +{ + "name": "own-keys", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": ["within.website.x.iam.v1.KeyService/ListKeys"], + "filter": "req.user_id == '' || req.user_id == principal.principal_id" + } + ] +} +``` + +Validate and upload the document, then attach it to a user: + +```sh +iam policies validate --file own-keys.json +POLICY_ID=$(iam policies create --file own-keys.json) +iam policies attach "$POLICY_ID" --user-id +``` + +`create` prints only the assigned policy ID on stdout. Both `validate` and +`create` accept `--file -` to read stdin; both reject unknown/duplicate JSON +fields, invalid policy field values, and CEL compilation errors before any +upload. Request-specific field/type checks still happen at enforcement: +offline validation cannot prove a filter is valid for every matching RPC. + +```sh +iam policies validate --file - < own-keys.json +iam policies create --file - < another-policy.json + +iam policies get "$POLICY_ID" +iam policies get "$POLICY_ID" --json > exported-policy.json +iam policies validate --file exported-policy.json + +iam policies list +iam policies list --count 20 --page 0 --json +iam policies list --user-id --json + +iam policies detach "$POLICY_ID" --user-id +iam policies delete "$POLICY_ID" +``` + +`get --json` exports a bare document accepted by the file loader, including +server-assigned ID and timestamps. `create` always assigns fresh metadata; +to create a copy of an existing document, give it a different name first. +Names are unique, so importing the same document does not update it. +`list --json` returns a `{"policies": [...]}` list, not a single importable +document. Global lists use zero-based pages (default count 40, page 0). +`list --user-id` returns all attachments for that user and cannot be combined +with pagination flags. + +Documents remain immutable. To replace one, create a differently named +policy, explicitly change attachments, and delete the old document once it +is unattached. Attaching the new document before detaching the old avoids +a gap in grants, but both policies apply during that overlap and any deny +still wins. Choose that order deliberately. Local file edits have no effect +until uploaded; there is no `apply`, file watcher, or automatic replacement. + +### Checked-in examples + +| File | Grant | +| -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [`own-keys.json`](../cmd/iam/examples/own-keys.json) | List and create keys for the caller's own user. | +| [`users-read-only.json`](../cmd/iam/examples/users-read-only.json) | List users with page size 1–50 and a non-negative page; no user creation or disabling. | +| [`sts-sigv4a-verifier.json`](../cmd/iam/examples/sts-sigv4a-verifier.json) | Fetch public verification keys and their associated identity/policy metadata; no symmetric signing keys. | +| [`sts-sigv4-verifier.json`](../cmd/iam/examples/sts-sigv4-verifier.json) | Fetch symmetric derived signing keys only for region `yow`, service `iam`. | + +The CLI bundles these files as offline templates, so an installed binary +does not need a source checkout or credentials to render them: + +```sh +iam policies templates list +iam policies templates render own-keys --name alice-keys > alice-keys.json +iam policies validate --file alice-keys.json + +POLICY_ID=$(iam policies templates render sts-sigv4a-verifier --name backend-verifier | + iam policies create --file -) +iam policies attach "$POLICY_ID" --user-id +``` + +`render` writes only a bare protobuf-JSON policy to stdout; it does not +create, attach, or replace anything on the server. `--name` overrides the +document name on any template. Otherwise, the example's name is retained. +Use a unique name when creating another document. + +For the classic SigV4 template, override the lookup scope with both +`--region` and `--service`: + +```sh +iam policies templates render sts-sigv4-verifier \ + --region us-east-1 --service mi --name mi-verifier > mi-verifier.json +``` + +Both scope flags must be supplied together and apply only to +`sts-sigv4-verifier`. Without them its example scope remains `yow`/`iam`. +With them the default document name becomes +`sts-sigv4-verifier--`, unless `--name` overrides it. +Scope values are quoted as CEL string literals; they cannot inject new +conditions into the filter. Changing scope does not wire the named +application to IAM enforcement. Classic signing-key access still carries +the forgery capability described below. + +The JSON examples remain the source of the bundled templates. Rebuild the +CLI after changing them; templates never read an editable runtime directory. + +For example, to give a non-admin backend credential permission to fetch +SigV4A verification keys: + +```sh +iam policies validate --file cmd/iam/examples/sts-sigv4a-verifier.json +POLICY_ID=$(iam policies create --file cmd/iam/examples/sts-sigv4a-verifier.json) +iam policies attach "$POLICY_ID" --user-id +``` + +Attach verifier policies to the backend's own IAM user, not to every end +user it authenticates. Lookup is intentionally not restricted to the +backend's own access key: verifiers need to look up incoming callers' keys. +Both lookup APIs also expose those callers' identity and attached policies, +so only grant lookup access to trusted backends. + +Prefer the SigV4A example: public keys permit signature verification, not +forgery. The classic SigV4 example grants **forgery-capable symmetric key +material** for any enabled credential within the allowed scope; use it only +for a backend you trust with that capability. Its `yow` region matches +`kube/alrest/x/iamd.yaml`, and `iam` matches iamd's default service name. +Before upload, set the condition to the region and service that the backend +verifies. These fields describe the requested key scope. The backend uses +its own credentials to authenticate the lookup RPC to iamd. + +These files grant only the listed operations; they do not revoke permissions +from other attached policies, and admin users bypass them entirely. +`PolicyService` requires `is_admin` in its handlers, so a policy document +cannot delegate policy administration to a non-admin. + +Currently only iamd's RPC routes are wired to IAM policy enforcement in +this repository. Other applications such as `mi` and `xedn` do not yet +use that enforcement; the mi IAM migration document is a plan, not live +wiring. Attaching a document does not protect an application's routes until +that application installs the verifier and policy evaluator. + +## Enforcement + +### In iamd + +A Twirp interceptor (`cmd/iamd/authz.go`) runs inside every mounted service +after the dual SigV4/SigV4A middleware authenticates the caller: + +``` +signature verify → resolve user → policy interceptor → handler +``` + +The interceptor loads the caller's attached policies per call, so attach and +detach take effect on the next request. Denials are `permission_denied` +with the deciding rule in the message. + +### Downstream (distributed) + +`SigningKeyService.GetSigningKey`/`GetPublicKey` responses carry the +caller's policy set and the identity's `is_admin` flag. The iamsts verifiers +(`web/middleware/sigv4/iamsts`, `web/middleware/sigv4a/iamsts`) cache both +alongside the key material for the server-advised TTL and store them in the +request context (`authctx.PrincipalPolicies`, `authctx.Caller().IsAdmin`). +Downstream services then enforce locally with the same semantics — no IAM +RPC on the hot path. + +For complete server/client wiring, backend lookup credentials, caller +policies, and handler responsibilities, see +[Integrating a service with iamd policies](iam-service-integration.md). + +Revocation/attachment latency is bounded by the signing-key cache TTL +(`-signing-key-cache-ttl`, default 5m). + +## Metrics + +- `iam_authz_decisions_total{decision}` — allow_admin / allow_policy / + deny_explicit / deny_implicit / deny_filter_error +- `iam_authz_filter_seconds` — per-filter evaluation time +- `iam_authz_filter_errors_total` — compile/eval failures (fail-closed) + +## Known limitations + +- Filters require boolean results and valid principal fields at write time. + Errors in request fields and types cause fail-closed decisions at enforcement. +- Policy documents are immutable; there is no in-place edit RPC. +- No groups or policy versioning yet; policies attach to users directly. diff --git a/docs/iam-service-integration.md b/docs/iam-service-integration.md new file mode 100644 index 00000000..ae751891 --- /dev/null +++ b/docs/iam-service-integration.md @@ -0,0 +1,403 @@ +# Integrating a service with iamd policies + +A service needs both **authentication** and **authorization**. Signature +verification identifies the caller; the policy evaluator decides whether +that caller can invoke a particular RPC with its decoded protobuf request. +Installing only the signature middleware does not enforce policies. + +This guide uses SigV4A and Twirp. The same authorization evaluator can be +called explicitly from another transport after authentication and decoding. +See [IAM policies](iam-policies.md) for document syntax and management. + +## Request path + +```mermaid +flowchart LR + C[Signed client request] --> V[SigV4A verifier middleware] + V --> T[Twirp decodes protobuf request] + T --> A[Policy authorization interceptor] + A -->|Allow| H[Service handler] + A -->|Deny| D[permission_denied] +``` + +The verifier fetches the caller's public key, identity, and attached policies +from iamd on a cache miss. It caches that response until iamd's +`cache_until`. Authorization then evaluates those policies locally; there +is no per-action authorization RPC to iamd on a warm cache. + +## 1. Define your service's protobuf API + +The examples below use this illustrative `widgets.proto`: + +```proto +syntax = "proto3"; +package example.widgets.v1; + +option go_package = "example.com/widgets/gen/widgetsv1"; + +service WidgetService { + rpc CreateWidget(CreateWidgetReq) returns (CreateWidgetResp); + rpc ListWidgets(ListWidgetsReq) returns (ListWidgetsResp); +} + +message Widget { + string id = 1; + string owner_id = 2; + string name = 3; +} + +message CreateWidgetReq { + string owner_id = 1; + string name = 2; +} + +message CreateWidgetResp { + Widget widget = 1; +} + +message ListWidgetsReq { + string owner_id = 1; +} + +message ListWidgetsResp { + repeated Widget widgets = 1; +} +``` + +Generate Go protobuf, Twirp, and gRPC bindings using your project's protobuf +tooling. The code below assumes the generated package is +`example.com/widgets/gen/widgetsv1`; the gRPC binding supplies the full +service-name descriptor, but this guide does not mount a gRPC listener. +Replace that import and the generated types with your own API's bindings. + +No protobuf schema registration in iamd is required. The enforcing service +knows its request types and compiles the CEL filters against them. iamd +checks policy fields, CEL boolean results, and the concrete principal type +before it stores a document. Request fields remain dynamic during this +check because iamd does not know every service's schemas. + +## 2. Provision a backend credential + +Use separate, non-admin IAM users for the backend and its callers: + +| Credential | Used for | Required grant | +| --------------------------- | ---------------------------------------------- | ---------------------------------- | +| Backend/verifier credential | The service's `GetPublicKey` requests to iamd | `sts-sigv4a-verifier` policy | +| Caller credential | The end user's signed requests to your service | A policy granting your RPC methods | + +Create those users using an administrator's IAM CLI configuration: + +```sh +iam --config admin.hcl users create --name widgets-verifier +iam --config admin.hcl users create --name alice +``` + +Save each returned secret: it is issued only once. Load the backend secret +from your service's secret-management mechanism, and do not log it. Replace +`` below with the backend user's returned UUID: + +```sh +VERIFIER_POLICY_ID=$(iam policies templates render sts-sigv4a-verifier --name widgets-verifier-lookup | + iam --config admin.hcl policies create --file -) +iam --config admin.hcl policies attach "$VERIFIER_POLICY_ID" --user-id +``` + +That policy lets the backend fetch incoming callers' public keys and their +identity/policy metadata. It does not grant end users any widget operations. +Grant lookup access only to trusted backends. Do not use an admin credential +as an end-user credential: `is_admin` bypasses policy evaluation, including +explicit denies, in downstream services too. + +## 3. Install authentication and authorization + +The following server-wiring file accepts your existing `WidgetService` +implementation as `svc`: + +```go +package widgets + +import ( + "context" + "net/http" + "time" + + "github.com/twitchtv/twirp" + widgetsv1 "example.com/widgets/gen/widgetsv1" + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + "within.website/x/iam/authz" + "within.website/x/web/middleware/authctx" + "within.website/x/web/middleware/sigv4a/iamsts" + "within.website/x/web/middleware/sigv4a/sigv4aclient" +) + +type Config struct { + IAMURL string + IAMRegion string + IAMService string + AppRegion string + BackendAccessKeyID string + BackendSecretAccessKey string +} + +func NewMux(cfg Config, svc widgetsv1.WidgetService) (*http.ServeMux, error) { + transport, err := sigv4aclient.NewSigV4ARoundTripper( + &sigv4aclient.Config{ + Region: cfg.IAMRegion, + ServiceName: cfg.IAMService, + AccessKey: cfg.BackendAccessKeyID, + SecretKey: cfg.BackendSecretAccessKey, + }, + nil, + ) + if err != nil { + return nil, err + } + + verifier := iamsts.New(iamsts.Config{ + BaseURL: cfg.IAMURL, + HTTPClient: &http.Client{Transport: transport, Timeout: 10 * time.Second}, + Region: cfg.AppRegion, + Service: "widgets", + MaxBodySize: 1 << 20, + }) + + evaluator, err := authz.New(map[string]interface{}{ + authz.MethodName(widgetsv1.WidgetService_ServiceDesc.ServiceName, "CreateWidget"): &widgetsv1.CreateWidgetReq{}, + authz.MethodName(widgetsv1.WidgetService_ServiceDesc.ServiceName, "ListWidgets"): &widgetsv1.ListWidgetsReq{}, + }) + if err != nil { + return nil, err + } + + names := map[string]string{ + "WidgetService": widgetsv1.WidgetService_ServiceDesc.ServiceName, + } + handler := widgetsv1.NewWidgetServiceServer( + svc, + twirp.WithServerInterceptors( + policyInterceptor(evaluator, names), + ), + ) + + mux := http.NewServeMux() + mux.Handle(widgetsv1.WidgetServicePathPrefix, verifier.Middleware(handler)) + return mux, nil +} + +func policyInterceptor(evaluator *authz.Evaluator, names map[string]string) twirp.Interceptor { + return func(next twirp.Method) twirp.Method { + return func(ctx context.Context, req interface{}) (interface{}, error) { + service, _ := twirp.ServiceName(ctx) + method, _ := twirp.MethodName(ctx) + if err := authorizeCaller(ctx, evaluator, authz.MethodName(names[service], method), req); err != nil { + return nil, err + } + return next(ctx, req) + } + } +} + +func authorizeCaller(ctx context.Context, evaluator *authz.Evaluator, method string, req interface{}) error { + caller, ok := authctx.Caller(ctx) + if !ok || caller == nil { + return twirp.NewError(twirp.Unauthenticated, "no verified caller in context") + } + policies, _ := authctx.PrincipalPolicies(ctx) + principal := &stsv1.TokenIdentity{ + AccessKeyId: caller.AccessKeyID, + OrganizationId: caller.OrganizationID, + PrincipalId: caller.PrincipalID, + DisplayName: caller.DisplayName, + IsAdmin: caller.IsAdmin, + } + decision := evaluator.Check(policies, method, req, principal) + if !decision.Allowed { + return twirp.NewError(twirp.PermissionDenied, decision.Reason) + } + return nil +} +``` + +Set `IAMURL` to your trusted iamd endpoint, using HTTPS outside local +development. The response contains authorization data; its integrity matters +just as much as the integrity of the incoming request. `IAMRegion` and +`IAMService` must match iamd's own `-region` and `-service` settings. The +checked-in deployment uses region `yow`; iamd's default service name is +`iam`. + +`AppRegion` and the `widgets` signing scope belong to your application. +They must match what its clients sign, and need not match iamd's scope. +The signing-scope string `widgets` is distinct from the full policy method +name `example.widgets.v1.WidgetService/CreateWidget`. + +Register every guarded method with its full name and a sample request +message. The evaluator checks request fields against that method's concrete +protobuf type. An unregistered method fails closed for non-admin callers. + +Include every mounted service in `names`. Twirp supplies short names such +as `WidgetService`, while policy patterns match full protobuf service names. + +Share the verifier and evaluator across routes instead of constructing them +for each request. Mount each protected RPC service with both layers. Do not +also expose an unwrapped copy of the same handler. + +## 4. Give the caller an action policy + +Save this as `widgets-user.json`. It permits the caller to create widgets +for itself and request its own listing: + +```json +{ + "name": "own-widgets", + "statements": [ + { + "effect": "EFFECT_ALLOW", + "methods": [ + "example.widgets.v1.WidgetService/CreateWidget", + "example.widgets.v1.WidgetService/ListWidgets" + ], + "filter": "req.owner_id == principal.principal_id" + } + ] +} +``` + +Both request types deliberately have an `owner_id` field, so this filter can +compile for both methods. `principal.principal_id` is the authenticated +caller's IAM user UUID, not a caller-supplied request value. + +Using admin credentials, attach this policy to the caller's user, not the +backend's verifier user: + +```sh +iam policies validate --file widgets-user.json +CALLER_POLICY_ID=$(iam --config admin.hcl policies create --file widgets-user.json) +iam --config admin.hcl policies attach "$CALLER_POLICY_ID" --user-id +``` + +An ordinary caller with no applicable allow is denied. Across attached +policies, an applicable explicit deny overrides an allow. Admin callers +bypass this evaluation. A broken ALLOW filter grants nothing; a broken DENY +filter denies the matching method. Server-side request validation is still +your responsibility, including for admins. + +## 5. Sign client requests for your service + +The caller uses its own credential, not the backend's credential. This +client-wiring file returns the generated Twirp client: + +```go +package widgets + +import ( + "net/http" + "time" + + widgetsv1 "example.com/widgets/gen/widgetsv1" + "within.website/x/web/middleware/sigv4a/sigv4aclient" +) + +func NewClient(endpoint, region, accessKeyID, secretAccessKey string) (widgetsv1.WidgetService, error) { + transport, err := sigv4aclient.NewSigV4ARoundTripper( + &sigv4aclient.Config{ + Region: region, + ServiceName: "widgets", + AccessKey: accessKeyID, + SecretKey: secretAccessKey, + }, + nil, + ) + if err != nil { + return nil, err + } + return widgetsv1.NewWidgetServiceProtobufClient( + endpoint, + &http.Client{Transport: transport, Timeout: 10 * time.Second}, + ), nil +} +``` + +Pass the same region used by the server's `AppRegion`. For this policy, +`CreateWidgetReq.OwnerId` and `ListWidgetsReq.OwnerId` must equal the +caller's UUID. The interceptor returns `permission_denied` before calling +`svc` when the policy does not allow the request. + +## 6. Keep business and resource checks in the handler + +The interceptor gates method invocation; it does not implement your data +model or modify a database query. Your handler must still: + +- Validate the request and enforce business invariants. +- Use the authorized `owner_id` when storing and listing widgets. Allowing + a list RPC does not automatically restrict which rows it returns. +- Resolve resource ownership from trusted storage when the request contains + only a resource ID. A supplied owner field does not prove the referenced + resource actually belongs to that owner. +- Read the verified caller with `authctx.Caller(ctx)` when it needs identity + for query scoping or audit attribution. Do not trust request identity + fields in place of that caller. + +The verifier stores identity through `authctx.Caller` and policies through +`authctx.PrincipalPolicies`. The example's `authorizeCaller` helper reads +both values and calls `Evaluator.Check`. This check preserves the admin +bypass. Without policies, a non-admin caller receives an implicit denial. +The Twirp interceptor calls this helper before the handler. + +## Other transports or explicit checks + +For a custom HTTP dispatcher, Connect interceptor, or gRPC interceptor, +authenticate through the transport's authentication layer. Then decode the +request and call `authorizeCaller` before any side effects. This example +uses the helper from the server example: + +```go +package widgets + +import ( + "context" + + widgetsv1 "example.com/widgets/gen/widgetsv1" + "within.website/x/iam/authz" +) + +func AuthorizeCreateWidget(ctx context.Context, evaluator *authz.Evaluator, req *widgetsv1.CreateWidgetReq) error { + return authorizeCaller(ctx, evaluator, "example.widgets.v1.WidgetService/CreateWidget", req) +} +``` + +A missing verified caller is denied. The Twirp error above is suitable for +Twirp/custom Twirp-style HTTP responses; map denials to your own transport's +permission-denied error for Connect or gRPC. There is currently no supplied +Connect/gRPC authorization adapter. The HTTP SigV4A middleware is not a +native gRPC authentication interceptor. + +Do not expose another listener or route that reaches the same operations +without equivalent authentication and authorization. Direct in-process +calls to `svc` bypass the Twirp interceptor too. Attachments in iamd alone +do not secure a service that has not installed enforcement. + +## Cache behavior and verification checklist + +Downstream grants, detaches, disabled credentials, and admin-state changes +can remain stale until the server-advised key-cache TTL expires. Plan for +that revocation delay. Warm cached entries work during an iamd outage. +Failed requests for uncached or expired keys fail closed. + +Signature verification does not prevent replay within the accepted timestamp +window. If your application requires replay protection, add an idempotency +or replay mechanism. + +Before exposing the service, exercise it with non-admin credentials: + +1. An unsigned request does not reach the handler. +2. A signed caller with no grant receives `permission_denied`. +3. The caller policy permits creating and listing widgets for its own UUID. +4. Requests naming another owner are denied before reaching the handler. +5. A matching DENY overrides an ALLOW from another attached policy. +6. Detaching the grant takes effect after the cached policy response expires. +7. An ungranted method and every alternative transport remain protected. + +The shared entrypoints are +[`authz.Evaluator.Check`](../iam/authz/authz.go) and +[`sigv4a/iamsts.Verifier`](../web/middleware/sigv4a/iamsts/iamsts.go). +The interceptor and context helper in this guide belong to your service. diff --git a/docs/reviews/sigv4a-implementation.md b/docs/reviews/sigv4a-implementation.md new file mode 100644 index 00000000..1820bb25 --- /dev/null +++ b/docs/reviews/sigv4a-implementation.md @@ -0,0 +1,140 @@ +# SigV4A Middleware Implementation Review + +## Overall Verdict + +The AWS Signature Version 4A (SigV4A) middleware implementation in `web/middleware/sigv4a/` is **correct, secure, and well-tested**. No bugs, security gaps, or spec deviations were found. The signer and verifier agree by construction through the shared `web/middleware/internal/awssig` canonicalization package. All tests pass, including with `-race` and `go vet` clean. + +## Review Scope + +The review covered all files under `web/middleware/sigv4a/`: + +| File | Purpose | +| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | +| `sigv4a.go` | Verifier, Middleware, Verify, verify, canonicalRequest, parseAuthHeader, ParseCredential, error mapping | +| `sign.go` | Signer, Sign, sign (client-side signing) | +| `keyderivation.go` | DeriveKeyPair (ECDSA P-256 keypair derivation) | +| `lookuper.go` | Lookuper, PublicKeyLookuper interfaces | +| `regionset.go` | regionSetMatches (X-Amz-Region-Set wildcard matching) | +| `context.go` | authctx helpers (KeyID/User/WithUser/WithCaller) | +| `iamsts/iamsts.go` | Cached public-key verifier with singleflight | +| `sigv4aclient/sigv4aclient.go` | Signing RoundTripper | +| `web/middleware/internal/awssig/` | Shared canonicalization + payload hash | +| Tests: `sigv4a_test.go`, `keyderivation_test.go`, `iamsts/iamsts_test.go`, `iamsts/e2e_test.go`, `sigv4aclient/sigv4aclient_test.go`, `testdata/v4a/*` | Test coverage | + +## Findings by Area + +### 1. Key derivation — CORRECT + +**`web/middleware/sigv4a/keyderivation.go`** faithfully implements the aws-c-auth SigV4A KDF (verified line-for-line against `source/key_derivation.c`): + +- Input layout: `BE32(1) || "AWS4-ECDSA-P256-SHA256" || 0x00 || accessKeyID || counterByte || BE32(256)` (lines 48-53) — exact match. +- HMAC key `"AWS4A" + secret`, PRF = HMAC-SHA256 via `awssig.HMACSHA256` (line 43, `awssig.go:34-38`) — exact. +- Counter range 1..254 with rejection sampling against `N-2`; accepted candidate `+1` landing in `[1, N-1]` (lines 44, 56-59) — exact. +- Scalar → P-256 keypair via `ecdh.P256().NewPrivateKey` (validates scalar ∈ [1, N−1], derives point) (line 64) — exact. +- Uncompressed SEC1 public bytes split `X = pub[1:33]`, `Y = pub[33:65]` (lines 68-74) — exact. + +**Tests:** `TestDeriveKeyPair_FixedVector` (fixed KDF vector from aws-c-auth) and `TestDeriveKeyPair_Vectors` (all copied aws-c-auth vector public keys) pass. + +**Nits (not bugs):** + +- Comment line 30 states per-attempt rejection probability is ~2⁻³²; for P-256 it is actually ~2⁻¹⁰¹. Harmless documentation error. +- Non-constant-time `big.Int` comparison (line 56) is explicitly justified (lines 39-41): derivation runs only over stored secrets, never attacker-controlled input. + +### 2. Signing — CORRECT + +**`web/middleware/sigv4a/sign.go`:** + +- Signed headers: `host`, `x-amz-content-sha256`, `x-amz-date`, `x-amz-region-set` (line 53). +- Canonical request over sorted signed headers via `awssig.BuildCanonicalRequest` (lines 72-75). +- Scope = `date/service/aws4_request` (no region) (line 78). +- `stringToSign = algorithm\namzDate\nscope\nhashed(canonicalRequest)` (lines 80-85). +- `ECDSA-SignASN1` over `sha256(stringToSign)` (line 88). +- `Sign` does not touch `r.Body`; `sign` is split from `Sign` so tests can reproduce AWS vectors that omit `x-amz-content-sha256`. + +**Interop:** The verifier uses the same `awssig.BuildCanonicalRequest` with `disablePathEscaping=false` (non-S3 within.website services), so signer↔verifier agree by construction. End-to-end interop validated by `TestSigner_Vectors`, `TestRoundTrip_SignVerify`, and `iamsts/e2e_test.go`. + +### 3. Verification — CORRECT + +**`web/middleware/sigv4a/sigv4a.go` (`verify`, lines 180-280):** + +- Scope pinning: service match (line 193), date vs X-Amz-Date (line 229). +- Signed `host` required (line 200). +- Signed `x-amz-region-set` required + `regionSetMatches` (lines 206-211). +- Clock skew vs default 15 min (lines 232-234). +- `X-Amz-Date` with `Date` header fallback (lines 213-224). +- Payload hash via `awssig.ResolvePayloadHash` in `Verify`; `verify` never touches `r.Body` (doc at line 179, confirmed by code). +- Public key via `KeyLookup` (precedence) or `Lookup`+`DeriveKeyPair` (lines 236-249). +- **Nil-key panic guard** (lines 253-259): a lookuper returning `(nil, nil)` → `ErrUnknownKey`, never reaches `ecdsa.VerifyASN1` which would panic on a nil key. +- `ECDSA-VerifyASN1` (line 276). +- Body buffered and reset in `Verify` (lines 163-171). + +**Observation (not a security issue):** The verifier requires `host` and `x-amz-region-set` signed but does not require `x-amz-date` or `x-amz-content-sha256` to be signed. This is safe because: (a) the signer always signs all four, and (b) `x-amz-date` is checked via clock-skew/scope independently of the signature, and `x-amz-content-sha256` is independently verified by `ResolvePayloadHash` (body re-hash + constant-time compare). An attacker stripping a signed header after signing cannot forge a valid signature because the signature covers the canonical request. + +### 4. parseAuthHeader / ParseCredential — CORRECT + +**`sigv4a.go:300-369`:** Scope must be exactly 4 parts ending in `aws4_request`; 5-part SigV4-style scopes rejected with `ErrMalformedAuth` (lines 336-339). Malformed → `ErrMalformedAuth`; missing/foreign prefix → `ErrMissingAuth` (line 301). Duplicate fields rejected (lines 314-326). `ParseCredential` wraps `parseAuthHeader`, propagating the same errors (lines 359-369). + +### 5. regionSetMatches — CORRECT + +**`regionset.go:10-30`:** `"*"` global wildcard, `"prefix*"` prefix wildcards, case-sensitive, empty region → `false` (line 11-13), empty entries skipped (lines 16-18), `*` anywhere but the end is not a wildcard (handled by `CutSuffix` only matching trailing `*`). Comprehensive test coverage in `regionset_test.go:5-31`. + +### 6. iamsts caching — CORRECT + +**`web/middleware/sigv4a/iamsts/iamsts.go`:** + +- **singleflight + context.WithoutCancel** (lines 195-209): `entry()` collapses concurrent misses via `v.sf.Do`. The fetch runs on `context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)`. A disconnected caller cannot fail collapsed waiters. Tested by `TestVerifier_FetchSurvivesCallerCancel`. +- **Refusal caching** (lines 232-239): `NOT_FOUND`/`PERMISSION_DENIED` → cached `ErrUnknownKey` refusal with `negTTL` (default 30s). Unknown-vs-disabled distinction logged, not exposed. Tested by `TestVerifier_NegativeCache`. +- **Other errors uncached → 500** (lines 242-243): iamd down, transport fault, malformed key propagate as `Internal` (500), not denial. Tested by `TestVerifier_IAMOutageIs500`. +- **cache_until honored** (lines 258-263): absent `cache_until` → entry used once, never cached. Tested by `TestVerifier_TTLSweep`. +- **Store sweep** (lines 269-279): `store()` sweeps TTL-expired entries under `mu`. O(n) sweep is fine for small key cardinality. Tested by `TestVerifier_TTLSweep`. +- **Identity re-read** (Verify lines 153-156): re-calls `v.entry` after `inner.Verify` succeeds; refetches if expired between. Tested by `TestVerifier_IdentityThreaded` and `TestVerifier_RevocationAfterCacheUntil`. + +### 7. Error mapping (TwirpError) — CORRECT + +**`sigv4a.go:126-149`:** + +| Error | Twirp code | HTTP | +| ------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ---- | +| `ErrMissingAuth` | `Unauthenticated` | 401 | +| `ErrMalformedAuth` | `InvalidArgument` | 400 | +| `ErrScopeMismatch`, `ErrClockSkew`, `ErrBodyTooLarge`, `ErrStreamingUnsupported`, `ErrMissingSignedHost`, `ErrMissingRegionSet` | `InvalidArgument` | 400 | +| `ErrUnknownKey`, `ErrUnauthorized`, `ErrBodyHash` | `PermissionDenied` | 403 | +| Unexpected (key store down) | `Internal` (logged) | 500 | + +Key/signature failures collapse to one opaque message ("invalid authentication header") so a probe cannot distinguish unknown, disabled, and mis-signed credentials. Tested by `TestTwirpErrorMapping`. + +### 8. Shared canonicalization (awssig) — CORRECT + +**`web/middleware/internal/awssig/`** (`canonicalization.go`, `payload.go`, `awssig.go`): + +- `BuildCanonicalRequest`, `CanonicalHeaderValue`, `collapseSpaces`, `CanonicalQuery`, `AWSURIEncode`, `CanonicalURI`, `wirePath`, `ResolvePayloadHash`. +- `collapseSpaces` folds whitespace runs unconditionally (matches AWS SDKs and aws-c-auth v4a vectors). +- `CanonicalQuery` sorts by name then value (not by joined string), avoiding prefix-name bugs. +- `ResolvePayloadHash`: STREAMING- rejected, body buffered+reset (capped at MaxBodySize), declared hash confirmed via `ConstantTimeCompare`, lowercase hex enforced, falls back to body hash when undeclared. + +### 9. Replay doc — PRESENT AND ACCURATE + +**`sigv4a.go:58-69`:** The `Verifier` doc comment clearly states SigV4A does NOT prevent replay, that a captured request verifies within `MaxClockSkew` (15 min default), and that callers requiring freshness must layer their own nonce/sequence/challenge. Accurate per the AWS SigV4A spec. + +### 10. Tests — mostly complete, minor gaps + +All test suites pass (`sigv4a`, `keyderivation`, `iamsts`, `sigv4aclient`, `awssig`) with `-race` and `go vet` clean. + +**Minor test-coverage gaps (not defects):** + +- `TestTwirpErrorMapping` does not exercise `ErrClockSkew`, `ErrBodyTooLarge`, `ErrStreamingUnsupported`, `ErrMissingSignedHost`, `ErrMissingRegionSet`, `ErrBodyHash` (all → InvalidArgument), or `ErrNotConfigured` (→ Internal). Straightforward switch branches; not a real gap. +- No test asserts replay protection (it is documentation, appropriately not tested). + +## Security Summary + +- **Replay:** Documented as not prevented; callers advised to layer nonce/sequence/challenge. +- **Probe indistinguishability:** Unknown key, disabled key, and signature mismatch all collapse to `PermissionDenied` (403) with opaque message. +- **Verification-only material:** `KeyLookup` path never sees the raw secret; public keys are verification-only. +- **Body size cap:** `MaxBodySize` (default 10 MiB) bounds memory an attacker can force the verifier to allocate. +- **Nil-key panic guard:** Prevents a lookuper bug from becoming a DoS via `ecdsa.VerifyASN1` nil dereference. +- **Singleflight resilience:** `context.WithoutCancel` ensures a disconnected caller cannot fail collapsed fetches. +- **Outage fail-closed:** IAM outages return 500, never 403. + +## Recommendations + +No code changes are required. The implementation is correct and spec-compliant. Optionally, expand `TestTwirpErrorMapping` to cover all InvalidArgument and Internal branches for completeness. diff --git a/gen/within/website/x/iam/sts/v1/sts.pb.go b/gen/within/website/x/iam/sts/v1/sts.pb.go index 394ceaa1..bd00b1ef 100644 --- a/gen/within/website/x/iam/sts/v1/sts.pb.go +++ b/gen/within/website/x/iam/sts/v1/sts.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/iam/sts/v1/sts.proto @@ -15,6 +15,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" timestamppb "google.golang.org/protobuf/types/known/timestamppb" + v1 "within.website/x/gen/within/website/x/iam/v1" ) const ( @@ -110,7 +111,12 @@ type GetSigningKeyResponse struct { // How long the caller may cache this response before asking again. Bounds // revocation latency: a disabled key stops verifying within one cache TTL // instead of at end of day. Always <= not_valid_after. - CacheUntil *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=cache_until,json=cacheUntil,proto3" json:"cache_until,omitempty"` + CacheUntil *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=cache_until,json=cacheUntil,proto3" json:"cache_until,omitempty"` + // The caller's effective policy set: every document attached to the + // identity's user. Downstream verifiers evaluate these locally (see + // within.website/x/iam/authz) instead of calling back per request. Empty + // for an admin identity, which bypasses policy evaluation. + Policies []*v1.Policy `protobuf:"bytes,5,rep,name=policies,proto3" json:"policies,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -173,6 +179,13 @@ func (x *GetSigningKeyResponse) GetCacheUntil() *timestamppb.Timestamp { return nil } +func (x *GetSigningKeyResponse) GetPolicies() []*v1.Policy { + if x != nil { + return x.Policies + } + return nil +} + type GetPublicKeyRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The access key id parsed from the request's Credential= component. @@ -229,7 +242,9 @@ type GetPublicKeyResponse struct { // How long the caller may cache this response before asking again. Bounds // revocation latency: a disabled key stops verifying within one cache TTL. // A response without cache_until may be used once but never cached. - CacheUntil *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=cache_until,json=cacheUntil,proto3" json:"cache_until,omitempty"` + CacheUntil *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=cache_until,json=cacheUntil,proto3" json:"cache_until,omitempty"` + // The caller's effective policy set, mirroring GetSigningKeyResponse. + Policies []*v1.Policy `protobuf:"bytes,4,rep,name=policies,proto3" json:"policies,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -285,6 +300,13 @@ func (x *GetPublicKeyResponse) GetCacheUntil() *timestamppb.Timestamp { return nil } +func (x *GetPublicKeyResponse) GetPolicies() []*v1.Policy { + if x != nil { + return x.Policies + } + return nil +} + // TokenIdentity identifies the principal a signing key belongs to. type TokenIdentity struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -297,7 +319,10 @@ type TokenIdentity struct { // The principal within the organization: iamd's user UUID. PrincipalId string `protobuf:"bytes,3,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"` // Human-readable name for logs and error messages: iamd's user name. - DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + DisplayName string `protobuf:"bytes,4,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Whether the principal is an iamd admin. Admin bypasses policy + // evaluation (allow-all); verifiers must honor it exactly as iamd does. + IsAdmin bool `protobuf:"varint,5,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -360,37 +385,47 @@ func (x *TokenIdentity) GetDisplayName() string { return "" } +func (x *TokenIdentity) GetIsAdmin() bool { + if x != nil { + return x.IsAdmin + } + return false +} + var File_within_website_x_iam_sts_v1_sts_proto protoreflect.FileDescriptor const file_within_website_x_iam_sts_v1_sts_proto_rawDesc = "" + "\n" + - "%within/website/x/iam/sts/v1/sts.proto\x12\x1bwithin.website.x.iam.sts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xae\x01\n" + + "%within/website/x/iam/sts/v1/sts.proto\x12\x1bwithin.website.x.iam.sts.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a$within/website/x/iam/v1/policy.proto\"\xae\x01\n" + "\x14GetSigningKeyRequest\x12*\n" + "\raccess_key_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\vaccessKeyId\x12(\n" + "\x04date\x18\x02 \x01(\tB\x14\xbaH\x11\xc8\x01\x01r\f2\n" + "^[0-9]{8}$R\x04date\x12\x1e\n" + "\x06region\x18\x03 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06region\x12 \n" + - "\aservice\x18\x04 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\aservice\"\x81\x02\n" + + "\aservice\x18\x04 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\aservice\"\xbe\x02\n" + "\x15GetSigningKeyResponse\x12\x1f\n" + "\vsigning_key\x18\x01 \x01(\fR\n" + "signingKey\x12F\n" + "\bidentity\x18\x02 \x01(\v2*.within.website.x.iam.sts.v1.TokenIdentityR\bidentity\x12B\n" + "\x0fnot_valid_after\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\rnotValidAfter\x12;\n" + "\vcache_until\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "cacheUntil\"A\n" + + "cacheUntil\x12;\n" + + "\bpolicies\x18\x05 \x03(\v2\x1f.within.website.x.iam.v1.PolicyR\bpolicies\"A\n" + "\x13GetPublicKeyRequest\x12*\n" + - "\raccess_key_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\vaccessKeyId\"\xba\x01\n" + + "\raccess_key_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\vaccessKeyId\"\xf7\x01\n" + "\x14GetPublicKeyResponse\x12\x1d\n" + "\n" + "public_key\x18\x01 \x01(\fR\tpublicKey\x12F\n" + "\bidentity\x18\x02 \x01(\v2*.within.website.x.iam.sts.v1.TokenIdentityR\bidentity\x12;\n" + "\vcache_until\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\n" + - "cacheUntil\"\xa2\x01\n" + + "cacheUntil\x12;\n" + + "\bpolicies\x18\x04 \x03(\v2\x1f.within.website.x.iam.v1.PolicyR\bpolicies\"\xbd\x01\n" + "\rTokenIdentity\x12\"\n" + "\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12'\n" + "\x0forganization_id\x18\x02 \x01(\tR\x0eorganizationId\x12!\n" + "\fprincipal_id\x18\x03 \x01(\tR\vprincipalId\x12!\n" + - "\fdisplay_name\x18\x04 \x01(\tR\vdisplayName2\x80\x02\n" + + "\fdisplay_name\x18\x04 \x01(\tR\vdisplayName\x12\x19\n" + + "\bis_admin\x18\x05 \x01(\bR\aisAdmin2\x80\x02\n" + "\x11SigningKeyService\x12v\n" + "\rGetSigningKey\x121.within.website.x.iam.sts.v1.GetSigningKeyRequest\x1a2.within.website.x.iam.sts.v1.GetSigningKeyResponse\x12s\n" + "\fGetPublicKey\x120.within.website.x.iam.sts.v1.GetPublicKeyRequest\x1a1.within.website.x.iam.sts.v1.GetPublicKeyResponseB\xf6\x01\n" + @@ -416,22 +451,25 @@ var file_within_website_x_iam_sts_v1_sts_proto_goTypes = []any{ (*GetPublicKeyResponse)(nil), // 3: within.website.x.iam.sts.v1.GetPublicKeyResponse (*TokenIdentity)(nil), // 4: within.website.x.iam.sts.v1.TokenIdentity (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*v1.Policy)(nil), // 6: within.website.x.iam.v1.Policy } var file_within_website_x_iam_sts_v1_sts_proto_depIdxs = []int32{ 4, // 0: within.website.x.iam.sts.v1.GetSigningKeyResponse.identity:type_name -> within.website.x.iam.sts.v1.TokenIdentity 5, // 1: within.website.x.iam.sts.v1.GetSigningKeyResponse.not_valid_after:type_name -> google.protobuf.Timestamp 5, // 2: within.website.x.iam.sts.v1.GetSigningKeyResponse.cache_until:type_name -> google.protobuf.Timestamp - 4, // 3: within.website.x.iam.sts.v1.GetPublicKeyResponse.identity:type_name -> within.website.x.iam.sts.v1.TokenIdentity - 5, // 4: within.website.x.iam.sts.v1.GetPublicKeyResponse.cache_until:type_name -> google.protobuf.Timestamp - 0, // 5: within.website.x.iam.sts.v1.SigningKeyService.GetSigningKey:input_type -> within.website.x.iam.sts.v1.GetSigningKeyRequest - 2, // 6: within.website.x.iam.sts.v1.SigningKeyService.GetPublicKey:input_type -> within.website.x.iam.sts.v1.GetPublicKeyRequest - 1, // 7: within.website.x.iam.sts.v1.SigningKeyService.GetSigningKey:output_type -> within.website.x.iam.sts.v1.GetSigningKeyResponse - 3, // 8: within.website.x.iam.sts.v1.SigningKeyService.GetPublicKey:output_type -> within.website.x.iam.sts.v1.GetPublicKeyResponse - 7, // [7:9] is the sub-list for method output_type - 5, // [5:7] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 6, // 3: within.website.x.iam.sts.v1.GetSigningKeyResponse.policies:type_name -> within.website.x.iam.v1.Policy + 4, // 4: within.website.x.iam.sts.v1.GetPublicKeyResponse.identity:type_name -> within.website.x.iam.sts.v1.TokenIdentity + 5, // 5: within.website.x.iam.sts.v1.GetPublicKeyResponse.cache_until:type_name -> google.protobuf.Timestamp + 6, // 6: within.website.x.iam.sts.v1.GetPublicKeyResponse.policies:type_name -> within.website.x.iam.v1.Policy + 0, // 7: within.website.x.iam.sts.v1.SigningKeyService.GetSigningKey:input_type -> within.website.x.iam.sts.v1.GetSigningKeyRequest + 2, // 8: within.website.x.iam.sts.v1.SigningKeyService.GetPublicKey:input_type -> within.website.x.iam.sts.v1.GetPublicKeyRequest + 1, // 9: within.website.x.iam.sts.v1.SigningKeyService.GetSigningKey:output_type -> within.website.x.iam.sts.v1.GetSigningKeyResponse + 3, // 10: within.website.x.iam.sts.v1.SigningKeyService.GetPublicKey:output_type -> within.website.x.iam.sts.v1.GetPublicKeyResponse + 9, // [9:11] is the sub-list for method output_type + 7, // [7:9] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_within_website_x_iam_sts_v1_sts_proto_init() } diff --git a/gen/within/website/x/iam/sts/v1/sts.twirp.go b/gen/within/website/x/iam/sts/v1/sts.twirp.go index e7756255..1b81bd14 100644 --- a/gen/within/website/x/iam/sts/v1/sts.twirp.go +++ b/gen/within/website/x/iam/sts/v1/sts.twirp.go @@ -1409,47 +1409,51 @@ func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) } var twirpFileDescriptor0 = []byte{ - // 658 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xc1, 0x6e, 0xd3, 0x4c, - 0x10, 0xc7, 0x65, 0xa7, 0x5f, 0xbf, 0x76, 0x93, 0x50, 0x75, 0x29, 0x52, 0x94, 0x0a, 0x5a, 0x22, - 0x50, 0xab, 0x4a, 0xac, 0xeb, 0x20, 0x21, 0x70, 0x4f, 0xcd, 0x81, 0xd6, 0xaa, 0x40, 0x91, 0x5d, - 0x92, 0x08, 0x02, 0xd1, 0xc6, 0xde, 0xba, 0xab, 0xc6, 0x6b, 0xe3, 0xdd, 0xa4, 0x0d, 0x08, 0x09, - 0x5e, 0xa5, 0x47, 0x0e, 0x3c, 0x40, 0x9f, 0xa0, 0xe2, 0x31, 0x38, 0xf2, 0x00, 0x9c, 0x91, 0xbd, - 0x4e, 0x88, 0xab, 0x2a, 0x55, 0x10, 0xa7, 0xc4, 0x33, 0xbf, 0xff, 0xec, 0xce, 0x7f, 0xc6, 0x06, - 0x0f, 0x4f, 0xa9, 0x38, 0xa6, 0x4c, 0x3b, 0x25, 0x5d, 0x4e, 0x05, 0xd1, 0xce, 0x34, 0x8a, 0x7d, - 0x8d, 0x0b, 0xae, 0x0d, 0xf4, 0xf8, 0x07, 0x85, 0x51, 0x20, 0x02, 0xb8, 0x2a, 0x31, 0x94, 0x62, - 0xe8, 0x0c, 0x51, 0xec, 0xa3, 0x38, 0x3f, 0xd0, 0xcb, 0xab, 0xdd, 0xfe, 0x91, 0x36, 0xc0, 0x3d, - 0xea, 0x62, 0x41, 0xc6, 0x7f, 0xa4, 0xb2, 0xbc, 0xe6, 0x05, 0x81, 0xd7, 0x23, 0x5a, 0xf2, 0x14, - 0x83, 0x82, 0xfa, 0x84, 0x0b, 0xec, 0x87, 0x12, 0xa8, 0x7c, 0x53, 0xc0, 0xca, 0x1e, 0x11, 0x36, - 0xf5, 0x18, 0x65, 0xde, 0x01, 0x19, 0x5a, 0xe4, 0x7d, 0x9f, 0x70, 0x01, 0xb7, 0x40, 0x11, 0x3b, - 0x0e, 0xe1, 0xbc, 0x73, 0x42, 0x86, 0x1d, 0xea, 0x96, 0x94, 0x75, 0x65, 0x73, 0xb1, 0x36, 0x7f, - 0xb1, 0x9f, 0xbb, 0x54, 0x14, 0x2b, 0x2f, 0x93, 0x07, 0x64, 0x68, 0xba, 0x70, 0x13, 0xcc, 0xc5, - 0x67, 0x96, 0xd4, 0x04, 0x59, 0xb9, 0xd8, 0x5f, 0xbe, 0x54, 0x94, 0xa8, 0x50, 0x05, 0xef, 0xde, - 0x6c, 0x3f, 0x7a, 0xf6, 0xf6, 0xe3, 0xd3, 0x4f, 0x0f, 0xac, 0x84, 0x80, 0xf7, 0xc0, 0x7c, 0x44, - 0x3c, 0x1a, 0xb0, 0x52, 0x2e, 0x53, 0x2e, 0x8d, 0xc2, 0x75, 0xf0, 0x3f, 0x27, 0xd1, 0x80, 0x3a, - 0xa4, 0x34, 0x97, 0x01, 0x46, 0xe1, 0xca, 0x17, 0x15, 0xdc, 0xb9, 0x72, 0x61, 0x1e, 0x06, 0x8c, - 0x13, 0xb8, 0x06, 0xf2, 0x5c, 0x46, 0xe3, 0x2b, 0x27, 0xf7, 0x2d, 0x58, 0x80, 0x8f, 0x41, 0xf8, - 0x1c, 0x2c, 0x50, 0x97, 0x30, 0x41, 0xc5, 0x30, 0xb9, 0x6a, 0xbe, 0xba, 0x85, 0xa6, 0x38, 0x8b, - 0x0e, 0x83, 0x13, 0xc2, 0xcc, 0x54, 0x61, 0x8d, 0xb5, 0xb0, 0x06, 0x96, 0x58, 0x20, 0x3a, 0x89, - 0xd5, 0x1d, 0x7c, 0x24, 0x48, 0x94, 0x74, 0x93, 0xaf, 0x96, 0x91, 0xb4, 0x1b, 0x8d, 0xec, 0x46, - 0x87, 0x23, 0xbb, 0xad, 0x22, 0x0b, 0x44, 0x23, 0x56, 0xec, 0xc6, 0x02, 0xb8, 0x03, 0xf2, 0x0e, - 0x76, 0x8e, 0x49, 0xa7, 0xcf, 0x04, 0xed, 0x95, 0xe6, 0x6e, 0xd4, 0x83, 0x04, 0x7f, 0x15, 0xd3, - 0x95, 0x5d, 0x70, 0x7b, 0x8f, 0x88, 0x7a, 0xbf, 0xdb, 0xa3, 0xce, 0xdf, 0x8d, 0xac, 0x72, 0x21, - 0xe7, 0x3e, 0x51, 0x23, 0x75, 0xf1, 0x2e, 0x00, 0x61, 0x12, 0x9c, 0x30, 0x71, 0x31, 0x1c, 0x61, - 0xff, 0xcc, 0xc3, 0x2b, 0xfd, 0xe7, 0x66, 0xea, 0xff, 0x5c, 0x01, 0xc5, 0x4c, 0x61, 0x58, 0xb9, - 0xb6, 0xf5, 0xec, 0x96, 0x6e, 0x80, 0xa5, 0x20, 0xf2, 0x30, 0xa3, 0x1f, 0xb0, 0xa0, 0x01, 0x8b, - 0xa9, 0x64, 0x61, 0xad, 0x5b, 0x93, 0x61, 0xd3, 0x85, 0xf7, 0x41, 0x21, 0x8c, 0x28, 0x73, 0x68, - 0x88, 0x7b, 0x31, 0x95, 0x93, 0xb5, 0xc6, 0x31, 0x89, 0xb8, 0x94, 0x87, 0x3d, 0x3c, 0xec, 0x30, - 0xec, 0xa7, 0xcb, 0x6a, 0xe5, 0xd3, 0xd8, 0x4b, 0xec, 0x93, 0xea, 0x67, 0x15, 0x2c, 0xff, 0xd9, - 0x52, 0x5b, 0xae, 0x2f, 0x1c, 0x80, 0x62, 0x66, 0x7b, 0xa1, 0x3e, 0xd5, 0xbe, 0xeb, 0x5e, 0xcd, - 0x72, 0x75, 0x16, 0x49, 0x3a, 0x56, 0x0e, 0x0a, 0x93, 0xe3, 0x86, 0xdb, 0x37, 0xd5, 0xb8, 0xba, - 0x5d, 0x65, 0x7d, 0x06, 0x85, 0x3c, 0xb4, 0xf6, 0x4b, 0x01, 0x6b, 0x4e, 0xe0, 0x4f, 0x13, 0xd6, - 0x16, 0x6c, 0xc1, 0xeb, 0xf1, 0xb8, 0xeb, 0xca, 0xeb, 0x27, 0x59, 0x50, 0x3b, 0xd3, 0x3c, 0xc2, - 0xb4, 0x29, 0xdf, 0xc8, 0x1d, 0x2e, 0xf8, 0x40, 0x3f, 0x57, 0xff, 0x6b, 0x36, 0x5b, 0xa6, 0xfd, - 0x55, 0x5d, 0x6d, 0xca, 0x02, 0xcd, 0xf4, 0xa4, 0x16, 0x32, 0xb1, 0x8f, 0x6c, 0xc1, 0x51, 0x43, - 0xff, 0x3e, 0xca, 0xb6, 0xd3, 0x6c, 0xbb, 0xd5, 0x36, 0xb1, 0xdf, 0xb6, 0x05, 0x6f, 0x37, 0xf4, - 0x1f, 0xea, 0xc6, 0x94, 0x6c, 0x7b, 0xaf, 0x5e, 0x7b, 0x41, 0x04, 0x76, 0xb1, 0xc0, 0x3f, 0xd5, - 0x75, 0x49, 0x1a, 0x46, 0x8a, 0x1a, 0x46, 0xcb, 0x30, 0x4c, 0xec, 0x1b, 0x86, 0x2d, 0xb8, 0x61, - 0x34, 0xf4, 0xee, 0x7c, 0xb2, 0xc0, 0x8f, 0x7f, 0x0f, 0x00, 0xd5, 0x72, 0xef, 0xe5, 0xe0, 0x05, - 0x00, 0x00, + // 721 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x94, 0xdf, 0x6e, 0x12, 0x4f, + 0x14, 0xc7, 0xb3, 0x40, 0x5b, 0x3a, 0xc0, 0xaf, 0xe9, 0xfc, 0x6a, 0x82, 0x34, 0x0a, 0x92, 0x9a, + 0x92, 0x26, 0xce, 0x76, 0x31, 0x31, 0xba, 0xbd, 0x2a, 0x17, 0xb6, 0xa4, 0xd1, 0x90, 0xa5, 0x02, + 0x51, 0x94, 0x0c, 0xbb, 0xd3, 0xed, 0xa4, 0xbb, 0xb3, 0xeb, 0xce, 0xb0, 0x2d, 0x1a, 0x13, 0x9f, + 0xc5, 0x4b, 0x2f, 0x7c, 0x80, 0x26, 0x7a, 0xdd, 0xf8, 0x18, 0x5e, 0xfa, 0x00, 0x7a, 0x6b, 0xf6, + 0x0f, 0x08, 0x0d, 0xa1, 0x69, 0xe3, 0x15, 0xec, 0x99, 0xcf, 0xf7, 0xec, 0xd9, 0xf3, 0x3d, 0x67, + 0xc0, 0xfd, 0x53, 0x2a, 0x8e, 0x29, 0x93, 0x4f, 0x49, 0x9f, 0x53, 0x41, 0xe4, 0x33, 0x99, 0x62, + 0x5b, 0xe6, 0x82, 0xcb, 0xbe, 0x12, 0xfc, 0x20, 0xd7, 0x73, 0x84, 0x03, 0xd7, 0x23, 0x0c, 0xc5, + 0x18, 0x3a, 0x43, 0x14, 0xdb, 0x28, 0x38, 0xf7, 0x95, 0xc2, 0x7a, 0x7f, 0x70, 0x24, 0xfb, 0xd8, + 0xa2, 0x06, 0x16, 0x64, 0xfc, 0x27, 0x52, 0x16, 0x8a, 0xa6, 0xe3, 0x98, 0x16, 0x91, 0xc3, 0xa7, + 0x00, 0x14, 0xd4, 0x26, 0x5c, 0x60, 0xdb, 0x8d, 0x81, 0x8d, 0x99, 0x15, 0xf8, 0x8a, 0xec, 0x3a, + 0x16, 0xd5, 0x87, 0x11, 0x55, 0xfe, 0x22, 0x81, 0xb5, 0x3d, 0x22, 0x9a, 0xd4, 0x64, 0x94, 0x99, + 0x07, 0x64, 0xa8, 0x91, 0xb7, 0x03, 0xc2, 0x05, 0xdc, 0x02, 0x39, 0xac, 0xeb, 0x84, 0xf3, 0xde, + 0x09, 0x19, 0xf6, 0xa8, 0x91, 0x97, 0x4a, 0x52, 0x65, 0xb9, 0xb6, 0x78, 0xbe, 0x9f, 0xbc, 0x90, + 0x24, 0x2d, 0x13, 0x1d, 0x1e, 0x90, 0x61, 0xdd, 0x80, 0x15, 0x90, 0x0a, 0x2a, 0xcb, 0x27, 0x42, + 0x64, 0xed, 0x7c, 0x7f, 0xf5, 0x42, 0x92, 0xbc, 0x6c, 0x15, 0xbc, 0x79, 0xb5, 0xfd, 0xe0, 0xc9, + 0xeb, 0xf7, 0x8f, 0x3f, 0x6c, 0x68, 0x21, 0x01, 0xef, 0x82, 0x45, 0x8f, 0x98, 0xd4, 0x61, 0xf9, + 0xe4, 0x54, 0xba, 0x38, 0x0a, 0x4b, 0x60, 0x89, 0x13, 0xcf, 0xa7, 0x3a, 0xc9, 0xa7, 0xa6, 0x80, + 0x51, 0xb8, 0xfc, 0x2d, 0x01, 0x6e, 0x5d, 0x2a, 0x98, 0xbb, 0x0e, 0xe3, 0x04, 0x16, 0x41, 0x86, + 0x47, 0xd1, 0xa0, 0xe4, 0xb0, 0xde, 0xac, 0x06, 0xf8, 0x18, 0x84, 0x4f, 0x41, 0x9a, 0x1a, 0x84, + 0x09, 0x2a, 0x86, 0x61, 0xa9, 0x99, 0xea, 0x16, 0x9a, 0xd3, 0x7f, 0x74, 0xe8, 0x9c, 0x10, 0x56, + 0x8f, 0x15, 0xda, 0x58, 0x0b, 0x6b, 0x60, 0x85, 0x39, 0xa2, 0x17, 0x1a, 0xd2, 0xc3, 0x47, 0x82, + 0x78, 0xe1, 0xd7, 0x64, 0xaa, 0x05, 0x14, 0x99, 0x82, 0x46, 0xa6, 0xa0, 0xc3, 0x91, 0x29, 0x5a, + 0x8e, 0x39, 0xa2, 0x15, 0x28, 0x76, 0x03, 0x01, 0xdc, 0x01, 0x19, 0x1d, 0xeb, 0xc7, 0xa4, 0x37, + 0x60, 0x82, 0x5a, 0xf9, 0xd4, 0x95, 0x7a, 0x10, 0xe2, 0x2f, 0x02, 0x1a, 0xee, 0x80, 0x74, 0x68, + 0x22, 0x25, 0x3c, 0xbf, 0x50, 0x4a, 0x56, 0x32, 0xd5, 0xe2, 0xec, 0x0f, 0xf1, 0x15, 0xd4, 0x08, + 0xdd, 0xd6, 0xc6, 0x82, 0xf2, 0x2e, 0xf8, 0x7f, 0x8f, 0x88, 0xc6, 0xa0, 0x6f, 0x51, 0xfd, 0x66, + 0x7e, 0x97, 0x7f, 0x47, 0x43, 0x33, 0x91, 0x23, 0xb6, 0xe0, 0x0e, 0x00, 0x6e, 0x18, 0x9c, 0x70, + 0x60, 0xd9, 0x1d, 0x61, 0xff, 0xcc, 0x80, 0x4b, 0xcd, 0x4b, 0xde, 0xb8, 0x79, 0xa9, 0xeb, 0x36, + 0xef, 0xab, 0x04, 0x72, 0x53, 0x55, 0xc1, 0xf2, 0xcc, 0xbe, 0x4d, 0xef, 0xc7, 0x26, 0x58, 0x71, + 0x3c, 0x13, 0x33, 0xfa, 0x0e, 0x0b, 0xea, 0xb0, 0x80, 0x0a, 0x57, 0x45, 0xfb, 0x6f, 0x32, 0x5c, + 0x37, 0xe0, 0x3d, 0x90, 0x75, 0x3d, 0xca, 0x74, 0xea, 0x62, 0x2b, 0xa0, 0x92, 0x51, 0xae, 0x71, + 0x2c, 0x42, 0x0c, 0xca, 0x5d, 0x0b, 0x0f, 0x7b, 0x0c, 0xdb, 0xf1, 0x9a, 0x68, 0x99, 0x38, 0xf6, + 0x1c, 0xdb, 0x04, 0xde, 0x06, 0x69, 0xca, 0x7b, 0xd8, 0xb0, 0x29, 0xcb, 0x2f, 0x94, 0xa4, 0x4a, + 0x5a, 0x5b, 0xa2, 0x7c, 0x37, 0x78, 0xac, 0x7e, 0x4c, 0x80, 0xd5, 0xbf, 0xab, 0xd3, 0x8c, 0x76, + 0x0a, 0xfa, 0x20, 0x37, 0xb5, 0x52, 0x50, 0x99, 0x6b, 0xcb, 0xac, 0xfb, 0xa2, 0x50, 0xbd, 0x8e, + 0x24, 0x1e, 0x17, 0x0e, 0xb2, 0x93, 0x63, 0x04, 0xb7, 0xaf, 0xca, 0x71, 0x79, 0x6a, 0x0b, 0xca, + 0x35, 0x14, 0xd1, 0x4b, 0x6b, 0xbf, 0x24, 0x50, 0xd4, 0x1d, 0x7b, 0x9e, 0xb0, 0x96, 0x6e, 0x0a, + 0xde, 0x08, 0xc6, 0xa8, 0x21, 0xbd, 0x7c, 0x34, 0x0d, 0xca, 0x67, 0xb2, 0x49, 0x98, 0x3c, 0xe7, + 0x7a, 0xdf, 0xe1, 0x82, 0xfb, 0xca, 0xa7, 0xc4, 0x42, 0xbb, 0xdd, 0xa9, 0x37, 0x3f, 0x27, 0xd6, + 0xdb, 0x51, 0x82, 0x76, 0xfc, 0xa6, 0x0e, 0xaa, 0x63, 0x1b, 0x35, 0x05, 0x47, 0x2d, 0xe5, 0xfb, + 0xe8, 0xb4, 0x1b, 0x9f, 0x76, 0x3b, 0xdd, 0x3a, 0xb6, 0xbb, 0x4d, 0xc1, 0xbb, 0x2d, 0xe5, 0x47, + 0x62, 0x73, 0xce, 0x69, 0x77, 0xaf, 0x51, 0x7b, 0x46, 0x04, 0x36, 0xb0, 0xc0, 0x3f, 0x13, 0xa5, + 0x88, 0x54, 0xd5, 0x18, 0x55, 0xd5, 0x8e, 0xaa, 0xd6, 0xb1, 0xad, 0xaa, 0x4d, 0xc1, 0x55, 0xb5, + 0xa5, 0xf4, 0x17, 0xc3, 0xc5, 0x78, 0xf8, 0x67, 0x00, 0xba, 0x6a, 0x0b, 0xd3, 0x9b, 0x06, 0x00, + 0x00, } diff --git a/gen/within/website/x/iam/v1/iamv1connect/policy.connect.go b/gen/within/website/x/iam/v1/iamv1connect/policy.connect.go new file mode 100644 index 00000000..0890d706 --- /dev/null +++ b/gen/within/website/x/iam/v1/iamv1connect/policy.connect.go @@ -0,0 +1,365 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: within/website/x/iam/v1/policy.proto + +package iamv1connect + +import ( + context "context" + errors "errors" + http "net/http" + strings "strings" + + connect "connectrpc.com/connect" + v1 "within.website/x/gen/within/website/x/iam/v1" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // PolicyServiceName is the fully-qualified name of the PolicyService service. + PolicyServiceName = "within.website.x.iam.v1.PolicyService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // PolicyServiceCreatePolicyProcedure is the fully-qualified name of the PolicyService's + // CreatePolicy RPC. + PolicyServiceCreatePolicyProcedure = "/within.website.x.iam.v1.PolicyService/CreatePolicy" + // PolicyServiceDeletePolicyProcedure is the fully-qualified name of the PolicyService's + // DeletePolicy RPC. + PolicyServiceDeletePolicyProcedure = "/within.website.x.iam.v1.PolicyService/DeletePolicy" + // PolicyServiceGetPolicyProcedure is the fully-qualified name of the PolicyService's GetPolicy RPC. + PolicyServiceGetPolicyProcedure = "/within.website.x.iam.v1.PolicyService/GetPolicy" + // PolicyServiceListPoliciesProcedure is the fully-qualified name of the PolicyService's + // ListPolicies RPC. + PolicyServiceListPoliciesProcedure = "/within.website.x.iam.v1.PolicyService/ListPolicies" + // PolicyServiceAttachPolicyProcedure is the fully-qualified name of the PolicyService's + // AttachPolicy RPC. + PolicyServiceAttachPolicyProcedure = "/within.website.x.iam.v1.PolicyService/AttachPolicy" + // PolicyServiceDetachPolicyProcedure is the fully-qualified name of the PolicyService's + // DetachPolicy RPC. + PolicyServiceDetachPolicyProcedure = "/within.website.x.iam.v1.PolicyService/DetachPolicy" + // PolicyServiceListPoliciesForUserProcedure is the fully-qualified name of the PolicyService's + // ListPoliciesForUser RPC. + PolicyServiceListPoliciesForUserProcedure = "/within.website.x.iam.v1.PolicyService/ListPoliciesForUser" +) + +// PolicyServiceClient is a client for the within.website.x.iam.v1.PolicyService service. +type PolicyServiceClient interface { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + CreatePolicy(context.Context, *connect.Request[v1.CreatePolicyReq]) (*connect.Response[v1.CreatePolicyResp], error) + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + DeletePolicy(context.Context, *connect.Request[v1.DeletePolicyReq]) (*connect.Response[v1.DeletePolicyResp], error) + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + GetPolicy(context.Context, *connect.Request[v1.GetPolicyReq]) (*connect.Response[v1.GetPolicyResp], error) + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + ListPolicies(context.Context, *connect.Request[v1.ListPoliciesReq]) (*connect.Response[v1.ListPoliciesResp], error) + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + AttachPolicy(context.Context, *connect.Request[v1.AttachPolicyReq]) (*connect.Response[v1.AttachPolicyResp], error) + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + DetachPolicy(context.Context, *connect.Request[v1.DetachPolicyReq]) (*connect.Response[v1.DetachPolicyResp], error) + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + ListPoliciesForUser(context.Context, *connect.Request[v1.ListPoliciesForUserReq]) (*connect.Response[v1.ListPoliciesForUserResp], error) +} + +// NewPolicyServiceClient constructs a client for the within.website.x.iam.v1.PolicyService service. +// By default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped +// responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewPolicyServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PolicyServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + policyServiceMethods := v1.File_within_website_x_iam_v1_policy_proto.Services().ByName("PolicyService").Methods() + return &policyServiceClient{ + createPolicy: connect.NewClient[v1.CreatePolicyReq, v1.CreatePolicyResp]( + httpClient, + baseURL+PolicyServiceCreatePolicyProcedure, + connect.WithSchema(policyServiceMethods.ByName("CreatePolicy")), + connect.WithClientOptions(opts...), + ), + deletePolicy: connect.NewClient[v1.DeletePolicyReq, v1.DeletePolicyResp]( + httpClient, + baseURL+PolicyServiceDeletePolicyProcedure, + connect.WithSchema(policyServiceMethods.ByName("DeletePolicy")), + connect.WithClientOptions(opts...), + ), + getPolicy: connect.NewClient[v1.GetPolicyReq, v1.GetPolicyResp]( + httpClient, + baseURL+PolicyServiceGetPolicyProcedure, + connect.WithSchema(policyServiceMethods.ByName("GetPolicy")), + connect.WithClientOptions(opts...), + ), + listPolicies: connect.NewClient[v1.ListPoliciesReq, v1.ListPoliciesResp]( + httpClient, + baseURL+PolicyServiceListPoliciesProcedure, + connect.WithSchema(policyServiceMethods.ByName("ListPolicies")), + connect.WithClientOptions(opts...), + ), + attachPolicy: connect.NewClient[v1.AttachPolicyReq, v1.AttachPolicyResp]( + httpClient, + baseURL+PolicyServiceAttachPolicyProcedure, + connect.WithSchema(policyServiceMethods.ByName("AttachPolicy")), + connect.WithClientOptions(opts...), + ), + detachPolicy: connect.NewClient[v1.DetachPolicyReq, v1.DetachPolicyResp]( + httpClient, + baseURL+PolicyServiceDetachPolicyProcedure, + connect.WithSchema(policyServiceMethods.ByName("DetachPolicy")), + connect.WithClientOptions(opts...), + ), + listPoliciesForUser: connect.NewClient[v1.ListPoliciesForUserReq, v1.ListPoliciesForUserResp]( + httpClient, + baseURL+PolicyServiceListPoliciesForUserProcedure, + connect.WithSchema(policyServiceMethods.ByName("ListPoliciesForUser")), + connect.WithClientOptions(opts...), + ), + } +} + +// policyServiceClient implements PolicyServiceClient. +type policyServiceClient struct { + createPolicy *connect.Client[v1.CreatePolicyReq, v1.CreatePolicyResp] + deletePolicy *connect.Client[v1.DeletePolicyReq, v1.DeletePolicyResp] + getPolicy *connect.Client[v1.GetPolicyReq, v1.GetPolicyResp] + listPolicies *connect.Client[v1.ListPoliciesReq, v1.ListPoliciesResp] + attachPolicy *connect.Client[v1.AttachPolicyReq, v1.AttachPolicyResp] + detachPolicy *connect.Client[v1.DetachPolicyReq, v1.DetachPolicyResp] + listPoliciesForUser *connect.Client[v1.ListPoliciesForUserReq, v1.ListPoliciesForUserResp] +} + +// CreatePolicy calls within.website.x.iam.v1.PolicyService.CreatePolicy. +func (c *policyServiceClient) CreatePolicy(ctx context.Context, req *connect.Request[v1.CreatePolicyReq]) (*connect.Response[v1.CreatePolicyResp], error) { + return c.createPolicy.CallUnary(ctx, req) +} + +// DeletePolicy calls within.website.x.iam.v1.PolicyService.DeletePolicy. +func (c *policyServiceClient) DeletePolicy(ctx context.Context, req *connect.Request[v1.DeletePolicyReq]) (*connect.Response[v1.DeletePolicyResp], error) { + return c.deletePolicy.CallUnary(ctx, req) +} + +// GetPolicy calls within.website.x.iam.v1.PolicyService.GetPolicy. +func (c *policyServiceClient) GetPolicy(ctx context.Context, req *connect.Request[v1.GetPolicyReq]) (*connect.Response[v1.GetPolicyResp], error) { + return c.getPolicy.CallUnary(ctx, req) +} + +// ListPolicies calls within.website.x.iam.v1.PolicyService.ListPolicies. +func (c *policyServiceClient) ListPolicies(ctx context.Context, req *connect.Request[v1.ListPoliciesReq]) (*connect.Response[v1.ListPoliciesResp], error) { + return c.listPolicies.CallUnary(ctx, req) +} + +// AttachPolicy calls within.website.x.iam.v1.PolicyService.AttachPolicy. +func (c *policyServiceClient) AttachPolicy(ctx context.Context, req *connect.Request[v1.AttachPolicyReq]) (*connect.Response[v1.AttachPolicyResp], error) { + return c.attachPolicy.CallUnary(ctx, req) +} + +// DetachPolicy calls within.website.x.iam.v1.PolicyService.DetachPolicy. +func (c *policyServiceClient) DetachPolicy(ctx context.Context, req *connect.Request[v1.DetachPolicyReq]) (*connect.Response[v1.DetachPolicyResp], error) { + return c.detachPolicy.CallUnary(ctx, req) +} + +// ListPoliciesForUser calls within.website.x.iam.v1.PolicyService.ListPoliciesForUser. +func (c *policyServiceClient) ListPoliciesForUser(ctx context.Context, req *connect.Request[v1.ListPoliciesForUserReq]) (*connect.Response[v1.ListPoliciesForUserResp], error) { + return c.listPoliciesForUser.CallUnary(ctx, req) +} + +// PolicyServiceHandler is an implementation of the within.website.x.iam.v1.PolicyService service. +type PolicyServiceHandler interface { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + CreatePolicy(context.Context, *connect.Request[v1.CreatePolicyReq]) (*connect.Response[v1.CreatePolicyResp], error) + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + DeletePolicy(context.Context, *connect.Request[v1.DeletePolicyReq]) (*connect.Response[v1.DeletePolicyResp], error) + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + GetPolicy(context.Context, *connect.Request[v1.GetPolicyReq]) (*connect.Response[v1.GetPolicyResp], error) + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + ListPolicies(context.Context, *connect.Request[v1.ListPoliciesReq]) (*connect.Response[v1.ListPoliciesResp], error) + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + AttachPolicy(context.Context, *connect.Request[v1.AttachPolicyReq]) (*connect.Response[v1.AttachPolicyResp], error) + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + DetachPolicy(context.Context, *connect.Request[v1.DetachPolicyReq]) (*connect.Response[v1.DetachPolicyResp], error) + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + ListPoliciesForUser(context.Context, *connect.Request[v1.ListPoliciesForUserReq]) (*connect.Response[v1.ListPoliciesForUserResp], error) +} + +// NewPolicyServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewPolicyServiceHandler(svc PolicyServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + policyServiceMethods := v1.File_within_website_x_iam_v1_policy_proto.Services().ByName("PolicyService").Methods() + policyServiceCreatePolicyHandler := connect.NewUnaryHandler( + PolicyServiceCreatePolicyProcedure, + svc.CreatePolicy, + connect.WithSchema(policyServiceMethods.ByName("CreatePolicy")), + connect.WithHandlerOptions(opts...), + ) + policyServiceDeletePolicyHandler := connect.NewUnaryHandler( + PolicyServiceDeletePolicyProcedure, + svc.DeletePolicy, + connect.WithSchema(policyServiceMethods.ByName("DeletePolicy")), + connect.WithHandlerOptions(opts...), + ) + policyServiceGetPolicyHandler := connect.NewUnaryHandler( + PolicyServiceGetPolicyProcedure, + svc.GetPolicy, + connect.WithSchema(policyServiceMethods.ByName("GetPolicy")), + connect.WithHandlerOptions(opts...), + ) + policyServiceListPoliciesHandler := connect.NewUnaryHandler( + PolicyServiceListPoliciesProcedure, + svc.ListPolicies, + connect.WithSchema(policyServiceMethods.ByName("ListPolicies")), + connect.WithHandlerOptions(opts...), + ) + policyServiceAttachPolicyHandler := connect.NewUnaryHandler( + PolicyServiceAttachPolicyProcedure, + svc.AttachPolicy, + connect.WithSchema(policyServiceMethods.ByName("AttachPolicy")), + connect.WithHandlerOptions(opts...), + ) + policyServiceDetachPolicyHandler := connect.NewUnaryHandler( + PolicyServiceDetachPolicyProcedure, + svc.DetachPolicy, + connect.WithSchema(policyServiceMethods.ByName("DetachPolicy")), + connect.WithHandlerOptions(opts...), + ) + policyServiceListPoliciesForUserHandler := connect.NewUnaryHandler( + PolicyServiceListPoliciesForUserProcedure, + svc.ListPoliciesForUser, + connect.WithSchema(policyServiceMethods.ByName("ListPoliciesForUser")), + connect.WithHandlerOptions(opts...), + ) + return "/within.website.x.iam.v1.PolicyService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case PolicyServiceCreatePolicyProcedure: + policyServiceCreatePolicyHandler.ServeHTTP(w, r) + case PolicyServiceDeletePolicyProcedure: + policyServiceDeletePolicyHandler.ServeHTTP(w, r) + case PolicyServiceGetPolicyProcedure: + policyServiceGetPolicyHandler.ServeHTTP(w, r) + case PolicyServiceListPoliciesProcedure: + policyServiceListPoliciesHandler.ServeHTTP(w, r) + case PolicyServiceAttachPolicyProcedure: + policyServiceAttachPolicyHandler.ServeHTTP(w, r) + case PolicyServiceDetachPolicyProcedure: + policyServiceDetachPolicyHandler.ServeHTTP(w, r) + case PolicyServiceListPoliciesForUserProcedure: + policyServiceListPoliciesForUserHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedPolicyServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedPolicyServiceHandler struct{} + +func (UnimplementedPolicyServiceHandler) CreatePolicy(context.Context, *connect.Request[v1.CreatePolicyReq]) (*connect.Response[v1.CreatePolicyResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.CreatePolicy is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) DeletePolicy(context.Context, *connect.Request[v1.DeletePolicyReq]) (*connect.Response[v1.DeletePolicyResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.DeletePolicy is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) GetPolicy(context.Context, *connect.Request[v1.GetPolicyReq]) (*connect.Response[v1.GetPolicyResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.GetPolicy is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) ListPolicies(context.Context, *connect.Request[v1.ListPoliciesReq]) (*connect.Response[v1.ListPoliciesResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.ListPolicies is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) AttachPolicy(context.Context, *connect.Request[v1.AttachPolicyReq]) (*connect.Response[v1.AttachPolicyResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.AttachPolicy is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) DetachPolicy(context.Context, *connect.Request[v1.DetachPolicyReq]) (*connect.Response[v1.DetachPolicyResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.DetachPolicy is not implemented")) +} + +func (UnimplementedPolicyServiceHandler) ListPoliciesForUser(context.Context, *connect.Request[v1.ListPoliciesForUserReq]) (*connect.Response[v1.ListPoliciesForUserResp], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("within.website.x.iam.v1.PolicyService.ListPoliciesForUser is not implemented")) +} diff --git a/gen/within/website/x/iam/v1/policy.pb.go b/gen/within/website/x/iam/v1/policy.pb.go new file mode 100644 index 00000000..74f27b75 --- /dev/null +++ b/gen/within/website/x/iam/v1/policy.pb.go @@ -0,0 +1,990 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: within/website/x/iam/v1/policy.proto + +package iamv1 + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +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) +) + +// Effect is what a statement does when it applies. +type Effect int32 + +const ( + Effect_EFFECT_UNSPECIFIED Effect = 0 + Effect_EFFECT_ALLOW Effect = 1 + Effect_EFFECT_DENY Effect = 2 +) + +// Enum value maps for Effect. +var ( + Effect_name = map[int32]string{ + 0: "EFFECT_UNSPECIFIED", + 1: "EFFECT_ALLOW", + 2: "EFFECT_DENY", + } + Effect_value = map[string]int32{ + "EFFECT_UNSPECIFIED": 0, + "EFFECT_ALLOW": 1, + "EFFECT_DENY": 2, + } +) + +func (x Effect) Enum() *Effect { + p := new(Effect) + *p = x + return p +} + +func (x Effect) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Effect) Descriptor() protoreflect.EnumDescriptor { + return file_within_website_x_iam_v1_policy_proto_enumTypes[0].Descriptor() +} + +func (Effect) Type() protoreflect.EnumType { + return &file_within_website_x_iam_v1_policy_proto_enumTypes[0] +} + +func (x Effect) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Effect.Descriptor instead. +func (Effect) EnumDescriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{0} +} + +// PolicyStatement is one allow or deny rule. +type PolicyStatement struct { + state protoimpl.MessageState `protogen:"open.v1"` + Effect Effect `protobuf:"varint,1,opt,name=effect,proto3,enum=within.website.x.iam.v1.Effect" json:"effect,omitempty"` + // Anchored glob patterns against full method names; at least one required. + Methods []string `protobuf:"bytes,2,rep,name=methods,proto3" json:"methods,omitempty"` + // Optional CEL filter over (method, req, principal). Empty means true. + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PolicyStatement) Reset() { + *x = PolicyStatement{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PolicyStatement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyStatement) ProtoMessage() {} + +func (x *PolicyStatement) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 PolicyStatement.ProtoReflect.Descriptor instead. +func (*PolicyStatement) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{0} +} + +func (x *PolicyStatement) GetEffect() Effect { + if x != nil { + return x.Effect + } + return Effect_EFFECT_UNSPECIFIED +} + +func (x *PolicyStatement) GetMethods() []string { + if x != nil { + return x.Methods + } + return nil +} + +func (x *PolicyStatement) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +// Policy is a named, reusable document. Users reference policies by id; +// editing a policy document is not supported after creation (attach a new +// one and detach the old), so a compiled snapshot always matches what an +// enforcing service fetched. +type Policy struct { + state protoimpl.MessageState `protogen:"open.v1"` + // UUID version 7 (time-ordered), like User.id. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Human-readable name, unique across the deployment. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Statements []*PolicyStatement `protobuf:"bytes,3,rep,name=statements,proto3" json:"statements,omitempty"` + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Policy) Reset() { + *x = Policy{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Policy) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Policy) ProtoMessage() {} + +func (x *Policy) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 Policy.ProtoReflect.Descriptor instead. +func (*Policy) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{1} +} + +func (x *Policy) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Policy) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Policy) GetStatements() []*PolicyStatement { + if x != nil { + return x.Statements + } + return nil +} + +func (x *Policy) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *Policy) GetUpdatedAt() *timestamppb.Timestamp { + if x != nil { + return x.UpdatedAt + } + return nil +} + +type CreatePolicyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policy *Policy `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePolicyReq) Reset() { + *x = CreatePolicyReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePolicyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePolicyReq) ProtoMessage() {} + +func (x *CreatePolicyReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 CreatePolicyReq.ProtoReflect.Descriptor instead. +func (*CreatePolicyReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{2} +} + +func (x *CreatePolicyReq) GetPolicy() *Policy { + if x != nil { + return x.Policy + } + return nil +} + +type CreatePolicyResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policy *Policy `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePolicyResp) Reset() { + *x = CreatePolicyResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePolicyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePolicyResp) ProtoMessage() {} + +func (x *CreatePolicyResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 CreatePolicyResp.ProtoReflect.Descriptor instead. +func (*CreatePolicyResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{3} +} + +func (x *CreatePolicyResp) GetPolicy() *Policy { + if x != nil { + return x.Policy + } + return nil +} + +type DeletePolicyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + PolicyId string `protobuf:"bytes,1,opt,name=policy_id,json=policyId,proto3" json:"policy_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePolicyReq) Reset() { + *x = DeletePolicyReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePolicyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePolicyReq) ProtoMessage() {} + +func (x *DeletePolicyReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 DeletePolicyReq.ProtoReflect.Descriptor instead. +func (*DeletePolicyReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{4} +} + +func (x *DeletePolicyReq) GetPolicyId() string { + if x != nil { + return x.PolicyId + } + return "" +} + +type DeletePolicyResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePolicyResp) Reset() { + *x = DeletePolicyResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePolicyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePolicyResp) ProtoMessage() {} + +func (x *DeletePolicyResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 DeletePolicyResp.ProtoReflect.Descriptor instead. +func (*DeletePolicyResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{5} +} + +type GetPolicyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + PolicyId string `protobuf:"bytes,1,opt,name=policy_id,json=policyId,proto3" json:"policy_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPolicyReq) Reset() { + *x = GetPolicyReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPolicyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyReq) ProtoMessage() {} + +func (x *GetPolicyReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 GetPolicyReq.ProtoReflect.Descriptor instead. +func (*GetPolicyReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{6} +} + +func (x *GetPolicyReq) GetPolicyId() string { + if x != nil { + return x.PolicyId + } + return "" +} + +type GetPolicyResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policy *Policy `protobuf:"bytes,1,opt,name=policy,proto3" json:"policy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPolicyResp) Reset() { + *x = GetPolicyResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPolicyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPolicyResp) ProtoMessage() {} + +func (x *GetPolicyResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 GetPolicyResp.ProtoReflect.Descriptor instead. +func (*GetPolicyResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{7} +} + +func (x *GetPolicyResp) GetPolicy() *Policy { + if x != nil { + return x.Policy + } + return nil +} + +type ListPoliciesReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + Count int32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"` + Page int32 `protobuf:"varint,2,opt,name=page,proto3" json:"page,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPoliciesReq) Reset() { + *x = ListPoliciesReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPoliciesReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPoliciesReq) ProtoMessage() {} + +func (x *ListPoliciesReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 ListPoliciesReq.ProtoReflect.Descriptor instead. +func (*ListPoliciesReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{8} +} + +func (x *ListPoliciesReq) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *ListPoliciesReq) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +type ListPoliciesResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policies []*Policy `protobuf:"bytes,1,rep,name=policies,proto3" json:"policies,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPoliciesResp) Reset() { + *x = ListPoliciesResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPoliciesResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPoliciesResp) ProtoMessage() {} + +func (x *ListPoliciesResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 ListPoliciesResp.ProtoReflect.Descriptor instead. +func (*ListPoliciesResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{9} +} + +func (x *ListPoliciesResp) GetPolicies() []*Policy { + if x != nil { + return x.Policies + } + return nil +} + +type AttachPolicyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + PolicyId string `protobuf:"bytes,1,opt,name=policy_id,json=policyId,proto3" json:"policy_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachPolicyReq) Reset() { + *x = AttachPolicyReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachPolicyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachPolicyReq) ProtoMessage() {} + +func (x *AttachPolicyReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_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 AttachPolicyReq.ProtoReflect.Descriptor instead. +func (*AttachPolicyReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{10} +} + +func (x *AttachPolicyReq) GetPolicyId() string { + if x != nil { + return x.PolicyId + } + return "" +} + +func (x *AttachPolicyReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type AttachPolicyResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachPolicyResp) Reset() { + *x = AttachPolicyResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachPolicyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachPolicyResp) ProtoMessage() {} + +func (x *AttachPolicyResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[11] + 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 AttachPolicyResp.ProtoReflect.Descriptor instead. +func (*AttachPolicyResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{11} +} + +type DetachPolicyReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + PolicyId string `protobuf:"bytes,1,opt,name=policy_id,json=policyId,proto3" json:"policy_id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachPolicyReq) Reset() { + *x = DetachPolicyReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachPolicyReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachPolicyReq) ProtoMessage() {} + +func (x *DetachPolicyReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[12] + 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 DetachPolicyReq.ProtoReflect.Descriptor instead. +func (*DetachPolicyReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{12} +} + +func (x *DetachPolicyReq) GetPolicyId() string { + if x != nil { + return x.PolicyId + } + return "" +} + +func (x *DetachPolicyReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type DetachPolicyResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DetachPolicyResp) Reset() { + *x = DetachPolicyResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DetachPolicyResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetachPolicyResp) ProtoMessage() {} + +func (x *DetachPolicyResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[13] + 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 DetachPolicyResp.ProtoReflect.Descriptor instead. +func (*DetachPolicyResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{13} +} + +type ListPoliciesForUserReq struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPoliciesForUserReq) Reset() { + *x = ListPoliciesForUserReq{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPoliciesForUserReq) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPoliciesForUserReq) ProtoMessage() {} + +func (x *ListPoliciesForUserReq) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[14] + 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 ListPoliciesForUserReq.ProtoReflect.Descriptor instead. +func (*ListPoliciesForUserReq) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{14} +} + +func (x *ListPoliciesForUserReq) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type ListPoliciesForUserResp struct { + state protoimpl.MessageState `protogen:"open.v1"` + Policies []*Policy `protobuf:"bytes,1,rep,name=policies,proto3" json:"policies,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPoliciesForUserResp) Reset() { + *x = ListPoliciesForUserResp{} + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPoliciesForUserResp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPoliciesForUserResp) ProtoMessage() {} + +func (x *ListPoliciesForUserResp) ProtoReflect() protoreflect.Message { + mi := &file_within_website_x_iam_v1_policy_proto_msgTypes[15] + 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 ListPoliciesForUserResp.ProtoReflect.Descriptor instead. +func (*ListPoliciesForUserResp) Descriptor() ([]byte, []int) { + return file_within_website_x_iam_v1_policy_proto_rawDescGZIP(), []int{15} +} + +func (x *ListPoliciesForUserResp) GetPolicies() []*Policy { + if x != nil { + return x.Policies + } + return nil +} + +var File_within_website_x_iam_v1_policy_proto protoreflect.FileDescriptor + +const file_within_website_x_iam_v1_policy_proto_rawDesc = "" + + "\n" + + "$within/website/x/iam/v1/policy.proto\x12\x17within.website.x.iam.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9a\x01\n" + + "\x0fPolicyStatement\x12C\n" + + "\x06effect\x18\x01 \x01(\x0e2\x1f.within.website.x.iam.v1.EffectB\n" + + "\xbaH\a\x82\x01\x04\x10\x01 \x00R\x06effect\x12*\n" + + "\amethods\x18\x02 \x03(\tB\x10\xbaH\r\x92\x01\n" + + "\b\x01\x18\x01\"\x04r\x02\x10\x01R\amethods\x12\x16\n" + + "\x06filter\x18\x03 \x01(\tR\x06filter\"\x82\x02\n" + + "\x06Policy\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1e\n" + + "\x04name\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x01R\x04name\x12R\n" + + "\n" + + "statements\x18\x03 \x03(\v2(.within.website.x.iam.v1.PolicyStatementB\b\xbaH\x05\x92\x01\x02\b\x01R\n" + + "statements\x129\n" + + "\n" + + "created_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + + "\n" + + "updated_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"R\n" + + "\x0fCreatePolicyReq\x12?\n" + + "\x06policy\x18\x01 \x01(\v2\x1f.within.website.x.iam.v1.PolicyB\x06\xbaH\x03\xc8\x01\x01R\x06policy\"K\n" + + "\x10CreatePolicyResp\x127\n" + + "\x06policy\x18\x01 \x01(\v2\x1f.within.website.x.iam.v1.PolicyR\x06policy\"6\n" + + "\x0fDeletePolicyReq\x12#\n" + + "\tpolicy_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bpolicyId\"\x12\n" + + "\x10DeletePolicyResp\"3\n" + + "\fGetPolicyReq\x12#\n" + + "\tpolicy_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bpolicyId\"H\n" + + "\rGetPolicyResp\x127\n" + + "\x06policy\x18\x01 \x01(\v2\x1f.within.website.x.iam.v1.PolicyR\x06policy\"C\n" + + "\x0fListPoliciesReq\x12\x1c\n" + + "\x05count\x18\x01 \x01(\x05B\x06\xbaH\x03\xc8\x01\x01R\x05count\x12\x12\n" + + "\x04page\x18\x02 \x01(\x05R\x04page\"O\n" + + "\x10ListPoliciesResp\x12;\n" + + "\bpolicies\x18\x01 \x03(\v2\x1f.within.website.x.iam.v1.PolicyR\bpolicies\"W\n" + + "\x0fAttachPolicyReq\x12#\n" + + "\tpolicy_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bpolicyId\x12\x1f\n" + + "\auser_id\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06userId\"\x12\n" + + "\x10AttachPolicyResp\"W\n" + + "\x0fDetachPolicyReq\x12#\n" + + "\tpolicy_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\bpolicyId\x12\x1f\n" + + "\auser_id\x18\x02 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06userId\"\x12\n" + + "\x10DetachPolicyResp\"9\n" + + "\x16ListPoliciesForUserReq\x12\x1f\n" + + "\auser_id\x18\x01 \x01(\tB\x06\xbaH\x03\xc8\x01\x01R\x06userId\"V\n" + + "\x17ListPoliciesForUserResp\x12;\n" + + "\bpolicies\x18\x01 \x03(\v2\x1f.within.website.x.iam.v1.PolicyR\bpolicies*C\n" + + "\x06Effect\x12\x16\n" + + "\x12EFFECT_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fEFFECT_ALLOW\x10\x01\x12\x0f\n" + + "\vEFFECT_DENY\x10\x022\xde\x05\n" + + "\rPolicyService\x12c\n" + + "\fCreatePolicy\x12(.within.website.x.iam.v1.CreatePolicyReq\x1a).within.website.x.iam.v1.CreatePolicyResp\x12c\n" + + "\fDeletePolicy\x12(.within.website.x.iam.v1.DeletePolicyReq\x1a).within.website.x.iam.v1.DeletePolicyResp\x12Z\n" + + "\tGetPolicy\x12%.within.website.x.iam.v1.GetPolicyReq\x1a&.within.website.x.iam.v1.GetPolicyResp\x12c\n" + + "\fListPolicies\x12(.within.website.x.iam.v1.ListPoliciesReq\x1a).within.website.x.iam.v1.ListPoliciesResp\x12c\n" + + "\fAttachPolicy\x12(.within.website.x.iam.v1.AttachPolicyReq\x1a).within.website.x.iam.v1.AttachPolicyResp\x12c\n" + + "\fDetachPolicy\x12(.within.website.x.iam.v1.DetachPolicyReq\x1a).within.website.x.iam.v1.DetachPolicyResp\x12x\n" + + "\x13ListPoliciesForUser\x12/.within.website.x.iam.v1.ListPoliciesForUserReq\x1a0.within.website.x.iam.v1.ListPoliciesForUserRespB\xdf\x01\n" + + "\x1bcom.within.website.x.iam.v1B\vPolicyProtoP\x01Z2within.website/x/gen/within/website/x/iam/v1;iamv1\xa2\x02\x04WWXI\xaa\x02\x17Within.Website.X.Iam.V1\xca\x02\x17Within\\Website\\X\\Iam\\V1\xe2\x02#Within\\Website\\X\\Iam\\V1\\GPBMetadata\xea\x02\x1bWithin::Website::X::Iam::V1b\x06proto3" + +var ( + file_within_website_x_iam_v1_policy_proto_rawDescOnce sync.Once + file_within_website_x_iam_v1_policy_proto_rawDescData []byte +) + +func file_within_website_x_iam_v1_policy_proto_rawDescGZIP() []byte { + file_within_website_x_iam_v1_policy_proto_rawDescOnce.Do(func() { + file_within_website_x_iam_v1_policy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_within_website_x_iam_v1_policy_proto_rawDesc), len(file_within_website_x_iam_v1_policy_proto_rawDesc))) + }) + return file_within_website_x_iam_v1_policy_proto_rawDescData +} + +var file_within_website_x_iam_v1_policy_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_within_website_x_iam_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_within_website_x_iam_v1_policy_proto_goTypes = []any{ + (Effect)(0), // 0: within.website.x.iam.v1.Effect + (*PolicyStatement)(nil), // 1: within.website.x.iam.v1.PolicyStatement + (*Policy)(nil), // 2: within.website.x.iam.v1.Policy + (*CreatePolicyReq)(nil), // 3: within.website.x.iam.v1.CreatePolicyReq + (*CreatePolicyResp)(nil), // 4: within.website.x.iam.v1.CreatePolicyResp + (*DeletePolicyReq)(nil), // 5: within.website.x.iam.v1.DeletePolicyReq + (*DeletePolicyResp)(nil), // 6: within.website.x.iam.v1.DeletePolicyResp + (*GetPolicyReq)(nil), // 7: within.website.x.iam.v1.GetPolicyReq + (*GetPolicyResp)(nil), // 8: within.website.x.iam.v1.GetPolicyResp + (*ListPoliciesReq)(nil), // 9: within.website.x.iam.v1.ListPoliciesReq + (*ListPoliciesResp)(nil), // 10: within.website.x.iam.v1.ListPoliciesResp + (*AttachPolicyReq)(nil), // 11: within.website.x.iam.v1.AttachPolicyReq + (*AttachPolicyResp)(nil), // 12: within.website.x.iam.v1.AttachPolicyResp + (*DetachPolicyReq)(nil), // 13: within.website.x.iam.v1.DetachPolicyReq + (*DetachPolicyResp)(nil), // 14: within.website.x.iam.v1.DetachPolicyResp + (*ListPoliciesForUserReq)(nil), // 15: within.website.x.iam.v1.ListPoliciesForUserReq + (*ListPoliciesForUserResp)(nil), // 16: within.website.x.iam.v1.ListPoliciesForUserResp + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp +} +var file_within_website_x_iam_v1_policy_proto_depIdxs = []int32{ + 0, // 0: within.website.x.iam.v1.PolicyStatement.effect:type_name -> within.website.x.iam.v1.Effect + 1, // 1: within.website.x.iam.v1.Policy.statements:type_name -> within.website.x.iam.v1.PolicyStatement + 17, // 2: within.website.x.iam.v1.Policy.created_at:type_name -> google.protobuf.Timestamp + 17, // 3: within.website.x.iam.v1.Policy.updated_at:type_name -> google.protobuf.Timestamp + 2, // 4: within.website.x.iam.v1.CreatePolicyReq.policy:type_name -> within.website.x.iam.v1.Policy + 2, // 5: within.website.x.iam.v1.CreatePolicyResp.policy:type_name -> within.website.x.iam.v1.Policy + 2, // 6: within.website.x.iam.v1.GetPolicyResp.policy:type_name -> within.website.x.iam.v1.Policy + 2, // 7: within.website.x.iam.v1.ListPoliciesResp.policies:type_name -> within.website.x.iam.v1.Policy + 2, // 8: within.website.x.iam.v1.ListPoliciesForUserResp.policies:type_name -> within.website.x.iam.v1.Policy + 3, // 9: within.website.x.iam.v1.PolicyService.CreatePolicy:input_type -> within.website.x.iam.v1.CreatePolicyReq + 5, // 10: within.website.x.iam.v1.PolicyService.DeletePolicy:input_type -> within.website.x.iam.v1.DeletePolicyReq + 7, // 11: within.website.x.iam.v1.PolicyService.GetPolicy:input_type -> within.website.x.iam.v1.GetPolicyReq + 9, // 12: within.website.x.iam.v1.PolicyService.ListPolicies:input_type -> within.website.x.iam.v1.ListPoliciesReq + 11, // 13: within.website.x.iam.v1.PolicyService.AttachPolicy:input_type -> within.website.x.iam.v1.AttachPolicyReq + 13, // 14: within.website.x.iam.v1.PolicyService.DetachPolicy:input_type -> within.website.x.iam.v1.DetachPolicyReq + 15, // 15: within.website.x.iam.v1.PolicyService.ListPoliciesForUser:input_type -> within.website.x.iam.v1.ListPoliciesForUserReq + 4, // 16: within.website.x.iam.v1.PolicyService.CreatePolicy:output_type -> within.website.x.iam.v1.CreatePolicyResp + 6, // 17: within.website.x.iam.v1.PolicyService.DeletePolicy:output_type -> within.website.x.iam.v1.DeletePolicyResp + 8, // 18: within.website.x.iam.v1.PolicyService.GetPolicy:output_type -> within.website.x.iam.v1.GetPolicyResp + 10, // 19: within.website.x.iam.v1.PolicyService.ListPolicies:output_type -> within.website.x.iam.v1.ListPoliciesResp + 12, // 20: within.website.x.iam.v1.PolicyService.AttachPolicy:output_type -> within.website.x.iam.v1.AttachPolicyResp + 14, // 21: within.website.x.iam.v1.PolicyService.DetachPolicy:output_type -> within.website.x.iam.v1.DetachPolicyResp + 16, // 22: within.website.x.iam.v1.PolicyService.ListPoliciesForUser:output_type -> within.website.x.iam.v1.ListPoliciesForUserResp + 16, // [16:23] is the sub-list for method output_type + 9, // [9:16] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name +} + +func init() { file_within_website_x_iam_v1_policy_proto_init() } +func file_within_website_x_iam_v1_policy_proto_init() { + if File_within_website_x_iam_v1_policy_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_within_website_x_iam_v1_policy_proto_rawDesc), len(file_within_website_x_iam_v1_policy_proto_rawDesc)), + NumEnums: 1, + NumMessages: 16, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_within_website_x_iam_v1_policy_proto_goTypes, + DependencyIndexes: file_within_website_x_iam_v1_policy_proto_depIdxs, + EnumInfos: file_within_website_x_iam_v1_policy_proto_enumTypes, + MessageInfos: file_within_website_x_iam_v1_policy_proto_msgTypes, + }.Build() + File_within_website_x_iam_v1_policy_proto = out.File + file_within_website_x_iam_v1_policy_proto_goTypes = nil + file_within_website_x_iam_v1_policy_proto_depIdxs = nil +} diff --git a/gen/within/website/x/iam/v1/policy.twirp.go b/gen/within/website/x/iam/v1/policy.twirp.go new file mode 100644 index 00000000..e3849dd3 --- /dev/null +++ b/gen/within/website/x/iam/v1/policy.twirp.go @@ -0,0 +1,2304 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: within/website/x/iam/v1/policy.proto + +package iamv1 + +import ( + context "context" + fmt "fmt" + + http "net/http" + + io "io" + + json "encoding/json" + + strconv "strconv" + + strings "strings" + + protojson "google.golang.org/protobuf/encoding/protojson" + + proto "google.golang.org/protobuf/proto" + + twirp "github.com/twitchtv/twirp" + + ctxsetters "github.com/twitchtv/twirp/ctxsetters" +) + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// ======================= +// PolicyService Interface +// ======================= + +// PolicyService manages policy documents and their attachment to users. +// Every method requires an admin caller; policy administration is itself +// gated by is_admin, never by policies, so there is no way to lock yourself +// out of the management surface. +type PolicyService interface { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + CreatePolicy(context.Context, *CreatePolicyReq) (*CreatePolicyResp, error) + + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + DeletePolicy(context.Context, *DeletePolicyReq) (*DeletePolicyResp, error) + + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + GetPolicy(context.Context, *GetPolicyReq) (*GetPolicyResp, error) + + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + ListPolicies(context.Context, *ListPoliciesReq) (*ListPoliciesResp, error) + + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + AttachPolicy(context.Context, *AttachPolicyReq) (*AttachPolicyResp, error) + + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + DetachPolicy(context.Context, *DetachPolicyReq) (*DetachPolicyResp, error) + + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + ListPoliciesForUser(context.Context, *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) +} + +// ============================= +// PolicyService Protobuf Client +// ============================= + +type policyServiceProtobufClient struct { + client HTTPClient + urls [7]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewPolicyServiceProtobufClient creates a Protobuf client that implements the PolicyService interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewPolicyServiceProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) PolicyService { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "within.website.x.iam.v1", "PolicyService") + urls := [7]string{ + serviceURL + "CreatePolicy", + serviceURL + "DeletePolicy", + serviceURL + "GetPolicy", + serviceURL + "ListPolicies", + serviceURL + "AttachPolicy", + serviceURL + "DetachPolicy", + serviceURL + "ListPoliciesForUser", + } + + return &policyServiceProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *policyServiceProtobufClient) CreatePolicy(ctx context.Context, in *CreatePolicyReq) (*CreatePolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "CreatePolicy") + caller := c.callCreatePolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *CreatePolicyReq) (*CreatePolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*CreatePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*CreatePolicyReq) when calling interceptor") + } + return c.callCreatePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*CreatePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*CreatePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callCreatePolicy(ctx context.Context, in *CreatePolicyReq) (*CreatePolicyResp, error) { + out := new(CreatePolicyResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) DeletePolicy(ctx context.Context, in *DeletePolicyReq) (*DeletePolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "DeletePolicy") + caller := c.callDeletePolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *DeletePolicyReq) (*DeletePolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeletePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeletePolicyReq) when calling interceptor") + } + return c.callDeletePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DeletePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DeletePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callDeletePolicy(ctx context.Context, in *DeletePolicyReq) (*DeletePolicyResp, error) { + out := new(DeletePolicyResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) GetPolicy(ctx context.Context, in *GetPolicyReq) (*GetPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "GetPolicy") + caller := c.callGetPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *GetPolicyReq) (*GetPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*GetPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*GetPolicyReq) when calling interceptor") + } + return c.callGetPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*GetPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*GetPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callGetPolicy(ctx context.Context, in *GetPolicyReq) (*GetPolicyResp, error) { + out := new(GetPolicyResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[2], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) ListPolicies(ctx context.Context, in *ListPoliciesReq) (*ListPoliciesResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "ListPolicies") + caller := c.callListPolicies + if c.interceptor != nil { + caller = func(ctx context.Context, req *ListPoliciesReq) (*ListPoliciesResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesReq) when calling interceptor") + } + return c.callListPolicies(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callListPolicies(ctx context.Context, in *ListPoliciesReq) (*ListPoliciesResp, error) { + out := new(ListPoliciesResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[3], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) AttachPolicy(ctx context.Context, in *AttachPolicyReq) (*AttachPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "AttachPolicy") + caller := c.callAttachPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *AttachPolicyReq) (*AttachPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*AttachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*AttachPolicyReq) when calling interceptor") + } + return c.callAttachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*AttachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*AttachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callAttachPolicy(ctx context.Context, in *AttachPolicyReq) (*AttachPolicyResp, error) { + out := new(AttachPolicyResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[4], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) DetachPolicy(ctx context.Context, in *DetachPolicyReq) (*DetachPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "DetachPolicy") + caller := c.callDetachPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *DetachPolicyReq) (*DetachPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DetachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DetachPolicyReq) when calling interceptor") + } + return c.callDetachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DetachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DetachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callDetachPolicy(ctx context.Context, in *DetachPolicyReq) (*DetachPolicyResp, error) { + out := new(DetachPolicyResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[5], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceProtobufClient) ListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "ListPoliciesForUser") + caller := c.callListPoliciesForUser + if c.interceptor != nil { + caller = func(ctx context.Context, req *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesForUserReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesForUserReq) when calling interceptor") + } + return c.callListPoliciesForUser(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesForUserResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesForUserResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceProtobufClient) callListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + out := new(ListPoliciesForUserResp) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[6], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ========================= +// PolicyService JSON Client +// ========================= + +type policyServiceJSONClient struct { + client HTTPClient + urls [7]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewPolicyServiceJSONClient creates a JSON client that implements the PolicyService interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewPolicyServiceJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) PolicyService { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "within.website.x.iam.v1", "PolicyService") + urls := [7]string{ + serviceURL + "CreatePolicy", + serviceURL + "DeletePolicy", + serviceURL + "GetPolicy", + serviceURL + "ListPolicies", + serviceURL + "AttachPolicy", + serviceURL + "DetachPolicy", + serviceURL + "ListPoliciesForUser", + } + + return &policyServiceJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *policyServiceJSONClient) CreatePolicy(ctx context.Context, in *CreatePolicyReq) (*CreatePolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "CreatePolicy") + caller := c.callCreatePolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *CreatePolicyReq) (*CreatePolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*CreatePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*CreatePolicyReq) when calling interceptor") + } + return c.callCreatePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*CreatePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*CreatePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callCreatePolicy(ctx context.Context, in *CreatePolicyReq) (*CreatePolicyResp, error) { + out := new(CreatePolicyResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) DeletePolicy(ctx context.Context, in *DeletePolicyReq) (*DeletePolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "DeletePolicy") + caller := c.callDeletePolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *DeletePolicyReq) (*DeletePolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeletePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeletePolicyReq) when calling interceptor") + } + return c.callDeletePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DeletePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DeletePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callDeletePolicy(ctx context.Context, in *DeletePolicyReq) (*DeletePolicyResp, error) { + out := new(DeletePolicyResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) GetPolicy(ctx context.Context, in *GetPolicyReq) (*GetPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "GetPolicy") + caller := c.callGetPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *GetPolicyReq) (*GetPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*GetPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*GetPolicyReq) when calling interceptor") + } + return c.callGetPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*GetPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*GetPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callGetPolicy(ctx context.Context, in *GetPolicyReq) (*GetPolicyResp, error) { + out := new(GetPolicyResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[2], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) ListPolicies(ctx context.Context, in *ListPoliciesReq) (*ListPoliciesResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "ListPolicies") + caller := c.callListPolicies + if c.interceptor != nil { + caller = func(ctx context.Context, req *ListPoliciesReq) (*ListPoliciesResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesReq) when calling interceptor") + } + return c.callListPolicies(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callListPolicies(ctx context.Context, in *ListPoliciesReq) (*ListPoliciesResp, error) { + out := new(ListPoliciesResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[3], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) AttachPolicy(ctx context.Context, in *AttachPolicyReq) (*AttachPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "AttachPolicy") + caller := c.callAttachPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *AttachPolicyReq) (*AttachPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*AttachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*AttachPolicyReq) when calling interceptor") + } + return c.callAttachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*AttachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*AttachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callAttachPolicy(ctx context.Context, in *AttachPolicyReq) (*AttachPolicyResp, error) { + out := new(AttachPolicyResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[4], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) DetachPolicy(ctx context.Context, in *DetachPolicyReq) (*DetachPolicyResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "DetachPolicy") + caller := c.callDetachPolicy + if c.interceptor != nil { + caller = func(ctx context.Context, req *DetachPolicyReq) (*DetachPolicyResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DetachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DetachPolicyReq) when calling interceptor") + } + return c.callDetachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DetachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DetachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callDetachPolicy(ctx context.Context, in *DetachPolicyReq) (*DetachPolicyResp, error) { + out := new(DetachPolicyResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[5], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *policyServiceJSONClient) ListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithMethodName(ctx, "ListPoliciesForUser") + caller := c.callListPoliciesForUser + if c.interceptor != nil { + caller = func(ctx context.Context, req *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesForUserReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesForUserReq) when calling interceptor") + } + return c.callListPoliciesForUser(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesForUserResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesForUserResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *policyServiceJSONClient) callListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + out := new(ListPoliciesForUserResp) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[6], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ============================ +// PolicyService Server Handler +// ============================ + +type policyServiceServer struct { + PolicyService + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewPolicyServiceServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewPolicyServiceServer(svc PolicyService, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &policyServiceServer{ + PolicyService: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *policyServiceServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *policyServiceServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// PolicyServicePathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const PolicyServicePathPrefix = "/twirp/within.website.x.iam.v1.PolicyService/" + +func (s *policyServiceServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "within.website.x.iam.v1") + ctx = ctxsetters.WithServiceName(ctx, "PolicyService") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: []/./ + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "within.website.x.iam.v1.PolicyService" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "CreatePolicy": + s.serveCreatePolicy(ctx, resp, req) + return + case "DeletePolicy": + s.serveDeletePolicy(ctx, resp, req) + return + case "GetPolicy": + s.serveGetPolicy(ctx, resp, req) + return + case "ListPolicies": + s.serveListPolicies(ctx, resp, req) + return + case "AttachPolicy": + s.serveAttachPolicy(ctx, resp, req) + return + case "DetachPolicy": + s.serveDetachPolicy(ctx, resp, req) + return + case "ListPoliciesForUser": + s.serveListPoliciesForUser(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *policyServiceServer) serveCreatePolicy(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveCreatePolicyJSON(ctx, resp, req) + case "application/protobuf": + s.serveCreatePolicyProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveCreatePolicyJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "CreatePolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(CreatePolicyReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.CreatePolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *CreatePolicyReq) (*CreatePolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*CreatePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*CreatePolicyReq) when calling interceptor") + } + return s.PolicyService.CreatePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*CreatePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*CreatePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *CreatePolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *CreatePolicyResp and nil error while calling CreatePolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveCreatePolicyProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "CreatePolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(CreatePolicyReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.CreatePolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *CreatePolicyReq) (*CreatePolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*CreatePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*CreatePolicyReq) when calling interceptor") + } + return s.PolicyService.CreatePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*CreatePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*CreatePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *CreatePolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *CreatePolicyResp and nil error while calling CreatePolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveDeletePolicy(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveDeletePolicyJSON(ctx, resp, req) + case "application/protobuf": + s.serveDeletePolicyProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveDeletePolicyJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DeletePolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(DeletePolicyReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.DeletePolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *DeletePolicyReq) (*DeletePolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeletePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeletePolicyReq) when calling interceptor") + } + return s.PolicyService.DeletePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DeletePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DeletePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *DeletePolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DeletePolicyResp and nil error while calling DeletePolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveDeletePolicyProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DeletePolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(DeletePolicyReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.DeletePolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *DeletePolicyReq) (*DeletePolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeletePolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeletePolicyReq) when calling interceptor") + } + return s.PolicyService.DeletePolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DeletePolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DeletePolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *DeletePolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DeletePolicyResp and nil error while calling DeletePolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveGetPolicy(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveGetPolicyJSON(ctx, resp, req) + case "application/protobuf": + s.serveGetPolicyProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveGetPolicyJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "GetPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(GetPolicyReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.GetPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *GetPolicyReq) (*GetPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*GetPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*GetPolicyReq) when calling interceptor") + } + return s.PolicyService.GetPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*GetPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*GetPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *GetPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *GetPolicyResp and nil error while calling GetPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveGetPolicyProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "GetPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(GetPolicyReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.GetPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *GetPolicyReq) (*GetPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*GetPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*GetPolicyReq) when calling interceptor") + } + return s.PolicyService.GetPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*GetPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*GetPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *GetPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *GetPolicyResp and nil error while calling GetPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveListPolicies(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveListPoliciesJSON(ctx, resp, req) + case "application/protobuf": + s.serveListPoliciesProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveListPoliciesJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ListPolicies") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(ListPoliciesReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.ListPolicies + if s.interceptor != nil { + handler = func(ctx context.Context, req *ListPoliciesReq) (*ListPoliciesResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesReq) when calling interceptor") + } + return s.PolicyService.ListPolicies(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ListPoliciesResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ListPoliciesResp and nil error while calling ListPolicies. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveListPoliciesProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ListPolicies") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(ListPoliciesReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.ListPolicies + if s.interceptor != nil { + handler = func(ctx context.Context, req *ListPoliciesReq) (*ListPoliciesResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesReq) when calling interceptor") + } + return s.PolicyService.ListPolicies(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ListPoliciesResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ListPoliciesResp and nil error while calling ListPolicies. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveAttachPolicy(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveAttachPolicyJSON(ctx, resp, req) + case "application/protobuf": + s.serveAttachPolicyProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveAttachPolicyJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "AttachPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(AttachPolicyReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.AttachPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *AttachPolicyReq) (*AttachPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*AttachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*AttachPolicyReq) when calling interceptor") + } + return s.PolicyService.AttachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*AttachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*AttachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *AttachPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *AttachPolicyResp and nil error while calling AttachPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveAttachPolicyProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "AttachPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(AttachPolicyReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.AttachPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *AttachPolicyReq) (*AttachPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*AttachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*AttachPolicyReq) when calling interceptor") + } + return s.PolicyService.AttachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*AttachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*AttachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *AttachPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *AttachPolicyResp and nil error while calling AttachPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveDetachPolicy(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveDetachPolicyJSON(ctx, resp, req) + case "application/protobuf": + s.serveDetachPolicyProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveDetachPolicyJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DetachPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(DetachPolicyReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.DetachPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *DetachPolicyReq) (*DetachPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DetachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DetachPolicyReq) when calling interceptor") + } + return s.PolicyService.DetachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DetachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DetachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *DetachPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetachPolicyResp and nil error while calling DetachPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveDetachPolicyProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DetachPolicy") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(DetachPolicyReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.DetachPolicy + if s.interceptor != nil { + handler = func(ctx context.Context, req *DetachPolicyReq) (*DetachPolicyResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DetachPolicyReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DetachPolicyReq) when calling interceptor") + } + return s.PolicyService.DetachPolicy(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*DetachPolicyResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*DetachPolicyResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *DetachPolicyResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *DetachPolicyResp and nil error while calling DetachPolicy. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveListPoliciesForUser(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveListPoliciesForUserJSON(ctx, resp, req) + case "application/protobuf": + s.serveListPoliciesForUserProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *policyServiceServer) serveListPoliciesForUserJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ListPoliciesForUser") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(ListPoliciesForUserReq) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.PolicyService.ListPoliciesForUser + if s.interceptor != nil { + handler = func(ctx context.Context, req *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesForUserReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesForUserReq) when calling interceptor") + } + return s.PolicyService.ListPoliciesForUser(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesForUserResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesForUserResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ListPoliciesForUserResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ListPoliciesForUserResp and nil error while calling ListPoliciesForUser. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) serveListPoliciesForUserProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "ListPoliciesForUser") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(ListPoliciesForUserReq) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.PolicyService.ListPoliciesForUser + if s.interceptor != nil { + handler = func(ctx context.Context, req *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ListPoliciesForUserReq) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ListPoliciesForUserReq) when calling interceptor") + } + return s.PolicyService.ListPoliciesForUser(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ListPoliciesForUserResp) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ListPoliciesForUserResp) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ListPoliciesForUserResp + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ListPoliciesForUserResp and nil error while calling ListPoliciesForUser. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *policyServiceServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor1, 0 +} + +func (s *policyServiceServer) ProtocGenTwirpVersion() string { + return "v8.1.3" +} + +// PathPrefix returns the base service path, in the form: "//./" +// that is everything in a Twirp route except for the . This can be used for routing, +// for example to identify the requests that are targeted to this service in a mux. +func (s *policyServiceServer) PathPrefix() string { + return baseServicePath(s.pathPrefix, "within.website.x.iam.v1", "PolicyService") +} + +var twirpFileDescriptor1 = []byte{ + // 855 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x56, 0x4d, 0x6e, 0xdb, 0x46, + 0x14, 0xce, 0x50, 0x3f, 0xb6, 0x9e, 0xec, 0x88, 0x98, 0x02, 0x36, 0x41, 0x17, 0xb5, 0xc0, 0xb4, + 0x85, 0x9c, 0x05, 0x59, 0x29, 0x40, 0x8b, 0x28, 0x8b, 0xc2, 0xfa, 0x71, 0x24, 0xd4, 0x4d, 0x04, + 0x26, 0xb1, 0xdc, 0x40, 0x80, 0x31, 0x16, 0xc7, 0xf6, 0x00, 0xa2, 0xc8, 0x72, 0xc6, 0x8a, 0xbb, + 0x2b, 0x72, 0x84, 0x2c, 0xbb, 0xec, 0xb2, 0x47, 0xf0, 0x09, 0x8a, 0x1e, 0xa2, 0x05, 0xba, 0xec, + 0x29, 0x0a, 0x72, 0x48, 0x95, 0x12, 0x2c, 0x97, 0x86, 0x91, 0x9d, 0xf8, 0xe6, 0xfb, 0x79, 0xf3, + 0x86, 0xdf, 0x50, 0xf0, 0xf9, 0x3b, 0x26, 0x2e, 0xd8, 0xd4, 0x7a, 0x47, 0x4f, 0x39, 0x13, 0xd4, + 0xba, 0xb2, 0x18, 0x71, 0xad, 0x59, 0xdd, 0xf2, 0xbd, 0x09, 0x1b, 0xff, 0x64, 0xfa, 0x81, 0x27, + 0x3c, 0xbc, 0x2d, 0x51, 0x66, 0x8c, 0x32, 0xaf, 0x4c, 0x46, 0x5c, 0x73, 0x56, 0xd7, 0x77, 0x4e, + 0x2f, 0xcf, 0xac, 0x19, 0x99, 0x30, 0x87, 0x08, 0x3a, 0xff, 0x21, 0x59, 0xfa, 0xee, 0xb9, 0xe7, + 0x9d, 0x4f, 0xa8, 0x15, 0x3d, 0x85, 0x40, 0xc1, 0x5c, 0xca, 0x05, 0x71, 0x7d, 0x09, 0x30, 0x7e, + 0x41, 0x50, 0x19, 0x44, 0x3e, 0xaf, 0x04, 0x11, 0xd4, 0xa5, 0x53, 0x81, 0xdb, 0x50, 0xa4, 0x67, + 0x67, 0x74, 0x2c, 0x34, 0x54, 0x45, 0xb5, 0x87, 0x8d, 0x5d, 0x73, 0x85, 0xb7, 0xd9, 0x8d, 0x60, + 0x2d, 0xb8, 0xee, 0xad, 0xbd, 0x47, 0x79, 0x15, 0x55, 0x1f, 0xd8, 0x31, 0x15, 0x3f, 0x86, 0x35, + 0x97, 0x8a, 0x0b, 0xcf, 0xe1, 0x9a, 0x52, 0xcd, 0xd5, 0x4a, 0x2d, 0xf5, 0xba, 0xb7, 0xf9, 0x01, + 0xc1, 0x3a, 0xd2, 0x90, 0x91, 0x0f, 0x14, 0x15, 0xd9, 0x09, 0x00, 0x6f, 0x41, 0xf1, 0x8c, 0x4d, + 0x04, 0x0d, 0xb4, 0x5c, 0x15, 0xd5, 0x4a, 0x76, 0xfc, 0x64, 0xbc, 0x57, 0xa0, 0x28, 0x9b, 0xc3, + 0x0f, 0x41, 0x61, 0x4e, 0xd4, 0x4f, 0xc9, 0x56, 0x98, 0x83, 0x3f, 0x83, 0xfc, 0x94, 0xb8, 0x54, + 0x53, 0xc2, 0x4a, 0xd4, 0x40, 0x50, 0x50, 0x91, 0xf6, 0x33, 0xb2, 0xa3, 0x3a, 0xb6, 0x01, 0x78, + 0xb2, 0x21, 0xae, 0xe5, 0xaa, 0xb9, 0x5a, 0xb9, 0x51, 0x5b, 0xb9, 0x8f, 0xa5, 0x09, 0xb4, 0xd6, + 0xaf, 0x7b, 0x85, 0x0f, 0x48, 0x59, 0x47, 0x76, 0x4a, 0x05, 0x3f, 0x05, 0x18, 0x07, 0x94, 0x08, + 0xea, 0x9c, 0x10, 0xa1, 0xe5, 0xab, 0xa8, 0x56, 0x6e, 0xe8, 0xa6, 0x9c, 0xb0, 0x99, 0x4c, 0xd8, + 0x7c, 0x9d, 0x4c, 0xd8, 0x2e, 0xc5, 0xe8, 0x7d, 0x11, 0x52, 0x2f, 0x7d, 0x27, 0xa1, 0x16, 0xfe, + 0x9f, 0x1a, 0xa3, 0xf7, 0x85, 0x61, 0x43, 0xa5, 0x1d, 0xe9, 0xc8, 0x26, 0x6d, 0xfa, 0x23, 0xfe, + 0x16, 0x8a, 0xf2, 0xdd, 0x88, 0x06, 0x52, 0xbe, 0xe5, 0x80, 0x24, 0xa7, 0x55, 0xbc, 0xee, 0xe5, + 0x7e, 0x47, 0xc8, 0x8e, 0x69, 0xc6, 0x77, 0xa0, 0x2e, 0x6a, 0x72, 0x1f, 0x7f, 0x73, 0x47, 0xd1, + 0xb9, 0xd8, 0xd7, 0x50, 0xe9, 0xd0, 0x09, 0x4d, 0x37, 0xf8, 0x08, 0x4a, 0x72, 0xf1, 0x24, 0x39, + 0xb4, 0x79, 0x0b, 0xeb, 0x72, 0xa1, 0xef, 0x18, 0x18, 0xd4, 0x45, 0x1e, 0xf7, 0x8d, 0x27, 0xb0, + 0xf1, 0x9c, 0x8a, 0x3b, 0x0a, 0xf5, 0x60, 0x33, 0x45, 0xba, 0xcf, 0x56, 0xda, 0x50, 0x39, 0x64, + 0x5c, 0x4a, 0x31, 0xca, 0xc3, 0x0e, 0x3e, 0x85, 0xc2, 0xd8, 0xbb, 0x9c, 0xca, 0x2c, 0x14, 0xe6, + 0xee, 0xb2, 0x88, 0x31, 0xe4, 0x7d, 0x72, 0x2e, 0x5f, 0xc3, 0x82, 0x1d, 0xfd, 0x36, 0x5e, 0x82, + 0xba, 0x28, 0xc2, 0x7d, 0xfc, 0x0c, 0x64, 0xbb, 0x8c, 0x72, 0x0d, 0x55, 0x73, 0x59, 0x7a, 0x9a, + 0x13, 0x8c, 0x21, 0x54, 0xf6, 0x85, 0x20, 0xe3, 0x8b, 0xbb, 0xcd, 0x05, 0xef, 0xc2, 0xda, 0x25, + 0xa7, 0x41, 0x08, 0x51, 0x16, 0x20, 0xc5, 0xb0, 0x2c, 0x4f, 0x60, 0x51, 0x98, 0xfb, 0xa1, 0x59, + 0x87, 0x7e, 0x24, 0xb3, 0x45, 0x61, 0xee, 0x1b, 0x4f, 0x61, 0x2b, 0x3d, 0xaa, 0x03, 0x2f, 0x78, + 0xc3, 0x69, 0x10, 0x7a, 0xa6, 0xe4, 0xd0, 0x8d, 0x72, 0x47, 0xb0, 0x7d, 0x23, 0xf5, 0x9e, 0xc3, + 0x7e, 0xdc, 0x86, 0xa2, 0xbc, 0xd5, 0xf0, 0x16, 0xe0, 0xee, 0xc1, 0x41, 0xb7, 0xfd, 0xfa, 0xe4, + 0xcd, 0x8b, 0x57, 0x83, 0x6e, 0xbb, 0x7f, 0xd0, 0xef, 0x76, 0xd4, 0x07, 0x58, 0x85, 0x8d, 0xb8, + 0xbe, 0x7f, 0x78, 0xf8, 0x72, 0xa8, 0x22, 0x5c, 0x81, 0x72, 0x5c, 0xe9, 0x74, 0x5f, 0xfc, 0xa0, + 0x2a, 0x8d, 0x3f, 0x0b, 0xb0, 0x19, 0xdf, 0x29, 0x34, 0x98, 0xb1, 0x31, 0xc5, 0x63, 0xd8, 0x48, + 0x27, 0x0e, 0xaf, 0xbe, 0x8b, 0x96, 0xc2, 0xae, 0xef, 0x65, 0x44, 0x72, 0x3f, 0x34, 0x49, 0x27, + 0xea, 0x16, 0x93, 0xa5, 0xc0, 0xea, 0x7b, 0x19, 0x91, 0xdc, 0xc7, 0x6f, 0xa1, 0x34, 0x4f, 0x1b, + 0xfe, 0x62, 0x25, 0x2f, 0x1d, 0x63, 0xfd, 0xcb, 0x2c, 0x30, 0xb9, 0x81, 0xf4, 0xa1, 0xde, 0xb2, + 0x81, 0xa5, 0x98, 0xea, 0x7b, 0x19, 0x91, 0xd2, 0x24, 0xfd, 0xd6, 0xdf, 0x62, 0xb2, 0x94, 0x3a, + 0x7d, 0x2f, 0x23, 0x32, 0x39, 0x8a, 0x4c, 0x26, 0x1d, 0x9a, 0xd5, 0x64, 0x39, 0x3e, 0xf8, 0x0a, + 0x3e, 0xb9, 0x21, 0x03, 0xd8, 0xca, 0x34, 0x8b, 0xff, 0xc2, 0xa6, 0x7f, 0x75, 0x37, 0x02, 0xf7, + 0x5b, 0x7f, 0x21, 0xd8, 0x19, 0x7b, 0xee, 0x2a, 0x5e, 0xab, 0x2c, 0xbb, 0x1c, 0x84, 0x5f, 0xb6, + 0x01, 0x7a, 0xdb, 0x58, 0xc4, 0x59, 0x57, 0xd6, 0x39, 0x9d, 0x5a, 0x2b, 0xfe, 0xf7, 0x3c, 0x63, + 0xc4, 0x9d, 0xd5, 0x7f, 0x55, 0xf2, 0xc3, 0xe1, 0x71, 0xff, 0x37, 0x65, 0x7b, 0x28, 0xb9, 0xc3, + 0xd8, 0xe3, 0xd8, 0xec, 0x13, 0xd7, 0x3c, 0xaa, 0xff, 0x91, 0xac, 0x8c, 0xe2, 0x95, 0xd1, 0xf1, + 0xa8, 0x4f, 0xdc, 0xd1, 0x51, 0xfd, 0x6f, 0xe5, 0xd1, 0x8a, 0x95, 0xd1, 0xf3, 0x41, 0xeb, 0x7b, + 0x2a, 0x88, 0x43, 0x04, 0xf9, 0x47, 0xd9, 0x91, 0xa8, 0x66, 0x33, 0x86, 0x35, 0x9b, 0xc7, 0xcd, + 0x66, 0x9f, 0xb8, 0xcd, 0xe6, 0x51, 0xfd, 0xb4, 0x18, 0x7d, 0x95, 0x9f, 0xfc, 0x3b, 0x00, 0xf5, + 0xbb, 0x24, 0x2f, 0x9e, 0x09, 0x00, 0x00, +} diff --git a/gen/within/website/x/iam/v1/policy_grpc.pb.go b/gen/within/website/x/iam/v1/policy_grpc.pb.go new file mode 100644 index 00000000..7884c455 --- /dev/null +++ b/gen/within/website/x/iam/v1/policy_grpc.pb.go @@ -0,0 +1,442 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: within/website/x/iam/v1/policy.proto + +package iamv1 + +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_CreatePolicy_FullMethodName = "/within.website.x.iam.v1.PolicyService/CreatePolicy" + PolicyService_DeletePolicy_FullMethodName = "/within.website.x.iam.v1.PolicyService/DeletePolicy" + PolicyService_GetPolicy_FullMethodName = "/within.website.x.iam.v1.PolicyService/GetPolicy" + PolicyService_ListPolicies_FullMethodName = "/within.website.x.iam.v1.PolicyService/ListPolicies" + PolicyService_AttachPolicy_FullMethodName = "/within.website.x.iam.v1.PolicyService/AttachPolicy" + PolicyService_DetachPolicy_FullMethodName = "/within.website.x.iam.v1.PolicyService/DetachPolicy" + PolicyService_ListPoliciesForUser_FullMethodName = "/within.website.x.iam.v1.PolicyService/ListPoliciesForUser" +) + +// 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 documents and their attachment to users. +// Every method requires an admin caller; policy administration is itself +// gated by is_admin, never by policies, so there is no way to lock yourself +// out of the management surface. +type PolicyServiceClient interface { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + CreatePolicy(ctx context.Context, in *CreatePolicyReq, opts ...grpc.CallOption) (*CreatePolicyResp, error) + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + DeletePolicy(ctx context.Context, in *DeletePolicyReq, opts ...grpc.CallOption) (*DeletePolicyResp, error) + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + GetPolicy(ctx context.Context, in *GetPolicyReq, opts ...grpc.CallOption) (*GetPolicyResp, error) + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + ListPolicies(ctx context.Context, in *ListPoliciesReq, opts ...grpc.CallOption) (*ListPoliciesResp, error) + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + AttachPolicy(ctx context.Context, in *AttachPolicyReq, opts ...grpc.CallOption) (*AttachPolicyResp, error) + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + DetachPolicy(ctx context.Context, in *DetachPolicyReq, opts ...grpc.CallOption) (*DetachPolicyResp, error) + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + ListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq, opts ...grpc.CallOption) (*ListPoliciesForUserResp, error) +} + +type policyServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPolicyServiceClient(cc grpc.ClientConnInterface) PolicyServiceClient { + return &policyServiceClient{cc} +} + +func (c *policyServiceClient) CreatePolicy(ctx context.Context, in *CreatePolicyReq, opts ...grpc.CallOption) (*CreatePolicyResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreatePolicyResp) + err := c.cc.Invoke(ctx, PolicyService_CreatePolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) DeletePolicy(ctx context.Context, in *DeletePolicyReq, opts ...grpc.CallOption) (*DeletePolicyResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeletePolicyResp) + err := c.cc.Invoke(ctx, PolicyService_DeletePolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) GetPolicy(ctx context.Context, in *GetPolicyReq, opts ...grpc.CallOption) (*GetPolicyResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetPolicyResp) + err := c.cc.Invoke(ctx, PolicyService_GetPolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) ListPolicies(ctx context.Context, in *ListPoliciesReq, opts ...grpc.CallOption) (*ListPoliciesResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListPoliciesResp) + err := c.cc.Invoke(ctx, PolicyService_ListPolicies_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) AttachPolicy(ctx context.Context, in *AttachPolicyReq, opts ...grpc.CallOption) (*AttachPolicyResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AttachPolicyResp) + err := c.cc.Invoke(ctx, PolicyService_AttachPolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) DetachPolicy(ctx context.Context, in *DetachPolicyReq, opts ...grpc.CallOption) (*DetachPolicyResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DetachPolicyResp) + err := c.cc.Invoke(ctx, PolicyService_DetachPolicy_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *policyServiceClient) ListPoliciesForUser(ctx context.Context, in *ListPoliciesForUserReq, opts ...grpc.CallOption) (*ListPoliciesForUserResp, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListPoliciesForUserResp) + err := c.cc.Invoke(ctx, PolicyService_ListPoliciesForUser_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 documents and their attachment to users. +// Every method requires an admin caller; policy administration is itself +// gated by is_admin, never by policies, so there is no way to lock yourself +// out of the management surface. +type PolicyServiceServer interface { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + CreatePolicy(context.Context, *CreatePolicyReq) (*CreatePolicyResp, error) + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + DeletePolicy(context.Context, *DeletePolicyReq) (*DeletePolicyResp, error) + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + GetPolicy(context.Context, *GetPolicyReq) (*GetPolicyResp, error) + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + ListPolicies(context.Context, *ListPoliciesReq) (*ListPoliciesResp, error) + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + AttachPolicy(context.Context, *AttachPolicyReq) (*AttachPolicyResp, error) + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + DetachPolicy(context.Context, *DetachPolicyReq) (*DetachPolicyResp, error) + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + ListPoliciesForUser(context.Context, *ListPoliciesForUserReq) (*ListPoliciesForUserResp, 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) CreatePolicy(context.Context, *CreatePolicyReq) (*CreatePolicyResp, error) { + return nil, status.Error(codes.Unimplemented, "method CreatePolicy not implemented") +} +func (UnimplementedPolicyServiceServer) DeletePolicy(context.Context, *DeletePolicyReq) (*DeletePolicyResp, error) { + return nil, status.Error(codes.Unimplemented, "method DeletePolicy not implemented") +} +func (UnimplementedPolicyServiceServer) GetPolicy(context.Context, *GetPolicyReq) (*GetPolicyResp, error) { + return nil, status.Error(codes.Unimplemented, "method GetPolicy not implemented") +} +func (UnimplementedPolicyServiceServer) ListPolicies(context.Context, *ListPoliciesReq) (*ListPoliciesResp, error) { + return nil, status.Error(codes.Unimplemented, "method ListPolicies not implemented") +} +func (UnimplementedPolicyServiceServer) AttachPolicy(context.Context, *AttachPolicyReq) (*AttachPolicyResp, error) { + return nil, status.Error(codes.Unimplemented, "method AttachPolicy not implemented") +} +func (UnimplementedPolicyServiceServer) DetachPolicy(context.Context, *DetachPolicyReq) (*DetachPolicyResp, error) { + return nil, status.Error(codes.Unimplemented, "method DetachPolicy not implemented") +} +func (UnimplementedPolicyServiceServer) ListPoliciesForUser(context.Context, *ListPoliciesForUserReq) (*ListPoliciesForUserResp, error) { + return nil, status.Error(codes.Unimplemented, "method ListPoliciesForUser 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_CreatePolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreatePolicyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).CreatePolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_CreatePolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).CreatePolicy(ctx, req.(*CreatePolicyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_DeletePolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeletePolicyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).DeletePolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_DeletePolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).DeletePolicy(ctx, req.(*DeletePolicyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_GetPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPolicyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).GetPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_GetPolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).GetPolicy(ctx, req.(*GetPolicyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_ListPolicies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListPoliciesReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).ListPolicies(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_ListPolicies_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).ListPolicies(ctx, req.(*ListPoliciesReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_AttachPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AttachPolicyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).AttachPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_AttachPolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).AttachPolicy(ctx, req.(*AttachPolicyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_DetachPolicy_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DetachPolicyReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).DetachPolicy(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_DetachPolicy_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).DetachPolicy(ctx, req.(*DetachPolicyReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _PolicyService_ListPoliciesForUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListPoliciesForUserReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PolicyServiceServer).ListPoliciesForUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PolicyService_ListPoliciesForUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PolicyServiceServer).ListPoliciesForUser(ctx, req.(*ListPoliciesForUserReq)) + } + 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: "within.website.x.iam.v1.PolicyService", + HandlerType: (*PolicyServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreatePolicy", + Handler: _PolicyService_CreatePolicy_Handler, + }, + { + MethodName: "DeletePolicy", + Handler: _PolicyService_DeletePolicy_Handler, + }, + { + MethodName: "GetPolicy", + Handler: _PolicyService_GetPolicy_Handler, + }, + { + MethodName: "ListPolicies", + Handler: _PolicyService_ListPolicies_Handler, + }, + { + MethodName: "AttachPolicy", + Handler: _PolicyService_AttachPolicy_Handler, + }, + { + MethodName: "DetachPolicy", + Handler: _PolicyService_DetachPolicy_Handler, + }, + { + MethodName: "ListPoliciesForUser", + Handler: _PolicyService_ListPoliciesForUser_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "within/website/x/iam/v1/policy.proto", +} diff --git a/go.mod b/go.mod index 85994cff..52bb471c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( al.essio.dev/pkg/shellescape v1.6.0 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 buf.build/go/protovalidate v1.1.2 + cel.dev/cel-go v0.32.0 cirello.io/goherokuname v0.0.0-20190914093443-b436bae8c2c5 codeberg.org/readeck/go-readability/v2 v2.1.0 connectrpc.com/connect v1.20.0 @@ -36,7 +37,6 @@ require ( github.com/goccy/go-json v0.10.5 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 - github.com/google/cel-go v0.29.0 github.com/google/gnostic v0.7.1 github.com/google/go-github v17.0.0+incompatible github.com/google/go-github/v81 v81.0.0 @@ -91,6 +91,7 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.2.1 gorm.io/gorm v1.31.2 gorm.io/plugin/prometheus v0.1.0 + modernc.org/quickjs v0.25.0 modernc.org/sqlite v1.37.0 nhooyr.io/websocket v1.8.17 rsc.io/gitfs v1.0.0 @@ -172,6 +173,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect @@ -305,7 +307,6 @@ require ( modernc.org/libquickjs v0.13.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.12.1 // indirect - modernc.org/quickjs v0.25.0 // indirect mvdan.cc/sh/v3 v3.12.0 // indirect ) diff --git a/go.sum b/go.sum index d203baed..b71d0f56 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-202602092021 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0= buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE= +cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= +cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251120230642-dcccabe2cd63 h1:KgI+p678truaonNOQek4i+aJdWAtdpvFzz5lqHBaDeI= @@ -480,8 +482,8 @@ github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0 h1:4gjrh/PN2Mu github.com/gomarkdown/markdown v0.0.0-20240328165702-4d01890c35c0/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/cel-go v0.29.0 h1:fEG+Ja3YRwNOqnQxTyJwoByAUAvTuxUGiro/jhrm4F4= -github.com/google/cel-go v0.29.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic v0.7.1 h1:t5Kc7j/8kYr8t2u11rykRrPPovlEMG4+xdc/SpekATs= github.com/google/gnostic v0.7.1/go.mod h1:KSw6sxnxEBFM8jLPfJd46xZP+yQcfE8XkiqfZx5zR28= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -759,8 +761,6 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -835,8 +835,6 @@ github.com/ncruces/go-sqlite3 v0.16.0 h1:O7eULuEjvSBnS1QCN+dDL/ixLQZoUGWr466A02G github.com/ncruces/go-sqlite3 v0.16.0/go.mod h1:2TmAeD93ImsKXJRsUIKohfMvt17dZSbS6pzJ3k6YYFg= github.com/ncruces/go-sqlite3/gormlite v0.16.0 h1:pBN2h323adfTF3NThIuHa3e7GCesgqrDZFEdAmdFNQE= github.com/ncruces/go-sqlite3/gormlite v0.16.0/go.mod h1:TyfqjFmR31x1iOth4t6hPgtUf2UvM8zupKSj6TZkP4c= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= @@ -1193,8 +1191,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1245,8 +1241,6 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1308,8 +1302,6 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1356,8 +1348,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1455,13 +1445,9 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518 h1:F5BWKvW126NXR74uxkxuc1jQHhm/rwm/J3rSiFyuRs4= golang.org/x/telemetry v0.0.0-20260908163034-4bcc4b2ee518/go.mod h1:i+ivNqjDnTF3WTElsdk5g9V5DTSBYgdNo7xTU9SDwYA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1474,8 +1460,6 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= -golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1495,8 +1479,6 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1566,8 +1548,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= @@ -1812,32 +1792,28 @@ honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= -modernc.org/cc/v4 v4.25.2 h1:T2oH7sZdGvTaie0BRNFbIYsabzCxUQg8nLqCdQ2i0ic= -modernc.org/cc/v4 v4.25.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= -modernc.org/ccgo/v4 v4.25.1 h1:TFSzPrAGmDsdnhT9X2UrcPMI3N/mJ9/X9ykKXwLhDsU= -modernc.org/ccgo/v4 v4.25.1/go.mod h1:njjuAYiPflywOOrm3B7kCB444ONP5pAVr8PIEoE0uDw= +modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= -modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= +modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs= modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0= modernc.org/libquickjs v0.13.1 h1:uCb9AYyEyL1JS2dnKtLZKWH/vDb36cx1Mr1SIdbzb8A= modernc.org/libquickjs v0.13.1/go.mod h1:tCEsA1Zda1+C5JagFIJGqjYZOxDPFOUYjK3dGhEeq0g= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= -modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= -modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/quickjs v0.25.0 h1:j0oeoGlDh7YMYiNgpqLxrnxXZ8DPawv3M4u4B+qqqP0= modernc.org/quickjs v0.25.0/go.mod h1:ptleYFKKqDn8J+JvpBauJcXajcpj4eWDGwW7qHpl6ao= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= diff --git a/iam/authz/authz.go b/iam/authz/authz.go new file mode 100644 index 00000000..5c3d1b59 --- /dev/null +++ b/iam/authz/authz.go @@ -0,0 +1,251 @@ +// Package authz evaluates IAM policy filters over (method, request, principal). +package authz + +import ( + "fmt" + "strings" + "time" + + "cel.dev/cel-go/cel" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/protobuf/proto" + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +var ( + decisions = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "iam_authz_decisions_total", + Help: "IAM authorization decisions by outcome.", + }, []string{"decision"}) + filterSeconds = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "iam_authz_filter_seconds", + Help: "Time spent compiling and evaluating each IAM policy filter.", + }) + filterErrors = promauto.NewCounter(prometheus.CounterOpts{ + Name: "iam_authz_filter_errors_total", + Help: "IAM policy filters that failed compilation or evaluation.", + }) +) + +func decision(allowed bool, reason, outcome string) *Decision { + decisions.WithLabelValues(outcome).Inc() + return &Decision{Allowed: allowed, Reason: reason} +} + +// Decision is the result of evaluating one policy set against a call. +type Decision struct { + // Allowed is true when some applying ALLOW statement wins and no DENY + // statement applies. An admin caller always yields Allowed=true. + Allowed bool + // Reason records the deciding rule: allow_admin, allow_policy, or the + // denying statement's effect/method for denials. + Reason string +} + +// Evaluator registers the mounted request types at startup and checks policy +// filters against their concrete request and principal types on each call. +type Evaluator struct { + envs map[string]*cel.Env +} + +// New builds an evaluator over every method the mounted services accept. +// methodTypes maps a full method name to a sample protobuf request message. +// Filters are type-checked against that method's request at enforcement time. +func New(methodTypes map[string]interface{}) (*Evaluator, error) { + e := &Evaluator{envs: make(map[string]*cel.Env, len(methodTypes))} + for method, sample := range methodTypes { + request, ok := sample.(proto.Message) + if !ok || request == nil { + return nil, fmt.Errorf("request type for %s must be a protobuf message", method) + } + env, err := cel.NewEnv( + cel.Types(request, &stsv1.TokenIdentity{}), + cel.Variable("method", cel.StringType), + cel.Variable("req", cel.ObjectType(string(request.ProtoReflect().Descriptor().FullName()))), + cel.Variable("principal", principalType()), + ) + if err != nil { + return nil, fmt.Errorf("CEL environment for %s: %w", method, err) + } + e.envs[method] = env + } + return e, nil +} + +func principalType() *cel.Type { + return cel.ObjectType(string((&stsv1.TokenIdentity{}).ProtoReflect().Descriptor().FullName())) +} + +// MethodName builds the full method name ./ that +// policy statements match against. +func MethodName(service, method string) string { + return strings.Join([]string{service, method}, "/") +} + +// PrincipalFromUser builds the TokenIdentity principal used by policy +// filters from the caller's access key and resolved user. +func PrincipalFromUser(keyID string, u *iamv1.User) *stsv1.TokenIdentity { + return &stsv1.TokenIdentity{ + AccessKeyId: keyID, + PrincipalId: u.GetId(), + IsAdmin: u.GetIsAdmin(), + } +} + +// TryCompile checks a boolean CEL filter with a concrete principal type. +// req remains dynamic because policies can target other services' messages; +// the evaluator checks request fields against the method's concrete type. +// An empty filter applies unconditionally. +func TryCompile(src string) error { + if src == "" { + return nil + } + env, err := cel.NewEnv( + cel.Types(&stsv1.TokenIdentity{}), + cel.Variable("method", cel.StringType), + cel.Variable("req", cel.DynType), + cel.Variable("principal", principalType()), + ) + if err != nil { + return err + } + _, err = compileFilter(env, src) + return err +} + +func compileFilter(env *cel.Env, src string) (*cel.Ast, error) { + ast, issues := env.Compile(src) + if err := issues.Err(); err != nil { + return nil, err + } + if !ast.OutputType().IsExactType(cel.BoolType) { + return nil, fmt.Errorf("filter must return a boolean, got %s", ast.OutputType()) + } + return ast, nil +} + +// Check evaluates every attached policy in order. Admin callers bypass +// everything. A DENY statement that applies wins; otherwise the first +// applying ALLOW allows. Failure is closed: a filter that fails to compile +// or errors at evaluation is treated as not applying on ALLOW and as +// applying on DENY. +func (e *Evaluator) Check(policies []*iamv1.Policy, method string, msg interface{}, principal *stsv1.TokenIdentity) *Decision { + if principal.GetIsAdmin() { + return decision(true, "allow_admin", "allow_admin") + } + + env, ok := e.envs[method] + if !ok { + // No environment registered for this method: fail closed. + return decision(false, "deny: no policy environment", "deny_filter_error") + } + + var allow *Decision + var filterErr error + for _, p := range policies { + for _, st := range p.GetStatements() { + if !applies(st, method) { + continue + } + started := time.Now() + matches, err := evalFilter(env, st.GetFilter(), method, msg, principal) + filterSeconds.Observe(time.Since(started).Seconds()) + if err != nil { + filterErr = err + filterErrors.Inc() + } + switch st.GetEffect() { + case iamv1.Effect_EFFECT_DENY: + if err != nil { + return decision(false, fmt.Sprintf("deny: policy filter on %s: %v", method, err), "deny_filter_error") + } + if matches { + return decision(false, "deny by policy: "+st.GetEffect().String()+" on "+method, "deny_explicit") + } + case iamv1.Effect_EFFECT_ALLOW: + if err == nil && matches && allow == nil { + allow = &Decision{Allowed: true, Reason: "allow by policy: " + st.GetEffect().String() + " on " + method} + } + } + } + } + if allow != nil { + return decision(true, allow.Reason, "allow_policy") + } + if filterErr != nil { + return decision(false, fmt.Sprintf("deny: policy filter on %s: %v", method, filterErr), "deny_filter_error") + } + return decision(false, "deny_implicit", "deny_implicit") +} + +// applies reports whether a statement's methods patterns match the method. +func applies(st *iamv1.PolicyStatement, method string) bool { + for _, pat := range st.GetMethods() { + if match(pat, method) { + return true + } + } + return false +} + +// match reports whether a glob pattern matches the whole string. '*' spans +// any characters including '/'; '?' matches one character. +func match(pattern, s string) bool { + pat, value := []rune(pattern), []rune(s) + pi, si, star, retry := 0, 0, -1, 0 + for si < len(value) { + switch { + case pi < len(pat) && (pat[pi] == '?' || pat[pi] == value[si]): + pi++ + si++ + case pi < len(pat) && pat[pi] == '*': + star, retry = pi, si + pi++ + case star >= 0: + retry++ + si, pi = retry, star+1 + default: + return false + } + } + for pi < len(pat) && pat[pi] == '*' { + pi++ + } + return pi == len(pat) +} + +// evalFilter compiles and evaluates a CEL filter over (method, req, +// principal). It returns true when the filter evaluates to true. Errors +// (including type errors) are surfaced to the caller so fail-closed logic +// can decide. +func evalFilter(env *cel.Env, filter string, method string, msg interface{}, principal *stsv1.TokenIdentity) (bool, error) { + if filter == "" { + return true, nil + } + ast, err := compileFilter(env, filter) + if err != nil { + return false, err + } + program, err := env.Program(ast, cel.EvalOptions( + cel.OptOptimize, + )) + if err != nil { + return false, fmt.Errorf("program: %w", err) + } + out, _, err := program.Eval(map[string]any{ + "method": method, + "req": msg, + "principal": principal, + }) + if err != nil { + return false, fmt.Errorf("eval: %w", err) + } + v := out.Value() + b, ok := v.(bool) + if !ok { + return false, fmt.Errorf("filter did not return a boolean") + } + return b, nil +} diff --git a/iam/authz/authz_test.go b/iam/authz/authz_test.go new file mode 100644 index 00000000..16fc93e9 --- /dev/null +++ b/iam/authz/authz_test.go @@ -0,0 +1,139 @@ +package authz + +import ( + "strings" + "testing" + + stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" +) + +func TestTryCompile(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name string + filter string + valid bool + }{ + {"empty is unconditional", "", true}, + {"boolean literal", "true", true}, + {"principal fields", `principal.principal_id == "alice" && !principal.is_admin`, true}, + {"request fields deferred", `req.user_id == principal.principal_id`, true}, + {"unknown principal field", `principal.misspelled == "alice"`, false}, + {"wrong principal field type", `principal.is_admin == "true"`, false}, + {"syntax error", `req.user_id ==`, false}, + {"string result", `"true"`, false}, + {"integer result", `1`, false}, + {"principal result", `principal`, false}, + } { + t.Run(tt.name, func(t *testing.T) { + err := TryCompile(tt.filter) + if (err == nil) != tt.valid { + t.Fatalf("TryCompile(%q) = %v; valid = %v", tt.filter, err, tt.valid) + } + }) + } +} + +func TestCheck(t *testing.T) { + t.Parallel() + method := MethodName(iamv1.KeyService_ServiceDesc.ServiceName, "ListKeys") + eval, err := New(map[string]interface{}{method: &iamv1.ListKeysReq{}}) + if err != nil { + t.Fatal(err) + } + allow := func(filter string) *iamv1.PolicyStatement { + return &iamv1.PolicyStatement{Effect: iamv1.Effect_EFFECT_ALLOW, Methods: []string{"*"}, Filter: filter} + } + deny := func(filter string) *iamv1.PolicyStatement { + return &iamv1.PolicyStatement{Effect: iamv1.Effect_EFFECT_DENY, Methods: []string{"*"}, Filter: filter} + } + for _, tt := range []struct { + name string + statements []*iamv1.PolicyStatement + userID string + admin bool + allowed bool + reason string + }{ + {name: "implicit deny", reason: "deny_implicit"}, + {name: "admin bypasses deny", statements: []*iamv1.PolicyStatement{deny("")}, admin: true, allowed: true, reason: "allow_admin"}, + {name: "empty allow", statements: []*iamv1.PolicyStatement{allow("")}, allowed: true}, + {name: "true allow", statements: []*iamv1.PolicyStatement{allow("true")}, allowed: true}, + {name: "false allow", statements: []*iamv1.PolicyStatement{allow("false")}, reason: "deny_implicit"}, + {name: "own request", statements: []*iamv1.PolicyStatement{allow(`req.user_id == principal.principal_id`)}, userID: "alice", allowed: true}, + {name: "other request", statements: []*iamv1.PolicyStatement{allow(`req.user_id == principal.principal_id`)}, userID: "bob"}, + {name: "default request", statements: []*iamv1.PolicyStatement{allow(`req.user_id == "" || req.user_id == principal.principal_id`)}, allowed: true}, + {name: "method variable", statements: []*iamv1.PolicyStatement{allow(`method.endsWith("/ListKeys")`)}, allowed: true}, + {name: "deny after allow", statements: []*iamv1.PolicyStatement{allow(""), deny("")}, reason: "deny by policy"}, + {name: "deny before allow", statements: []*iamv1.PolicyStatement{deny(""), allow("")}, reason: "deny by policy"}, + {name: "false deny falls through", statements: []*iamv1.PolicyStatement{deny("false"), allow("")}, allowed: true}, + {name: "conditional deny applies", statements: []*iamv1.PolicyStatement{allow(""), deny(`req.user_id == "bob"`)}, userID: "bob"}, + {name: "conditional deny does not apply", statements: []*iamv1.PolicyStatement{allow(""), deny(`req.user_id == "bob"`)}, userID: "alice", allowed: true}, + {name: "unknown request field allow fails closed", statements: []*iamv1.PolicyStatement{allow(`req.misspelled == "alice"`)}}, + {name: "unknown request field deny fails closed", statements: []*iamv1.PolicyStatement{allow(""), deny(`req.misspelled == "alice"`)}, reason: "undefined field"}, + {name: "unknown principal field allow fails closed", statements: []*iamv1.PolicyStatement{allow(`principal.misspelled == "alice"`)}}, + {name: "nonboolean allow fails closed", statements: []*iamv1.PolicyStatement{allow(`"true"`)}}, + {name: "nonboolean deny fails closed", statements: []*iamv1.PolicyStatement{allow(""), deny(`"false"`)}, reason: "must return a boolean"}, + {name: "runtime allow fails closed", statements: []*iamv1.PolicyStatement{allow(`1 / size(req.user_id) > 0`)}}, + {name: "runtime deny fails closed", statements: []*iamv1.PolicyStatement{allow(""), deny(`1 / size(req.user_id) > 0`)}, reason: "division by zero"}, + {name: "unmatched deny ignored", statements: []*iamv1.PolicyStatement{allow(""), {Effect: iamv1.Effect_EFFECT_DENY, Methods: []string{"Other/*"}}}, allowed: true}, + } { + t.Run(tt.name, func(t *testing.T) { + // Each statement is in its own policy to exercise precedence across attachments. + var policies []*iamv1.Policy + for _, statement := range tt.statements { + policies = append(policies, &iamv1.Policy{Statements: []*iamv1.PolicyStatement{statement}}) + } + got := eval.Check(policies, method, &iamv1.ListKeysReq{UserId: tt.userID}, &stsv1.TokenIdentity{PrincipalId: "alice", IsAdmin: tt.admin}) + if got.Allowed != tt.allowed || !strings.Contains(got.Reason, tt.reason) { + t.Fatalf("Check() = %+v; want allowed=%v, reason containing %q", got, tt.allowed, tt.reason) + } + }) + } + if got := eval.Check(nil, "unknown", &iamv1.ListKeysReq{}, nil); got.Allowed { + t.Fatalf("unknown method allowed: %+v", got) + } + if got := eval.Check(nil, "unknown", nil, &stsv1.TokenIdentity{IsAdmin: true}); !got.Allowed { + t.Fatalf("admin should bypass environment lookup: %+v", got) + } +} + +func TestNewRejectsNonProtoRequest(t *testing.T) { + if _, err := New(map[string]interface{}{"service/method": struct{}{}}); err == nil { + t.Fatal("New accepted a non-protobuf request") + } +} + +func TestMatch(t *testing.T) { + t.Parallel() + for _, tt := range []struct { + name, pattern, value string + want bool + }{ + {"empty", "", "", true}, + {"star empty", "*", "", true}, + {"trailing star empty", "service/Read*", "service/Read", true}, + {"repeated stars", "service/Read***", "service/Read", true}, + {"global spans slash", "*", "service/Read", true}, + {"service wildcard", "service/*", "service/Read", true}, + {"prefix wildcard", "*/Read", "service/Read", true}, + {"middle wildcard", "s*ce/R*d", "service/Read", true}, + {"retry wildcard", "*ab", "aaab", true}, + {"multiple wildcard", "*a*b*c", "xxaayybbzzc", true}, + {"one character", "service/Re?d", "service/Read", true}, + {"one unicode character", "?", "é", true}, + {"exact", "service/Read", "service/Read", true}, + {"prefix insufficient", "service", "service/Read", false}, + {"empty pattern", "", "service/Read", false}, + {"question requires character", "?", "", false}, + {"wrong suffix", "*Write", "service/Read", false}, + {"literal brackets", "[ab]", "a", false}, + } { + t.Run(tt.name, func(t *testing.T) { + if got := match(tt.pattern, tt.value); got != tt.want { + t.Fatalf("match(%q, %q) = %v; want %v", tt.pattern, tt.value, got, tt.want) + } + }) + } +} diff --git a/pb/within/website/x/iam/sts/v1/sts.proto b/pb/within/website/x/iam/sts/v1/sts.proto index 2fa59bab..0a9a85be 100644 --- a/pb/within/website/x/iam/sts/v1/sts.proto +++ b/pb/within/website/x/iam/sts/v1/sts.proto @@ -3,6 +3,7 @@ package within.website.x.iam.sts.v1; import "buf/validate/validate.proto"; import "google/protobuf/timestamp.proto"; +import "within/website/x/iam/v1/policy.proto"; // SigningKeyService hands out SigV4 derived signing keys so that downstream // services can verify request signatures locally without ever holding raw @@ -73,11 +74,16 @@ message GetSigningKeyResponse { // clock skew, after which no legitimate request can carry this scope. // Never cache the key past this instant. google.protobuf.Timestamp not_valid_after = 3; - // How long the caller may cache this response before asking again. Bounds // revocation latency: a disabled key stops verifying within one cache TTL // instead of at end of day. Always <= not_valid_after. google.protobuf.Timestamp cache_until = 4; + + // The caller's effective policy set: every document attached to the + // identity's user. Downstream verifiers evaluate these locally (see + // within.website/x/iam/authz) instead of calling back per request. Empty + // for an admin identity, which bypasses policy evaluation. + repeated within.website.x.iam.v1.Policy policies = 5; } message GetPublicKeyRequest { @@ -98,6 +104,9 @@ message GetPublicKeyResponse { // revocation latency: a disabled key stops verifying within one cache TTL. // A response without cache_until may be used once but never cached. google.protobuf.Timestamp cache_until = 3; + + // The caller's effective policy set, mirroring GetSigningKeyResponse. + repeated within.website.x.iam.v1.Policy policies = 4; } // TokenIdentity identifies the principal a signing key belongs to. @@ -115,4 +124,8 @@ message TokenIdentity { // Human-readable name for logs and error messages: iamd's user name. string display_name = 4; + + // Whether the principal is an iamd admin. Admin bypasses policy + // evaluation (allow-all); verifiers must honor it exactly as iamd does. + bool is_admin = 5; } diff --git a/pb/within/website/x/iam/v1/policy.proto b/pb/within/website/x/iam/v1/policy.proto new file mode 100644 index 00000000..20b7d6e3 --- /dev/null +++ b/pb/within/website/x/iam/v1/policy.proto @@ -0,0 +1,209 @@ +syntax = "proto3"; +package within.website.x.iam.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +// The policy engine grants and denies RPC calls the way AWS IAM policies +// grant and deny API actions, with one substitution: where AWS matches on +// action + resource + condition operators, here a statement matches on the +// RPC's full method name and an optional CEL filter evaluated over the typed +// request message. +// +// Evaluation, per call, over every policy attached to the caller: +// +// 1. A statement applies when one of its methods matches the RPC's full +// method name and, when filter is set, the filter evaluates to true. +// 2. Any applying DENY statement denies the call. Explicit deny wins. +// 3. Otherwise any applying ALLOW statement allows the call. +// 4. Otherwise the call is implicitly denied. +// +// A user flagged is_admin bypasses policy evaluation entirely (allow-all); +// that flag is the bootstrap credential that can mint and attach policies. +// +// Method names are "./", e.g. +// "within.website.x.iam.v1.KeyService/ListKeys". Patterns are anchored +// full-string matches where "*" spans any characters including "/" and "?" +// matches exactly one character: +// +// "*" - every method +// "within.website.x.iam.v1.KeyService/*" - one whole service +// "within.website.x.iam.v1.*/List*" - listers across services +// +// Filters are CEL expressions evaluated with three bindings: +// +// method string - the full method name being called +// req message - the decoded request proto for that method +// principal message - within.website.x.iam.sts.v1.TokenIdentity for the +// caller; principal.principal_id is the user UUID +// +// Example filters: +// +// "" - no condition +// "req.user_id == principal.principal_id" - own resources only +// "has(req.comment) && req.comment != ''" - field inspection +// +// An empty filter is always true. A filter that fails to compile against the +// method's request type, or errors at evaluation, fails closed: the +// statement is treated as not applying and the reason is logged. Policies +// are syntax-checked when written; only services that know a method's +// request type can type-check its filters. + +// Effect is what a statement does when it applies. +enum Effect { + EFFECT_UNSPECIFIED = 0; + EFFECT_ALLOW = 1; + EFFECT_DENY = 2; +} + +// PolicyStatement is one allow or deny rule. +message PolicyStatement { + Effect effect = 1 [(buf.validate.field).enum = { + defined_only: true + not_in: [0] + }]; + + // Anchored glob patterns against full method names; at least one required. + repeated string methods = 2 [ + (buf.validate.field).repeated.min_items = 1, + (buf.validate.field).repeated.unique = true, + (buf.validate.field).repeated.items.string.min_len = 1 + ]; + + // Optional CEL filter over (method, req, principal). Empty means true. + string filter = 3; +} + +// Policy is a named, reusable document. Users reference policies by id; +// editing a policy document is not supported after creation (attach a new +// one and detach the old), so a compiled snapshot always matches what an +// enforcing service fetched. +message Policy { + // UUID version 7 (time-ordered), like User.id. + string id = 1; + + // Human-readable name, unique across the deployment. + string name = 2 [(buf.validate.field).string = { + min_len: 1 + max_len: 128 + }]; + + repeated PolicyStatement statements = 3 [(buf.validate.field).repeated.min_items = 1]; + + google.protobuf.Timestamp created_at = 4; + google.protobuf.Timestamp updated_at = 5; +} + +// PolicyService manages policy documents and their attachment to users. +// Every method requires an admin caller; policy administration is itself +// gated by is_admin, never by policies, so there is no way to lock yourself +// out of the management surface. +service PolicyService { + // CreatePolicy stores a new document. The statements' filters are + // syntax-checked; INVALID_ARGUMENT reports the first failure. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // INVALID_ARGUMENT - validation failure, including CEL syntax + // ALREADY_EXISTS - name already in use + rpc CreatePolicy(CreatePolicyReq) returns (CreatePolicyResp); + + // DeletePolicy removes a document. It is refused while any user still has + // it attached, so an enforcement snapshot never references a missing doc. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // FAILED_PRECONDITION - policy still attached to at least one user + // NOT_FOUND - no such policy id + rpc DeletePolicy(DeletePolicyReq) returns (DeletePolicyResp); + + // GetPolicy returns one document by id. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id + rpc GetPolicy(GetPolicyReq) returns (GetPolicyResp); + + // ListPolicies enumerates documents, newest first, paginated. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + rpc ListPolicies(ListPoliciesReq) returns (ListPoliciesResp); + + // AttachPolicy binds a document to a user. Takes effect on the caller's + // next authenticated RPC (or when a downstream verifier's policy cache + // expires, bounded by the signing-key cache TTL). + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such policy id or user id + // ALREADY_EXISTS - user already has this policy attached + rpc AttachPolicy(AttachPolicyReq) returns (AttachPolicyResp); + + // DetachPolicy unbinds a document from a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such attachment + rpc DetachPolicy(DetachPolicyReq) returns (DetachPolicyResp); + + // ListPoliciesForUser returns every document attached to a user. + // + // Errors: + // PERMISSION_DENIED - caller is not an admin + // NOT_FOUND - no such user id + rpc ListPoliciesForUser(ListPoliciesForUserReq) returns (ListPoliciesForUserResp); +} + +message CreatePolicyReq { + Policy policy = 1 [(buf.validate.field).required = true]; +} + +message CreatePolicyResp { + Policy policy = 1; +} + +message DeletePolicyReq { + string policy_id = 1 [(buf.validate.field).required = true]; +} + +message DeletePolicyResp {} + +message GetPolicyReq { + string policy_id = 1 [(buf.validate.field).required = true]; +} + +message GetPolicyResp { + Policy policy = 1; +} + +message ListPoliciesReq { + int32 count = 1 [(buf.validate.field).required = true]; + int32 page = 2; +} + +message ListPoliciesResp { + repeated Policy policies = 1; +} + +message AttachPolicyReq { + string policy_id = 1 [(buf.validate.field).required = true]; + string user_id = 2 [(buf.validate.field).required = true]; +} + +message AttachPolicyResp {} + +message DetachPolicyReq { + string policy_id = 1 [(buf.validate.field).required = true]; + string user_id = 2 [(buf.validate.field).required = true]; +} + +message DetachPolicyResp {} + +message ListPoliciesForUserReq { + string user_id = 1 [(buf.validate.field).required = true]; +} + +message ListPoliciesForUserResp { + repeated Policy policies = 1; +} diff --git a/web/middleware/authctx/authctx.go b/web/middleware/authctx/authctx.go index ac9e059c..497b19d1 100644 --- a/web/middleware/authctx/authctx.go +++ b/web/middleware/authctx/authctx.go @@ -50,7 +50,28 @@ type Identity struct { OrganizationID string PrincipalID string DisplayName string - SignedAt time.Time + // IsAdmin propagates iamd's admin flag: admin callers bypass policy + // evaluation everywhere, exactly as they do inside iamd. + IsAdmin bool + SignedAt time.Time +} + +type principalPoliciesKey struct{} + +// WithPrincipalPolicies stores the caller's effective policy set in ctx. It +// is populated by the iamsts verifiers from the SigningKeyService response +// alongside WithCaller, so downstream services can authorize calls locally +// (see within.website/x/iam/authz) without a per-request IAM RPC. +func WithPrincipalPolicies(ctx context.Context, policies []*iamv1.Policy) context.Context { + return context.WithValue(ctx, principalPoliciesKey{}, policies) +} + +// PrincipalPolicies returns the caller's policy set stored in ctx, if any. +// The bool is false when nothing populated it (for example an admin caller, +// which carries no policies because it bypasses evaluation). +func PrincipalPolicies(ctx context.Context) ([]*iamv1.Policy, bool) { + p, ok := ctx.Value(principalPoliciesKey{}).([]*iamv1.Policy) + return p, ok } type callerKey struct{} diff --git a/web/middleware/sigv4/iamsts/iamsts.go b/web/middleware/sigv4/iamsts/iamsts.go index 95df8fdf..358c7365 100644 --- a/web/middleware/sigv4/iamsts/iamsts.go +++ b/web/middleware/sigv4/iamsts/iamsts.go @@ -34,6 +34,7 @@ import ( "golang.org/x/sync/singleflight" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" "within.website/x/web/middleware/authctx" "within.website/x/web/middleware/sigv4" ) @@ -104,6 +105,7 @@ func (k scopeKey) String() string { type entry struct { signingKey []byte identity *stsv1.TokenIdentity + policies []*iamv1.Policy err error expiresAt time.Time } @@ -133,17 +135,20 @@ func New(cfg Config) *Verifier { } // Middleware wraps next so every request is verified locally against a cached -// signing key. On success the caller identity is stored in the request -// context (see Caller). Error mapping matches the local sigv4 middleware. +// signing key. On success the caller identity and its effective policy set +// are stored in the request context (see Caller and authctx.PrincipalPolicies). +// Error mapping matches the local sigv4 middleware. func (v *Verifier) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - id, err := v.Verify(r) + id, e, err := v.verify(r) if err != nil { slog.DebugContext(r.Context(), "iamsts: cannot verify request", "err", err, "method", r.Method, "path", r.URL.Path) twirp.WriteError(w, sigv4.TwirpError(r.Context(), err)) return } - next.ServeHTTP(w, r.WithContext(withCaller(r.Context(), id))) + ctx := withCaller(r.Context(), id) + ctx = authctx.WithPrincipalPolicies(ctx, e.policies) + next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -153,9 +158,16 @@ func (v *Verifier) Middleware(next http.Handler) http.Handler { // Verifier's cache. The request body is buffered and reset so downstream // handlers can read it. On success it returns the caller identity. func (v *Verifier) Verify(r *http.Request) (*Identity, error) { + id, _, err := v.verify(r) + return id, err +} + +// verify is Verify's implementation, also returning the cache entry used so +// Middleware can surface the caller's policies without a second lookup. +func (v *Verifier) verify(r *http.Request) (*Identity, *entry, error) { keyID, err := v.inner.Verify(r) if err != nil { - return nil, err + return nil, nil, err } // The signature checked out, so the credential parses and its scope @@ -163,11 +175,11 @@ func (v *Verifier) Verify(r *http.Request) (*Identity, error) { // it expired in between, this refetches — still off the hot path. cred, err := sigv4.ParseCredential(r.Header.Get("Authorization")) if err != nil { - return nil, err + return nil, nil, err } e, err := v.entry(r.Context(), scopeKey{cred.AccessKeyID, cred.Date, cred.Region, cred.Service}) if err != nil { - return nil, err + return nil, nil, err } id := &Identity{ @@ -175,11 +187,12 @@ func (v *Verifier) Verify(r *http.Request) (*Identity, error) { OrganizationID: e.identity.GetOrganizationId(), PrincipalID: e.identity.GetPrincipalId(), DisplayName: e.identity.GetDisplayName(), + IsAdmin: e.identity.GetIsAdmin(), } if t, terr := time.Parse(amzTimeFormat, r.Header.Get("X-Amz-Date")); terr == nil { id.SignedAt = t } - return id, nil + return id, e, nil } // LookupSigningKey implements sigv4.SigningKeyLookuper through the cache. The @@ -266,6 +279,7 @@ func (v *Verifier) fetch(ctx context.Context, k scopeKey) (*entry, error) { e := &entry{ signingKey: resp.GetSigningKey(), identity: resp.GetIdentity(), + policies: resp.GetPolicies(), } // Honor the server's caching bounds exactly: cache_until is the TTL, // clamped by not_valid_after. A response without cache_until is used for diff --git a/web/middleware/sigv4/iamsts/iamsts_test.go b/web/middleware/sigv4/iamsts/iamsts_test.go index a3d7dab2..8b0deb02 100644 --- a/web/middleware/sigv4/iamsts/iamsts_test.go +++ b/web/middleware/sigv4/iamsts/iamsts_test.go @@ -13,9 +13,12 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" signer "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/twitchtv/twirp" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/web/middleware/authctx" "within.website/x/web/middleware/sigv4" ) @@ -35,6 +38,8 @@ type fakeKeys struct { calls atomic.Int64 refuseCode twirp.ErrorCode // when non-empty, refuse with this code cacheTTL time.Duration + isAdmin bool + policies []*iamv1.Policy now func() time.Time } @@ -75,8 +80,10 @@ func (f *fakeKeys) GetSigningKey(_ context.Context, req *stsv1.GetSigningKeyRequ AccessKeyId: req.GetAccessKeyId(), PrincipalId: "u1", DisplayName: "tester", + IsAdmin: f.isAdmin, }, NotValidAfter: timestamppb.New(nva), + Policies: f.policies, CacheUntil: timestamppb.New(cu), }, nil } @@ -370,3 +377,53 @@ func TestVerifier_KnownAnswerFixture(t *testing.T) { t.Errorf("AccessKeyID = %q", id.AccessKeyID) } } + +func TestVerifier_PolicyMetadataSurvivesCache(t *testing.T) { + for _, tt := range []struct { + name string + admin bool + policies []*iamv1.Policy + }{ + {name: "admin", admin: true}, + {name: "policy", policies: []*iamv1.Policy{{Id: "p1", Name: "own-things", Statements: []*iamv1.PolicyStatement{{Effect: iamv1.Effect_EFFECT_ALLOW, Methods: []string{"example.Things/Get"}, Filter: "req.id == principal.principal_id"}}}}}, + } { + t.Run(tt.name, func(t *testing.T) { + h, closeSrv := newHarness(t, 5*time.Minute) + defer closeSrv() + h.fake.isAdmin = tt.admin + h.fake.policies = tt.policies + handled := 0 + h.handler = h.verifier.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handled++ + id, ok := authctx.Caller(r.Context()) + if !ok || id.IsAdmin != tt.admin { + t.Errorf("caller = %+v, want is_admin %v", id, tt.admin) + } + ps, ok := authctx.PrincipalPolicies(r.Context()) + if !ok || len(ps) != len(tt.policies) { + t.Errorf("policy metadata = %v (present %v), want %v", ps, ok, tt.policies) + } else { + for i, p := range ps { + if !proto.Equal(p, tt.policies[i]) { + t.Errorf("policy = %v, want %v", p, tt.policies[i]) + } + } + } + w.WriteHeader(http.StatusNoContent) + })) + // The first verified request fetches metadata; the second restores it + // from the credential cache without another STS request. + for range 2 { + if rec := do(h, signedGET(t, h.clock())); rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) + } + } + if handled != 2 { + t.Errorf("handled %d requests, want 2", handled) + } + if got := h.fake.calls.Load(); got != 1 { + t.Errorf("STS RPCs = %d, want 1", got) + } + }) + } +} diff --git a/web/middleware/sigv4/iamsts/integration.md b/web/middleware/sigv4/iamsts/integration.md index 42e61545..fedc8f18 100644 --- a/web/middleware/sigv4/iamsts/integration.md +++ b/web/middleware/sigv4/iamsts/integration.md @@ -9,6 +9,12 @@ derived signing key for each credential scope from iamd's `SigningKeyService` once, caches it for the server-advised TTL, and recomputes signatures itself. When the cache is warm there are **zero** IAM RPCs on the request path. +This page covers signature authentication only. Application policy +enforcement also requires the authorization integration in +[Integrating a service with iamd policies](../../../../docs/iam-service-integration.md). +For this classic chain, the backend credential needs a scoped +`GetSigningKey` grant rather than the guide's SigV4A `GetPublicKey` grant. + ## Prerequisites - An IAM credential for your service (the "verifier credential"), used only to diff --git a/web/middleware/sigv4a/iamsts/iamsts.go b/web/middleware/sigv4a/iamsts/iamsts.go index 2b38200c..18a8ef08 100644 --- a/web/middleware/sigv4a/iamsts/iamsts.go +++ b/web/middleware/sigv4a/iamsts/iamsts.go @@ -36,6 +36,7 @@ import ( "golang.org/x/sync/singleflight" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" "within.website/x/web/middleware/authctx" "within.website/x/web/middleware/sigv4a" ) @@ -93,6 +94,7 @@ type Verifier struct { type entry struct { pub *ecdsa.PublicKey identity *stsv1.TokenIdentity + policies []*iamv1.Policy err error expiresAt time.Time } @@ -122,17 +124,20 @@ func New(cfg Config) *Verifier { } // Middleware wraps next so every request is verified locally against a cached -// public key. On success the caller identity is stored in the request -// context (see Caller). Error mapping matches the local sigv4a middleware. +// public key. On success the caller identity and its effective policy set +// are stored in the request context (see Caller and authctx.PrincipalPolicies). +// Error mapping matches the local sigv4a middleware. func (v *Verifier) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - id, err := v.Verify(r) + id, e, err := v.verify(r) if err != nil { slog.DebugContext(r.Context(), "iamsts: cannot verify request", "err", err, "method", r.Method, "path", r.URL.Path) twirp.WriteError(w, sigv4a.TwirpError(r.Context(), err)) return } - next.ServeHTTP(w, r.WithContext(withCaller(r.Context(), id))) + ctx := withCaller(r.Context(), id) + ctx = authctx.WithPrincipalPolicies(ctx, e.policies) + next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -142,9 +147,16 @@ func (v *Verifier) Middleware(next http.Handler) http.Handler { // cache. The request body is buffered and reset so downstream handlers can // read it. On success it returns the caller identity. func (v *Verifier) Verify(r *http.Request) (*Identity, error) { + id, _, err := v.verify(r) + return id, err +} + +// verify is Verify's implementation, also returning the cache entry used so +// Middleware can surface the caller's policies without a second lookup. +func (v *Verifier) verify(r *http.Request) (*Identity, *entry, error) { keyID, err := v.inner.Verify(r) if err != nil { - return nil, err + return nil, nil, err } // The signature checked out, so this entry was just used; re-read it for @@ -152,7 +164,7 @@ func (v *Verifier) Verify(r *http.Request) (*Identity, error) { // refetches — still off the hot path. e, err := v.entry(r.Context(), keyID) if err != nil { - return nil, err + return nil, nil, err } id := &Identity{ @@ -160,11 +172,12 @@ func (v *Verifier) Verify(r *http.Request) (*Identity, error) { OrganizationID: e.identity.GetOrganizationId(), PrincipalID: e.identity.GetPrincipalId(), DisplayName: e.identity.GetDisplayName(), + IsAdmin: e.identity.GetIsAdmin(), } if t, terr := time.Parse(amzTimeFormat, r.Header.Get("X-Amz-Date")); terr == nil { id.SignedAt = t } - return id, nil + return id, e, nil } // LookupPublicKey implements sigv4a.PublicKeyLookuper through the cache. The @@ -251,7 +264,7 @@ func (v *Verifier) fetch(ctx context.Context, accessKeyID string) (*entry, error return nil, fmt.Errorf("iamsts: response public key is %T, want ECDSA P-256", parsed) } - e := &entry{pub: pub, identity: resp.GetIdentity()} + e := &entry{pub: pub, identity: resp.GetIdentity(), policies: resp.GetPolicies()} // Honor the server's caching bound exactly: a response without // cache_until is used for this request but never cached — the server // declined to grant a TTL and we do not invent one. diff --git a/web/middleware/sigv4a/iamsts/iamsts_test.go b/web/middleware/sigv4a/iamsts/iamsts_test.go index 336683d1..8fe3f3cb 100644 --- a/web/middleware/sigv4a/iamsts/iamsts_test.go +++ b/web/middleware/sigv4a/iamsts/iamsts_test.go @@ -11,9 +11,12 @@ import ( "time" "github.com/twitchtv/twirp" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" stsv1 "within.website/x/gen/within/website/x/iam/sts/v1" + iamv1 "within.website/x/gen/within/website/x/iam/v1" + "within.website/x/web/middleware/authctx" "within.website/x/web/middleware/sigv4a" ) @@ -48,6 +51,8 @@ type fakeKeys struct { calls atomic.Int64 refuseCode twirp.ErrorCode // when non-empty, refuse with this code cacheTTL time.Duration + isAdmin bool + policies []*iamv1.Policy now func() time.Time } @@ -87,7 +92,9 @@ func (f *fakeKeys) GetPublicKey(_ context.Context, req *stsv1.GetPublicKeyReques AccessKeyId: req.GetAccessKeyId(), PrincipalId: "u1", DisplayName: "tester", + IsAdmin: f.isAdmin, }, + Policies: f.policies, CacheUntil: timestamppb.New(f.now().Add(f.cacheTTL)), }, nil } @@ -390,3 +397,53 @@ func TestVerifier_FetchSurvivesCallerCancel(t *testing.T) { t.Errorf("RPCs = %d, want 1", got) } } + +func TestVerifier_PolicyMetadataSurvivesCache(t *testing.T) { + for _, tt := range []struct { + name string + admin bool + policies []*iamv1.Policy + }{ + {name: "admin", admin: true}, + {name: "policy", policies: []*iamv1.Policy{{Id: "p1", Name: "own-things", Statements: []*iamv1.PolicyStatement{{Effect: iamv1.Effect_EFFECT_ALLOW, Methods: []string{"example.Things/Get"}, Filter: "req.id == principal.principal_id"}}}}}, + } { + t.Run(tt.name, func(t *testing.T) { + h, closeSrv := newHarness(t, 5*time.Minute) + defer closeSrv() + h.fake.isAdmin = tt.admin + h.fake.policies = tt.policies + handled := 0 + h.handler = h.verifier.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handled++ + id, ok := authctx.Caller(r.Context()) + if !ok || id.IsAdmin != tt.admin { + t.Errorf("caller = %+v, want is_admin %v", id, tt.admin) + } + ps, ok := authctx.PrincipalPolicies(r.Context()) + if !ok || len(ps) != len(tt.policies) { + t.Errorf("policy metadata = %v (present %v), want %v", ps, ok, tt.policies) + } else { + for i, p := range ps { + if !proto.Equal(p, tt.policies[i]) { + t.Errorf("policy = %v, want %v", p, tt.policies[i]) + } + } + } + w.WriteHeader(http.StatusNoContent) + })) + // The first verified request fetches metadata; the second restores it + // from the credential cache without another STS request. + for range 2 { + if rec := do(h, signedGET(t, h.clock())); rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, body=%s", rec.Code, rec.Body.String()) + } + } + if handled != 2 { + t.Errorf("handled %d requests, want 2", handled) + } + if got := h.fake.calls.Load(); got != 1 { + t.Errorf("STS RPCs = %d, want 1", got) + } + }) + } +} diff --git a/web/middleware/sigv4a/iamsts/integration.md b/web/middleware/sigv4a/iamsts/integration.md index 056ac2c9..af6ab8d0 100644 --- a/web/middleware/sigv4a/iamsts/integration.md +++ b/web/middleware/sigv4a/iamsts/integration.md @@ -10,6 +10,12 @@ Unlike the classic SigV4 `iamsts` this guide is modeled on, the cache holds only public material: even a full compromise of this service's cache lets an attacker verify signatures, never mint them. +This page covers signature authentication only. To enforce IAM policies, +also install the authorization interceptor described in +[Integrating a service with iamd policies](../../../../docs/iam-service-integration.md). +The verifier's backend credential needs a `GetPublicKey` policy grant; +authentication alone does not authorize callers' application RPCs. + ## Prerequisites - An IAM credential for your service (the "verifier credential"), used only to From f15a46685574c23e12efefcb321bfbe86636d4f5 Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Sat, 17 Oct 2026 01:35:16 -0400 Subject: [PATCH 2/2] chore: make generation stop complaining Signed-off-by: Xe Iaso --- gen/within/website/x/external/jsonfeed/v1/jsonfeed.pb.go | 2 +- gen/within/website/x/futuresight/v1/futuresight.pb.go | 2 +- gen/within/website/x/iam/v1/iam.pb.go | 2 +- gen/within/website/x/mi/v1/mi.pb.go | 2 +- gen/within/website/x/mimi/announce/v1/announce.pb.go | 2 +- gen/within/website/x/mimi/scheduling/v1/scheduling.pb.go | 2 +- gen/within/website/x/mimi/statuspage/statuspage.pb.go | 2 +- gen/within/website/x/patchouli/v1/patchouli.pb.go | 2 +- gen/within/website/x/relayd/v1/relayd.pb.go | 2 +- gen/within/website/x/sanguisuga/v1/sanguisuga.pb.go | 2 +- gen/within/website/x/xedn/uplodr/v1/uplodr.pb.go | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/gen/within/website/x/external/jsonfeed/v1/jsonfeed.pb.go b/gen/within/website/x/external/jsonfeed/v1/jsonfeed.pb.go index 754bd1e0..04c91f3c 100644 --- a/gen/within/website/x/external/jsonfeed/v1/jsonfeed.pb.go +++ b/gen/within/website/x/external/jsonfeed/v1/jsonfeed.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/external/jsonfeed/v1/jsonfeed.proto diff --git a/gen/within/website/x/futuresight/v1/futuresight.pb.go b/gen/within/website/x/futuresight/v1/futuresight.pb.go index 244e1f1c..40adb765 100644 --- a/gen/within/website/x/futuresight/v1/futuresight.pb.go +++ b/gen/within/website/x/futuresight/v1/futuresight.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/futuresight/v1/futuresight.proto diff --git a/gen/within/website/x/iam/v1/iam.pb.go b/gen/within/website/x/iam/v1/iam.pb.go index d44f50d2..13f2195e 100644 --- a/gen/within/website/x/iam/v1/iam.pb.go +++ b/gen/within/website/x/iam/v1/iam.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/iam/v1/iam.proto diff --git a/gen/within/website/x/mi/v1/mi.pb.go b/gen/within/website/x/mi/v1/mi.pb.go index 2678373f..e7d23522 100644 --- a/gen/within/website/x/mi/v1/mi.pb.go +++ b/gen/within/website/x/mi/v1/mi.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/mi/v1/mi.proto diff --git a/gen/within/website/x/mimi/announce/v1/announce.pb.go b/gen/within/website/x/mimi/announce/v1/announce.pb.go index 8c7f183a..d1f5b28b 100644 --- a/gen/within/website/x/mimi/announce/v1/announce.pb.go +++ b/gen/within/website/x/mimi/announce/v1/announce.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/mimi/announce/v1/announce.proto diff --git a/gen/within/website/x/mimi/scheduling/v1/scheduling.pb.go b/gen/within/website/x/mimi/scheduling/v1/scheduling.pb.go index 1febeb8f..245ffb01 100644 --- a/gen/within/website/x/mimi/scheduling/v1/scheduling.pb.go +++ b/gen/within/website/x/mimi/scheduling/v1/scheduling.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/mimi/scheduling/v1/scheduling.proto diff --git a/gen/within/website/x/mimi/statuspage/statuspage.pb.go b/gen/within/website/x/mimi/statuspage/statuspage.pb.go index 3e827602..de1c3537 100644 --- a/gen/within/website/x/mimi/statuspage/statuspage.pb.go +++ b/gen/within/website/x/mimi/statuspage/statuspage.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/mimi/statuspage/statuspage.proto diff --git a/gen/within/website/x/patchouli/v1/patchouli.pb.go b/gen/within/website/x/patchouli/v1/patchouli.pb.go index fae3a7b9..4796f28c 100644 --- a/gen/within/website/x/patchouli/v1/patchouli.pb.go +++ b/gen/within/website/x/patchouli/v1/patchouli.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/patchouli/v1/patchouli.proto diff --git a/gen/within/website/x/relayd/v1/relayd.pb.go b/gen/within/website/x/relayd/v1/relayd.pb.go index 16ae3d7b..8aa0bf95 100644 --- a/gen/within/website/x/relayd/v1/relayd.pb.go +++ b/gen/within/website/x/relayd/v1/relayd.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/relayd/v1/relayd.proto diff --git a/gen/within/website/x/sanguisuga/v1/sanguisuga.pb.go b/gen/within/website/x/sanguisuga/v1/sanguisuga.pb.go index b79e354f..e10beabc 100644 --- a/gen/within/website/x/sanguisuga/v1/sanguisuga.pb.go +++ b/gen/within/website/x/sanguisuga/v1/sanguisuga.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/sanguisuga/v1/sanguisuga.proto diff --git a/gen/within/website/x/xedn/uplodr/v1/uplodr.pb.go b/gen/within/website/x/xedn/uplodr/v1/uplodr.pb.go index c7a26bcb..8c9edce5 100644 --- a/gen/within/website/x/xedn/uplodr/v1/uplodr.pb.go +++ b/gen/within/website/x/xedn/uplodr/v1/uplodr.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.12 +// protoc-gen-go v1.36.11 // protoc (unknown) // source: within/website/x/xedn/uplodr/v1/uplodr.proto