From feb3aea38fbea68a391516a520104f8fbc4c0706 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 6 Jun 2023 16:13:44 -0400 Subject: [PATCH] Support multiple IDs in grants (#3263) This allows specifying multiple IDs in grants via an `ids` field, which will eventually become the normal way to specify IDs. Internally, each grant with multiple IDs turns into multiple ACL grants each with a single ID; to make this distinction clearer and allow for better separation of concerns an AclGrant type has been introduced, so really multiple IDs in a Grant turn into multiple AclGrants. The nice thing about doing it this way is that it becomes purely a parsing concern; no actual validation logic has changed. For backwards compatibility with grants already stored in roles, we do not error out if `id` is specified. However, eventually we will simply not support grants with `id` coming via the API; they will fail API validation, such that new grants will require the use of the updated `ids` field. A follow-on PR will plumb this through the service handlers/API/CLI, but I decided to do it in separate chunks to make reviewing easier, given that they are easily separable into the internal and external aspects of the change. * Add CLI/API checks around using ID in grants (#3264) * Plumb through some contexts to remove a lot of deprecated errors (#3268) --- CHANGELOG.md | 13 + api/roles/grant_json.gen.go | 1 + globals/fields.go | 1 + globals/prefixes.go | 14 + internal/cmd/commands/rolescmd/funcs.go | 12 +- internal/cmd/config/config.go | 10 +- internal/cmd/config/options.go | 72 ++- internal/daemon/controller/auth/auth.go | 1 + .../controller/handlers/roles/role_service.go | 38 +- .../handlers/roles/role_service_test.go | 79 ++-- internal/gen/controller.swagger.json | 10 +- internal/iam/repository_role_grant.go | 10 +- internal/iam/repository_role_grant_test.go | 14 +- internal/iam/repository_scope.go | 14 +- internal/iam/role_grant.go | 12 +- internal/iam/role_grant_test.go | 15 +- internal/iam/testing.go | 2 +- internal/perms/acl.go | 54 ++- internal/perms/acl_test.go | 94 +++- internal/perms/grants.go | 428 +++++++++++------- internal/perms/grants_test.go | 356 +++++++++++++-- internal/perms/output_fields_test.go | 5 +- .../api/resources/roles/v1/role.proto | 6 +- .../controller/api/resources/roles/role.pb.go | 156 ++++--- version/feature_manager.go | 7 + 25 files changed, 1043 insertions(+), 381 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ce55ace..dbd9937ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. ## Next +### New and Improved + +* roles: In grants, the `id` field has been changed to `ids` (but `id` will + still be accepted for now, up until 0.15.0). In the `ids` field, multiple IDs + can now be specified in a grant, either via commas (text format) or array + (JSON format). ([PR](https://github.com/hashicorp/boundary/pull/3263)). + +### Deprecations/Changes + +* Grants can now accept more than one ID per grant string (or entry in JSON) via + the `ids` parameter. In 0.15.0 the ability to add new grants via the `id` + parameter will be removed. + ## 0.13 (2023/06/13) ### New and Improved diff --git a/api/roles/grant_json.gen.go b/api/roles/grant_json.gen.go index 6612a5cf1..f2d4c069b 100644 --- a/api/roles/grant_json.gen.go +++ b/api/roles/grant_json.gen.go @@ -6,6 +6,7 @@ package roles type GrantJson struct { Id string `json:"id,omitempty"` + Ids []string `json:"ids,omitempty"` Type string `json:"type,omitempty"` Actions []string `json:"actions,omitempty"` } diff --git a/globals/fields.go b/globals/fields.go index f5ac55674..2b8d3e5eb 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -5,6 +5,7 @@ package globals const ( IdField = "id" + IdsField = "ids" VersionField = "version" NameField = "name" DescriptionField = "description" diff --git a/globals/prefixes.go b/globals/prefixes.go index 3e8adae7d..c1bd26e73 100644 --- a/globals/prefixes.go +++ b/globals/prefixes.go @@ -163,6 +163,14 @@ var prefixToResourceType = map[string]resource.Type{ SessionRecordingPrefix: resource.SessionRecording, } +var resourceTypeToPrefixes map[resource.Type][]string = func() map[resource.Type][]string { + ret := make(map[resource.Type][]string) + for k, v := range prefixToResourceType { + ret[v] = append(ret[v], k) + } + return ret +}() + // ResourceTypeFromPrefix takes in a resource ID (or a prefix) and returns the // corresponding resource typ func ResourceTypeFromPrefix(in string) resource.Type { @@ -170,3 +178,9 @@ func ResourceTypeFromPrefix(in string) resource.Type { in, _, _ = strings.Cut(in, "_") return prefixToResourceType[in] } + +// ResourcePrefixesFromType returns the known prefixes for a given type; if a +// type is not known the return value will be nil +func ResourcePrefixesFromType(in resource.Type) []string { + return resourceTypeToPrefixes[in] +} diff --git a/internal/cmd/commands/rolescmd/funcs.go b/internal/cmd/commands/rolescmd/funcs.go index 23b1f4c20..8cbfaf801 100644 --- a/internal/cmd/commands/rolescmd/funcs.go +++ b/internal/cmd/commands/rolescmd/funcs.go @@ -13,6 +13,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/types/scope" + "github.com/hashicorp/boundary/version" "github.com/mitchellh/go-wordwrap" ) @@ -218,11 +219,20 @@ func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]roles.Opti if len(c.flagGrants) > 0 { for _, grant := range c.flagGrants { - _, err := perms.Parse(scope.Global.String(), grant) + parsed, err := perms.Parse(c.Context, scope.Global.String(), grant) if err != nil { c.UI.Error(fmt.Errorf("Grant %q could not be parsed successfully: %w", grant, err).Error()) return false } + switch { + case parsed.Id() == "": + // Nothing + case version.SupportsFeature(version.Binary, version.SupportIdInGrants): + c.UI.Warn(fmt.Sprintf("Grant %q uses the %q field, which is deprecated and will not be allowed in version 0.15.0+. Please use %q instead.", grant, "id", "ids")) + default: + c.UI.Error(fmt.Sprintf("Grant %q uses the %q field which is no longer supported. Please use %q instead.", grant, "id", "ids")) + return false + } } } diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 6d099dbf4..feeb2c1c0 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -346,7 +346,10 @@ func DevWorker(opt ...Option) (*Config, error) { if err != nil { return nil, fmt.Errorf("error parsing dev config: %w", err) } - opts := getOpts(opt...) + opts, err := getOpts(opt...) + if err != nil { + return nil, fmt.Errorf("error parsing options: %w", err) + } parsed.Eventing.AuditEnabled = opts.withAuditEventsEnabled parsed.Eventing.ObservationsEnabled = opts.withObservationsEnabled parsed.Eventing.SysEventsEnabled = opts.withSysEventsEnabled @@ -389,7 +392,10 @@ func DevController(opt ...Option) (*Config, error) { parsed.DevWorkerAuthKey = workerAuthKey parsed.DevBsrKey = bsrKey parsed.DevRecoveryKey = recoveryKey - opts := getOpts(opt...) + opts, err := getOpts(opt...) + if err != nil { + return nil, fmt.Errorf("error parsing options: %w", err) + } parsed.Eventing.AuditEnabled = opts.withAuditEventsEnabled parsed.Eventing.ObservationsEnabled = opts.withObservationsEnabled parsed.Eventing.SysEventsEnabled = opts.withSysEventsEnabled diff --git a/internal/cmd/config/options.go b/internal/cmd/config/options.go index 2a50d8da6..aed401b7a 100644 --- a/internal/cmd/config/options.go +++ b/internal/cmd/config/options.go @@ -5,19 +5,30 @@ package config import ( "os" "testing" + + "github.com/hashicorp/go-secure-stdlib/parseutil" ) // getOpts - iterate the inbound Options and return a struct -func getOpts(opt ...Option) options { - opts := getDefaultOptions() - for _, o := range opt { - o(&opts) +func getOpts(opt ...Option) (options, error) { + opts, err := getDefaultOptions() + if err != nil { + return opts, err } - return opts + for _, o := range opt { + if o == nil { + continue + } + err = o(&opts) + if err != nil { + return opts, err + } + } + return opts, nil } // Option - how Options are passed as arguments -type Option func(*options) +type Option func(*options) error // options = how options are represented type options struct { @@ -27,50 +38,65 @@ type options struct { testWithErrorEventsEnabled bool } -func getDefaultOptions() options { +func getDefaultOptions() (options, error) { opts := options{} - if os.Getenv("BOUNDARY_ENABLE_TEST_SYS_EVENTS") != "" { - opts.withSysEventsEnabled = true - } - if os.Getenv("BOUNDARY_ENABLE_TEST_AUDIT_EVENTS") != "" { - opts.withAuditEventsEnabled = true - } - if os.Getenv("BOUNDARY_ENABLE_TEST_OBSERVATIONS") != "" { - opts.withObservationsEnabled = true - } - if os.Getenv("BOUNDARY_ENABLE_TEST_ERROR_EVENTS") != "" { - opts.testWithErrorEventsEnabled = true + sysEvents, err := parseutil.ParseBool(os.Getenv("BOUNDARY_ENABLE_TEST_SYS_EVENTS")) + if err != nil { + return opts, err } + opts.withSysEventsEnabled = sysEvents - return opts + auditEvents, err := parseutil.ParseBool(os.Getenv("BOUNDARY_ENABLE_TEST_AUDIT_EVENTS")) + if err != nil { + return opts, err + } + opts.withAuditEventsEnabled = auditEvents + + obs, err := parseutil.ParseBool(os.Getenv("BOUNDARY_ENABLE_TEST_OBSERVATIONS")) + if err != nil { + return opts, err + } + opts.withObservationsEnabled = obs + + errEvents, err := parseutil.ParseBool(os.Getenv("BOUNDARY_ENABLE_TEST_ERROR_EVENTS")) + if err != nil { + return opts, err + } + opts.testWithErrorEventsEnabled = errEvents + + return opts, nil } // WithSysEventsEnabled provides an option for enabling system events func WithSysEventsEnabled(enable bool) Option { - return func(o *options) { + return func(o *options) error { o.withSysEventsEnabled = enable + return nil } } // WithAuditEventsEnabled provides an option for enabling audit events func WithAuditEventsEnabled(enable bool) Option { - return func(o *options) { + return func(o *options) error { o.withAuditEventsEnabled = enable + return nil } } // WithObservationsEnabled provides an option for enabling observation events func WithObservationsEnabled(enable bool) Option { - return func(o *options) { + return func(o *options) error { o.withObservationsEnabled = enable + return nil } } // TestWithErrorEventsEnabled provides an option for enabling error events // during tests. func TestWithErrorEventsEnabled(_ testing.TB, enable bool) Option { - return func(o *options) { + return func(o *options) error { o.testWithErrorEventsEnabled = enable + return nil } } diff --git a/internal/daemon/controller/auth/auth.go b/internal/daemon/controller/auth/auth.go index 1a058f76c..72124c27f 100644 --- a/internal/daemon/controller/auth/auth.go +++ b/internal/daemon/controller/auth/auth.go @@ -656,6 +656,7 @@ func (v verifier) performAuthCheck(ctx context.Context) ( permsOpts = append(permsOpts, perms.WithAccountId(*userData.Account.Id)) } parsed, err := perms.Parse( + ctx, pair.ScopeId, pair.Grant, permsOpts...) diff --git a/internal/daemon/controller/handlers/roles/role_service.go b/internal/daemon/controller/handlers/roles/role_service.go index 766a8e7f9..88d55a19d 100644 --- a/internal/daemon/controller/handlers/roles/role_service.go +++ b/internal/daemon/controller/handlers/roles/role_service.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/roles" + "github.com/hashicorp/boundary/version" "github.com/hashicorp/go-secure-stdlib/strutil" "google.golang.org/grpc/codes" "google.golang.org/protobuf/types/known/wrapperspb" @@ -401,7 +402,7 @@ func (s Service) RemoveRolePrincipals(ctx context.Context, req *pbs.RemoveRolePr func (s Service) AddRoleGrants(ctx context.Context, req *pbs.AddRoleGrantsRequest) (*pbs.AddRoleGrantsResponse, error) { const op = "roles.(Service).AddRoleGrants" - if err := validateAddRoleGrantsRequest(req); err != nil { + if err := validateAddRoleGrantsRequest(ctx, req); err != nil { return nil, err } authResults := s.authResult(ctx, req.GetId(), action.AddGrants) @@ -439,7 +440,7 @@ func (s Service) AddRoleGrants(ctx context.Context, req *pbs.AddRoleGrantsReques func (s Service) SetRoleGrants(ctx context.Context, req *pbs.SetRoleGrantsRequest) (*pbs.SetRoleGrantsResponse, error) { const op = "roles.(Service).SetRoleGrants" - if err := validateSetRoleGrantsRequest(req); err != nil { + if err := validateSetRoleGrantsRequest(ctx, req); err != nil { return nil, err } authResults := s.authResult(ctx, req.GetId(), action.SetGrants) @@ -477,7 +478,7 @@ func (s Service) SetRoleGrants(ctx context.Context, req *pbs.SetRoleGrantsReques func (s Service) RemoveRoleGrants(ctx context.Context, req *pbs.RemoveRoleGrantsRequest) (*pbs.RemoveRoleGrantsResponse, error) { const op = "roles.(Service).RemoveRoleGrants" - if err := validateRemoveRoleGrantsRequest(req); err != nil { + if err := validateRemoveRoleGrantsRequest(ctx, req); err != nil { return nil, err } authResults := s.authResult(ctx, req.GetId(), action.RemoveGrants) @@ -853,7 +854,7 @@ func toProto(ctx context.Context, in *iam.Role, principals []*iam.PrincipalRole, } if outputFields.Has(globals.GrantsField) { for _, g := range grants { - parsed, err := perms.Parse(in.GetGrantScopeId(), g.GetRawGrant()) + parsed, err := perms.Parse(ctx, in.GetGrantScopeId(), g.GetRawGrant()) if err != nil { // This should never happen as we validate on the way in, but let's // return what we can since we are still returning the raw grant @@ -869,6 +870,7 @@ func toProto(ctx context.Context, in *iam.Role, principals []*iam.PrincipalRole, Canonical: g.GetCanonicalGrant(), Json: &pb.GrantJson{ Id: parsed.Id(), + Ids: parsed.Ids(), Type: parsed.Type().String(), Actions: actions, }, @@ -1039,7 +1041,7 @@ func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) e return nil } -func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { +func validateAddRoleGrantsRequest(ctx context.Context, req *pbs.AddRoleGrantsRequest) error { badFields := map[string]string{} if !handlers.ValidId(handlers.Id(req.GetId()), globals.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." @@ -1055,7 +1057,7 @@ func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { badFields["grant_strings"] = "Grant strings must not be empty." break } - grant, err := perms.Parse("p_anything", v) + grant, err := perms.Parse(ctx, "p_anything", v) if err != nil { badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) break @@ -1066,6 +1068,14 @@ func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { badFields["grant_strings"] = fmt.Sprintf("Action %q has been deprecated and is not allowed to be set in grants. Use %q instead.", actStr, depAct.String()) } } + switch { + case grant.Id() == "": + // Nothing + case version.SupportsFeature(version.Binary, version.SupportIdInGrants): + // This will warn on the CLI + default: + badFields["grant_strings"] = fmt.Sprintf("Grant %q uses the %q field which is no longer supported. Please use %q instead.", v, "id", "ids") + } } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields) @@ -1073,7 +1083,7 @@ func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { return nil } -func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { +func validateSetRoleGrantsRequest(ctx context.Context, req *pbs.SetRoleGrantsRequest) error { badFields := map[string]string{} if !handlers.ValidId(handlers.Id(req.GetId()), globals.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." @@ -1086,7 +1096,7 @@ func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { badFields["grant_strings"] = "Grant strings must not be empty." break } - grant, err := perms.Parse("p_anything", v) + grant, err := perms.Parse(ctx, "p_anything", v) if err != nil { badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) break @@ -1097,6 +1107,14 @@ func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { badFields["grant_strings"] = fmt.Sprintf("Action %q has been deprecated and is not allowed to be set in grants. Use %q instead.", actStr, depAct.String()) } } + switch { + case grant.Id() == "": + // Nothing + case version.SupportsFeature(version.Binary, version.SupportIdInGrants): + // This will warn on the CLI + default: + badFields["grant_strings"] = fmt.Sprintf("Grant %q uses the %q field which is no longer supported. Please use %q instead.", v, "id", "ids") + } } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields) @@ -1104,7 +1122,7 @@ func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { return nil } -func validateRemoveRoleGrantsRequest(req *pbs.RemoveRoleGrantsRequest) error { +func validateRemoveRoleGrantsRequest(ctx context.Context, req *pbs.RemoveRoleGrantsRequest) error { badFields := map[string]string{} if !handlers.ValidId(handlers.Id(req.GetId()), globals.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." @@ -1120,7 +1138,7 @@ func validateRemoveRoleGrantsRequest(req *pbs.RemoveRoleGrantsRequest) error { badFields["grant_strings"] = "Grant strings must not be empty." break } - if _, err := perms.Parse("p_anything", v); err != nil { + if _, err := perms.Parse(ctx, "p_anything", v); err != nil { badFields["grant_strings"] = fmt.Sprintf("Improperly formatted grant %q.", v) break } diff --git a/internal/daemon/controller/handlers/roles/role_service_test.go b/internal/daemon/controller/handlers/roles/role_service_test.go index d68f25b3b..627a4444a 100644 --- a/internal/daemon/controller/handlers/roles/role_service_test.go +++ b/internal/daemon/controller/handlers/roles/role_service_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/roles" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" + "github.com/hashicorp/boundary/version" "github.com/kr/pretty" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" @@ -571,7 +572,7 @@ func TestCreate(t *testing.T) { func TestUpdate(t *testing.T) { grantString := "id=*;type=*;actions=*" - g, err := perms.Parse("global", grantString) + g, err := perms.Parse(context.Background(), "global", grantString) require.NoError(t, err) _, actions := g.Actions() grant := &pb.Grant{ @@ -579,6 +580,7 @@ func TestUpdate(t *testing.T) { Canonical: g.CanonicalString(), Json: &pb.GrantJson{ Id: g.Id(), + Ids: g.Ids(), Type: g.Type().String(), Actions: actions, }, @@ -1632,7 +1634,7 @@ func checkEqualGrants(t *testing.T, expected []string, got *pb.Role) { require.Equal(len(expected), len(got.GrantStrings)) require.Equal(len(expected), len(got.Grants)) for i, v := range expected { - parsed, err := perms.Parse("o_abc123", v) + parsed, err := perms.Parse(context.Background(), "o_abc123", v) require.NoError(err) assert.Equal(expected[i], got.GrantStrings[i]) assert.Equal(expected[i], got.Grants[i].GetRaw()) @@ -1640,6 +1642,7 @@ func checkEqualGrants(t *testing.T, expected []string, got *pb.Role) { j := got.Grants[i].GetJson() require.NotNil(j) assert.Equal(parsed.Id(), j.GetId()) + assert.Equal(parsed.Ids(), j.GetIds()) assert.Equal(parsed.Type().String(), j.GetType()) _, acts := parsed.Actions() assert.Equal(acts, j.GetActions()) @@ -1666,34 +1669,43 @@ func TestAddGrants(t *testing.T) { }{ { name: "Add grant on empty role", - add: []string{"id=*;type=*;actions=delete"}, - result: []string{"id=*;type=*;actions=delete"}, + add: []string{"ids=*;type=*;actions=delete"}, + result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Add grant on role with grant", existing: []string{"id=u_foo;actions=read"}, - add: []string{"id=*;type=*;actions=delete"}, - result: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, + add: []string{"ids=*;type=*;actions=delete"}, + result: []string{"id=u_foo;actions=read", "ids=*;type=*;actions=delete"}, }, { name: "Add duplicate grant on role with grant", existing: []string{"id=u_fooaA1;actions=read"}, - add: []string{"id=*;type=*;actions=delete", "id=*;type=*;actions=delete"}, - result: []string{"id=u_fooaA1;actions=read", "id=*;type=*;actions=delete"}, + add: []string{"ids=*;type=*;actions=delete", "ids=*;type=*;actions=delete"}, + result: []string{"id=u_fooaA1;actions=read", "ids=*;type=*;actions=delete"}, }, { name: "Add grant matching existing grant", - existing: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, - add: []string{"id=*;type=*;actions=delete"}, + existing: []string{"ids=u_foo;actions=read", "ids=*;type=*;actions=delete"}, + add: []string{"ids=*;type=*;actions=delete"}, wantErr: true, }, { - name: "Check deprecation", + name: "Check add-host-sets deprecation", existing: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, - add: []string{"id=*;type=target;actions=add-host-sets"}, + add: []string{"ids=*;type=target;actions=add-host-sets"}, wantErr: true, wantErrContains: "Use \\\"add-host-sources\\\" instead", }, + { + name: "Check id field deprecation", + existing: []string{"id=u_fooaA1;actions=read"}, + add: []string{"id=*;type=*;actions=delete"}, + result: []string{"id=u_fooaA1;actions=read", "id=*;type=*;actions=delete"}, + wantErr: func() bool { + return !version.SupportsFeature(version.Binary, version.SupportIdInGrants) + }(), + }, } for _, tc := range addCases { @@ -1739,7 +1751,7 @@ func TestAddGrants(t *testing.T) { name: "Bad Version", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), - GrantStrings: []string{"id=*;type=*;actions=create"}, + GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), @@ -1748,7 +1760,7 @@ func TestAddGrants(t *testing.T) { name: "Bad Role Id", req: &pbs.AddRoleGrantsRequest{ Id: "bad id", - GrantStrings: []string{"id=*;type=*;actions=create"}, + GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -1757,7 +1769,7 @@ func TestAddGrants(t *testing.T) { name: "Unparseable Grant", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), - GrantStrings: []string{"id=*;type=*;actions=create", "unparseable"}, + GrantStrings: []string{"ids=*;type=*;actions=create", "unparseable"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -1766,7 +1778,7 @@ func TestAddGrants(t *testing.T) { name: "Empty Grant", req: &pbs.AddRoleGrantsRequest{ Id: role.GetPublicId(), - GrantStrings: []string{"id=*;type=*;actions=create", ""}, + GrantStrings: []string{"ids=*;type=*;actions=create", ""}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -1805,26 +1817,26 @@ func TestSetGrants(t *testing.T) { }{ { name: "Set grant on empty role", - set: []string{"id=*;type=*;actions=delete"}, - result: []string{"id=*;type=*;actions=delete"}, + set: []string{"ids=*;type=*;actions=delete"}, + result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set grant on role with grant", existing: []string{"id=u_foo;actions=read"}, - set: []string{"id=*;type=*;actions=delete"}, - result: []string{"id=*;type=*;actions=delete"}, + set: []string{"ids=*;type=*;actions=delete"}, + result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set grant matching existing grant", existing: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, - set: []string{"id=*;type=*;actions=delete"}, - result: []string{"id=*;type=*;actions=delete"}, + set: []string{"ids=*;type=*;actions=delete"}, + result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set duplicate grant matching existing grant", existing: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, - set: []string{"id=*;type=*;actions=delete", "id=*;type=*;actions=delete"}, - result: []string{"id=*;type=*;actions=delete"}, + set: []string{"ids=*;type=*;actions=delete", "ids=*;type=*;actions=delete"}, + result: []string{"ids=*;type=*;actions=delete"}, }, { name: "Set empty on role", @@ -1833,12 +1845,21 @@ func TestSetGrants(t *testing.T) { result: nil, }, { - name: "Check deprecation", + name: "Check add-host-sets deprecation", existing: []string{"id=u_foo;actions=read", "id=*;type=*;actions=delete"}, - set: []string{"id=*;type=target;actions=add-host-sets"}, + set: []string{"ids=*;type=target;actions=add-host-sets"}, wantErr: true, wantErrContains: "Use \\\"add-host-sources\\\" instead", }, + { + name: "Check id field deprecation", + existing: []string{"id=u_fooaA1;actions=read"}, + set: []string{"id=*;type=*;actions=delete"}, + result: []string{"id=*;type=*;actions=delete"}, + wantErr: func() bool { + return !version.SupportsFeature(version.Binary, version.SupportIdInGrants) + }(), + }, } for _, tc := range setCases { @@ -1885,7 +1906,7 @@ func TestSetGrants(t *testing.T) { name: "Bad Version", req: &pbs.SetRoleGrantsRequest{ Id: role.GetPublicId(), - GrantStrings: []string{"id=*;type=*;actions=create"}, + GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion() + 2, }, err: handlers.ApiErrorWithCode(codes.Internal), @@ -1894,7 +1915,7 @@ func TestSetGrants(t *testing.T) { name: "Bad Role Id", req: &pbs.SetRoleGrantsRequest{ Id: "bad id", - GrantStrings: []string{"id=*;type=*;actions=create"}, + GrantStrings: []string{"ids=*;type=*;actions=create"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -1903,7 +1924,7 @@ func TestSetGrants(t *testing.T) { name: "Unparsable grant", req: &pbs.SetRoleGrantsRequest{ Id: role.GetPublicId(), - GrantStrings: []string{"id=*;type=*;actions=create", "unparseable"}, + GrantStrings: []string{"ids=*;type=*;actions=create", "unparseable"}, Version: role.GetVersion(), }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index 632772f5a..f59cd01e6 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -5248,7 +5248,15 @@ "properties": { "id": { "type": "string", - "description": "Output only. The ID, if set.", + "description": "Output only. The ID, if set.\nDeprecated: use \"ids\" instead.", + "readOnly": true + }, + "ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Output only. The IDs, if set.", "readOnly": true }, "type": { diff --git a/internal/iam/repository_role_grant.go b/internal/iam/repository_role_grant.go index 711c79d74..6c12ae142 100644 --- a/internal/iam/repository_role_grant.go +++ b/internal/iam/repository_role_grant.go @@ -34,7 +34,7 @@ func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersi newRoleGrants := make([]any, 0, len(grants)) for _, grant := range grants { - roleGrant, err := NewRoleGrant(roleId, grant) + roleGrant, err := NewRoleGrant(ctx, roleId, grant) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } @@ -169,7 +169,7 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe deleteRoleGrants := make([]any, 0, len(grants)) for _, grant := range grants { // Use a fake scope, just want to get out a canonical string - perm, err := perms.Parse("o_abcd1234", grant, perms.WithSkipFinalValidation(true)) + perm, err := perms.Parse(ctx, "o_abcd1234", grant, perms.WithSkipFinalValidation(true)) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } @@ -178,7 +178,7 @@ func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVe continue } - roleGrant, err := NewRoleGrant(roleId, grant) + roleGrant, err := NewRoleGrant(ctx, roleId, grant) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } @@ -261,7 +261,7 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi deleteRoleGrants := make([]any, 0, len(grants)) for _, grant := range grants { // Use a fake scope, just want to get out a canonical string - perm, err := perms.Parse("o_abcd1234", grant, perms.WithSkipFinalValidation(true)) + perm, err := perms.Parse(ctx, "o_abcd1234", grant, perms.WithSkipFinalValidation(true)) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing grant string")) } @@ -277,7 +277,7 @@ func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersi } // Not found, so add - rg, err = NewRoleGrant(roleId, grant) + rg, err = NewRoleGrant(ctx, roleId, grant) if err != nil { return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } diff --git a/internal/iam/repository_role_grant_test.go b/internal/iam/repository_role_grant_test.go index 6594c9dd1..051a40844 100644 --- a/internal/iam/repository_role_grant_test.go +++ b/internal/iam/repository_role_grant_test.go @@ -223,6 +223,7 @@ func TestRepository_ListRoleGrants(t *testing.T) { func TestRepository_DeleteRoleGrants(t *testing.T) { t.Parallel() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) @@ -340,7 +341,7 @@ func TestRepository_DeleteRoleGrants(t *testing.T) { grants := make([]*RoleGrant, 0, tt.args.createCnt) grantStrings := make([]string, 0, tt.args.createCnt) for i := 0; i < tt.args.createCnt; i++ { - g, err := NewRoleGrant(tt.args.role.PublicId, fmt.Sprintf("actions=*;id=s_%d", i), tt.args.opt...) + g, err := NewRoleGrant(ctx, tt.args.role.PublicId, fmt.Sprintf("actions=*;id=s_%d", i), tt.args.opt...) require.NoError(err) grantStrings = append(grantStrings, g.RawGrant) grants = append(grants, g) @@ -405,6 +406,7 @@ func TestRepository_DeleteRoleGrants(t *testing.T) { func TestRepository_SetRoleGrants_Randomize(t *testing.T) { t.Parallel() + ctx := context.Background() require := require.New(t) conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) @@ -421,7 +423,7 @@ func TestRepository_SetRoleGrants_Randomize(t *testing.T) { totalCnt := 30 grants := make([]*roleGrantWrapper, 0, totalCnt) for i := 0; i < totalCnt; i++ { - g, err := NewRoleGrant(role.PublicId, fmt.Sprintf("id=s_%d;actions=*", i)) + g, err := NewRoleGrant(ctx, role.PublicId, fmt.Sprintf("id=s_%d;actions=*", i)) require.NoError(err) grants = append(grants, &roleGrantWrapper{ grantString: g.RawGrant, @@ -446,16 +448,16 @@ func TestRepository_SetRoleGrants_Randomize(t *testing.T) { // First time, run a couple of error conditions if i == 1 { - _, _, err := repo.SetRoleGrants(context.Background(), "", 1, []string{}) + _, _, err := repo.SetRoleGrants(ctx, "", 1, []string{}) require.Error(err) - _, _, err = repo.SetRoleGrants(context.Background(), role.PublicId, 1, nil) + _, _, err = repo.SetRoleGrants(ctx, role.PublicId, 1, nil) require.Error(err) } - _, _, err := repo.SetRoleGrants(context.Background(), role.PublicId, uint32(i), grantsToSet) + _, _, err := repo.SetRoleGrants(ctx, role.PublicId, uint32(i), grantsToSet) require.NoError(err) - roleGrants, err := repo.ListRoleGrants(context.Background(), role.PublicId) + roleGrants, err := repo.ListRoleGrants(ctx, role.PublicId) require.NoError(err) require.Equal(len(grantsToSet), len(roleGrants)) for _, rg := range roleGrants { diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index e06a2d226..7f0609334 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -223,7 +223,7 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o msgs = append(msgs, &roleOplogMsg) - roleGrant, err := NewRoleGrant(adminRolePublicId, "id=*;type=*;actions=*") + roleGrant, err := NewRoleGrant(ctx, adminRolePublicId, "id=*;type=*;actions=*") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } @@ -291,38 +291,38 @@ func (r *Repository) CreateScope(ctx context.Context, s *Scope, userId string, o switch s.Type { case scope.Project.String(): - roleGrant, err := NewRoleGrant(defaultRolePublicId, "id=*;type=session;actions=list,read:self,cancel:self") + roleGrant, err := NewRoleGrant(ctx, defaultRolePublicId, "id=*;type=session;actions=list,read:self,cancel:self") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "type=target;actions=list") + roleGrant, err = NewRoleGrant(ctx, defaultRolePublicId, "type=target;actions=list") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } grants = append(grants, roleGrant) default: - roleGrant, err := NewRoleGrant(defaultRolePublicId, "id=*;type=scope;actions=list,no-op") + roleGrant, err := NewRoleGrant(ctx, defaultRolePublicId, "id=*;type=scope;actions=list,no-op") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "id=*;type=auth-method;actions=authenticate,list") + roleGrant, err = NewRoleGrant(ctx, defaultRolePublicId, "id=*;type=auth-method;actions=authenticate,list") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "id={{.Account.Id}};actions=read,change-password") + roleGrant, err = NewRoleGrant(ctx, defaultRolePublicId, "id={{.Account.Id}};actions=read,change-password") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } grants = append(grants, roleGrant) - roleGrant, err = NewRoleGrant(defaultRolePublicId, "id=*;type=auth-token;actions=list,read:self,delete:self") + roleGrant, err = NewRoleGrant(ctx, defaultRolePublicId, "id=*;type=auth-token;actions=list,read:self,delete:self") if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant")) } diff --git a/internal/iam/role_grant.go b/internal/iam/role_grant.go index 4047a2ce2..05e6592da 100644 --- a/internal/iam/role_grant.go +++ b/internal/iam/role_grant.go @@ -28,21 +28,21 @@ var ( ) // NewRoleGrant creates a new in memory role grant -func NewRoleGrant(roleId string, grant string, _ ...Option) (*RoleGrant, error) { +func NewRoleGrant(ctx context.Context, roleId string, grant string, _ ...Option) (*RoleGrant, error) { const op = "iam.NewRoleGrant" if roleId == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing role id") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id") } if grant == "" { - return nil, errors.NewDeprecated(errors.InvalidParameter, op, "missing grant") + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grant") } // Validate that the grant parses successfully. Note that we fake the scope // here to avoid a lookup as the scope is only relevant at actual ACL // checking time and we just care that it parses correctly. - perm, err := perms.Parse("o_abcd1234", grant) + perm, err := perms.Parse(ctx, "o_abcd1234", grant) if err != nil { - return nil, errors.WrapDeprecated(err, op, errors.WithMsg("parsing grant string")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } rg := &RoleGrant{ RoleGrant: &store.RoleGrant{ @@ -80,7 +80,7 @@ func (g *RoleGrant) VetForWrite(ctx context.Context, _ db.Reader, _ db.OpType, _ // checking time and we just care that it parses correctly. We may have // already done this in NewRoleGrant, but we re-check and set it here // anyways because it should still be part of the vetting process. - perm, err := perms.Parse("o_abcd1234", g.RawGrant) + perm, err := perms.Parse(ctx, "o_abcd1234", g.RawGrant) if err != nil { return errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string")) } diff --git a/internal/iam/role_grant_test.go b/internal/iam/role_grant_test.go index ca63f81cc..f8d13b9a5 100644 --- a/internal/iam/role_grant_test.go +++ b/internal/iam/role_grant_test.go @@ -17,6 +17,7 @@ import ( func TestRoleGrant_Create(t *testing.T) { t.Parallel() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) @@ -90,7 +91,7 @@ func TestRoleGrant_Create(t *testing.T) { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) db.TestDeleteWhere(t, conn, func() any { a := allocRoleGrant(); return &a }(), "1=1") - got, err := NewRoleGrant(tt.args.roleId, tt.args.grant, tt.args.opt...) + got, err := NewRoleGrant(ctx, tt.args.roleId, tt.args.grant, tt.args.opt...) if tt.wantErr { require.Error(err) assert.True(errors.Match(errors.T(tt.wantIsErr), err)) @@ -138,6 +139,7 @@ func TestRoleGrant_Update(t *testing.T) { func TestRoleGrant_Delete(t *testing.T) { t.Parallel() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) @@ -196,10 +198,10 @@ func TestRoleGrant_Delete(t *testing.T) { assert, require := assert.New(t), require.New(t) r := allocRoleGrant() db.TestDeleteWhere(t, conn, &r, "1=1") - rg, err := NewRoleGrant(projRole.PublicId, "id=u_bcde;actions=read,update") + rg, err := NewRoleGrant(ctx, projRole.PublicId, "id=u_bcde;actions=read,update") require.NoError(err) require.NoError(rw.Create(context.Background(), rg)) - rg, err = NewRoleGrant(projRole.PublicId, "id=u_cdef;actions=read,update") + rg, err = NewRoleGrant(ctx, projRole.PublicId, "id=u_cdef;actions=read,update") require.NoError(err) require.NoError(rw.Create(context.Background(), rg)) @@ -224,6 +226,7 @@ func TestRoleGrant_Delete(t *testing.T) { func TestRoleGrant_Clone(t *testing.T) { t.Parallel() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) @@ -232,7 +235,7 @@ func TestRoleGrant_Clone(t *testing.T) { s := testOrg(t, repo, "", "") role := TestRole(t, conn, s.PublicId) - g, err := NewRoleGrant(role.PublicId, "id=*;type=*;actions=*") + g, err := NewRoleGrant(ctx, role.PublicId, "id=*;type=*;actions=*") assert.NoError(err) assert.NotNil(g) assert.Equal(g.RoleId, role.PublicId) @@ -246,13 +249,13 @@ func TestRoleGrant_Clone(t *testing.T) { s := testOrg(t, repo, "", "") role := TestRole(t, conn, s.PublicId) - g, err := NewRoleGrant(role.PublicId, "id=*;type=*;actions=*") + g, err := NewRoleGrant(ctx, role.PublicId, "id=*;type=*;actions=*") assert.NoError(err) require.NotNil(g) assert.Equal(g.RoleId, role.PublicId) assert.Equal(g.RawGrant, "id=*;type=*;actions=*") - g2, err := NewRoleGrant(role.PublicId, "id=u_foo;actions=read") + g2, err := NewRoleGrant(ctx, role.PublicId, "id=u_foo;actions=read") assert.NoError(err) require.NotNil(g2) assert.Equal(g2.RoleId, role.PublicId) diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 5951f4d66..597ffe917 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -200,7 +200,7 @@ func TestRoleGrant(t testing.TB, conn *db.DB, roleId, grant string, opt ...Optio require := require.New(t) rw := db.New(conn) - g, err := NewRoleGrant(roleId, grant, opt...) + g, err := NewRoleGrant(context.Background(), roleId, grant, opt...) require.NoError(err) err = rw.Create(context.Background(), g) require.NoError(err) diff --git a/internal/perms/acl.go b/internal/perms/acl.go index 94ad3d1ba..ebbeee790 100644 --- a/internal/perms/acl.go +++ b/internal/perms/acl.go @@ -12,10 +12,35 @@ import ( "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" ) +// AclGrant is used to decouple API-based grants from those we utilize for ACLs. +// Notably it uses a single ID per grant instead of multiple IDs. +type AclGrant struct { + // The scope ID, which will be a project ID or an org ID + scope Scope + + // The ID to use + id string + + // The type, if provided + typ resource.Type + + // The set of actions being granted + actions actionSet + + // The set of output fields granted + OutputFields *OutputFields +} + +// Actions returns the actions as a slice from the internal map, along with the +// string representations of those actions. +func (a AclGrant) Actions() ([]action.Type, []string) { + return a.actions.Actions() +} + // ACL provides an entry point into the permissions engine for determining if an // action is allowed on a resource based on a principal's (user or group) grants. type ACL struct { - scopeMap map[string][]Grant + scopeMap map[string][]AclGrant } // ACLResults provides a type for the permission's engine results so that we can @@ -27,7 +52,7 @@ type ACLResults struct { OutputFields *OutputFields // This is included but unexported for testing/debugging - scopeMap map[string][]Grant + scopeMap map[string][]AclGrant } // Permission provides information about the specific @@ -65,19 +90,38 @@ type Resource struct { Pin string `json:"pin,omitempty"` } -// NewACL creates an ACL from the grants provided. +// NewACL creates an ACL from the grants provided. Note that this converts the +// API-based Grants to AclGrants. func NewACL(grants ...Grant) ACL { ret := ACL{ - scopeMap: make(map[string][]Grant, len(grants)), + scopeMap: make(map[string][]AclGrant, len(grants)), } for _, grant := range grants { - ret.scopeMap[grant.scope.Id] = append(ret.scopeMap[grant.scope.Id], grant) + switch { + case len(grant.ids) > 0: + for _, id := range grant.ids { + ret.scopeMap[grant.scope.Id] = append(ret.scopeMap[grant.scope.Id], aclGrantFromGrant(grant, id)) + } + default: + // This handles the no-ID case as well as the deprecated single-ID case + ret.scopeMap[grant.scope.Id] = append(ret.scopeMap[grant.scope.Id], aclGrantFromGrant(grant, grant.id)) + } } return ret } +func aclGrantFromGrant(grant Grant, id string) AclGrant { + return AclGrant{ + scope: grant.scope, + id: id, + typ: grant.typ, + actions: grant.actions, + OutputFields: grant.OutputFields, + } +} + // Allowed determines if the grants for an ACL allow an action for a resource. func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option) (results ACLResults) { opts := getOpts(opt...) diff --git a/internal/perms/acl_test.go b/internal/perms/acl_test.go index fd1a5dac8..890aacc26 100644 --- a/internal/perms/acl_test.go +++ b/internal/perms/acl_test.go @@ -4,6 +4,7 @@ package perms import ( + "context" "encoding/json" "fmt" "testing" @@ -25,6 +26,8 @@ type scopeGrant struct { func Test_ACLAllowed(t *testing.T) { t.Parallel() + ctx := context.Background() + type actionAuthorized struct { action action.Type authorized bool @@ -44,8 +47,8 @@ func Test_ACLAllowed(t *testing.T) { { scope: "o_a", grants: []string{ - "id=ampw_bar;actions=read,update", - "id=ampw_baz;actions=read:self,update", + "ids=ampw_bar,ampw_baz;actions=read,update", + "id=ampw_bop;actions=read:self,update", "type=host-catalog;actions=create", "type=target;actions=list", "id=*;type=host-set;actions=list,create", @@ -55,7 +58,7 @@ func Test_ACLAllowed(t *testing.T) { scope: "o_b", grants: []string{ "id=*;type=host-set;actions=list,create", - "id=hcst_mypin;type=host;actions=*;output_fields=name,description", + "ids=hcst_mypin;type=host;actions=*;output_fields=name,description", "id=*;type=*;actions=authenticate", "id=*;type=*;output_fields=id", }, @@ -118,7 +121,7 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - name: "matching scope and id and matching action", + name: "matching scope and id and matching action first id", resource: Resource{ScopeId: "o_a", Id: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ @@ -127,6 +130,16 @@ func Test_ACLAllowed(t *testing.T) { {action: action.Delete}, }, }, + { + name: "matching scope and id and matching action second id", + resource: Resource{ScopeId: "o_a", Id: "ampw_baz"}, + scopeGrants: commonGrants, + actionsAuthorized: []actionAuthorized{ + {action: action.Read, authorized: true}, + {action: action.Update, authorized: true}, + {action: action.Delete}, + }, + }, { name: "matching scope and type and all action with valid pin", resource: Resource{ScopeId: "o_b", Pin: "hcst_mypin", Type: resource.Host}, @@ -187,7 +200,7 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - name: "matching scope, type, action, random id and bad pin", + name: "matching scope, type, action, random id and bad pin first id", resource: Resource{ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ @@ -196,6 +209,16 @@ func Test_ACLAllowed(t *testing.T) { {action: action.Read}, }, }, + { + name: "matching scope, type, action, random id and bad pin second id", + resource: Resource{ScopeId: "o_a", Id: "anything", Type: resource.HostCatalog, Pin: "ampw_baz"}, + scopeGrants: commonGrants, + actionsAuthorized: []actionAuthorized{ + {action: action.Update}, + {action: action.Delete}, + {action: action.Read}, + }, + }, { name: "wrong scope and matching type", resource: Resource{ScopeId: "o_bad", Type: resource.HostSet}, @@ -306,7 +329,7 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - name: "read self with top level read", + name: "read self with top level read first id", resource: Resource{ScopeId: "o_a", Id: "ampw_bar"}, scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ @@ -315,9 +338,18 @@ func Test_ACLAllowed(t *testing.T) { }, }, { - name: "read self only", + name: "read self with top level read second id", resource: Resource{ScopeId: "o_a", Id: "ampw_baz"}, scopeGrants: commonGrants, + actionsAuthorized: []actionAuthorized{ + {action: action.Read, authorized: true}, + {action: action.ReadSelf, authorized: true}, + }, + }, + { + name: "read self only", + resource: Resource{ScopeId: "o_a", Id: "ampw_bop"}, + scopeGrants: commonGrants, actionsAuthorized: []actionAuthorized{ {action: action.Read}, {action: action.ReadSelf, authorized: true}, @@ -360,7 +392,7 @@ func Test_ACLAllowed(t *testing.T) { var grants []Grant for _, sg := range test.scopeGrants { for _, g := range sg.grants { - grant, err := Parse(sg.scope, g, WithAccountId(test.accountId), WithUserId(test.userId)) + grant, err := Parse(ctx, sg.scope, g, WithAccountId(test.accountId), WithUserId(test.userId)) require.NoError(t, err) grants = append(grants, grant) } @@ -381,6 +413,10 @@ func Test_ACLAllowed(t *testing.T) { } func TestACL_ListPermissions(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tests := []struct { name string userId string @@ -498,8 +534,7 @@ func TestACL_ListPermissions(t *testing.T) { scope: "o_1", grants: []string{ "id=s_1;type=session;actions=list,read", - "id=s_2;type=session;actions=list,read", - "id=s_3;type=session;actions=list,read", + "ids=s_2,s_3;type=session;actions=list,read", }, }, }, @@ -581,8 +616,7 @@ func TestACL_ListPermissions(t *testing.T) { scope: "o_1", grants: []string{ "id=s_1;type=session;actions=list,no-op", - "id=s_2;type=session;actions=list,no-op", - "id=s_3;type=session;actions=list,no-op", + "ids=s_2,s_3;type=session;actions=list,no-op", }, }, }, @@ -698,8 +732,7 @@ func TestACL_ListPermissions(t *testing.T) { scope: "o_1", grants: []string{ "id=s_1;type=session;actions=list,read", - "id=s_2;type=session;actions=list,read", - "id=s_3;type=session;actions=list,read", + "ids=s_2,s_3;type=session;actions=list,read", }, }, { @@ -815,7 +848,7 @@ func TestACL_ListPermissions(t *testing.T) { var grants []Grant for _, sg := range tt.aclGrants { for _, g := range sg.grants { - grant, err := Parse(sg.scope, g, WithSkipFinalValidation(tt.skipGrantValidationChecking)) + grant, err := Parse(ctx, sg.scope, g, WithSkipFinalValidation(tt.skipGrantValidationChecking)) require.NoError(t, err) grants = append(grants, grant) } @@ -847,6 +880,8 @@ func TestJsonMarshal(t *testing.T) { func Test_AnonRestrictions(t *testing.T) { t.Parallel() + ctx := context.Background() + type input struct { name string grant string @@ -858,16 +893,30 @@ func Test_AnonRestrictions(t *testing.T) { name: "id-specific", grant: "id=foobar;actions=%s", }, + { + name: "ids-specific", + grant: "ids=foobar;actions=%s", + }, { name: "wildcard-id", grant: "id=*;type=%s;actions=%s", templatedType: true, shouldHaveSuccess: true, }, + { + name: "wildcard-ids", + grant: "ids=*;type=%s;actions=%s", + templatedType: true, + shouldHaveSuccess: true, + }, { name: "wildcard-id-and-type", grant: "id=*;type=*;actions=%s", }, + { + name: "wildcard-ids-and-type", + grant: "ids=*;type=*;actions=%s", + }, { name: "no-id", grant: "type=%s;actions=%s", @@ -883,10 +932,19 @@ func Test_AnonRestrictions(t *testing.T) { if i == resource.Controller || i == resource.Worker { continue } - for j := action.Type(1); j <= action.ReadCertificateAuthority; j++ { + for j := action.Type(1); j <= action.Download; j++ { + id := "foobar" + prefixes := globals.ResourcePrefixesFromType(resource.Type(i)) + if len(prefixes) > 0 { + id = fmt.Sprintf("%s_%s", prefixes[0], id) + // If it's global scope, correct it + if id == "global_foobar" { + id = "global" + } + } res := Resource{ ScopeId: scope.Global.String(), - Id: "foobar", + Id: id, Type: resource.Type(i), } grant := test.grant @@ -896,7 +954,7 @@ func Test_AnonRestrictions(t *testing.T) { grant = fmt.Sprintf(grant, action.Type(j).String()) } - parsedGrant, err := Parse(scope.Global.String(), grant, WithSkipFinalValidation(true)) + parsedGrant, err := Parse(ctx, scope.Global.String(), grant, WithSkipFinalValidation(true)) require.NoError(err) acl := NewACL(parsedGrant) diff --git a/internal/perms/grants.go b/internal/perms/grants.go index 8d472625a..87c423b4e 100644 --- a/internal/perms/grants.go +++ b/internal/perms/grants.go @@ -4,6 +4,7 @@ package perms import ( + "context" "encoding/json" "fmt" "sort" @@ -15,8 +16,27 @@ import ( "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/scope" + "golang.org/x/exp/slices" ) +type actionSet map[action.Type]bool + +// Actions is a helper that goes through the map and returns both the actual +// types of actions as a slice and the equivalent strings +func (a actionSet) Actions() (typs []action.Type, strs []string) { + typs = make([]action.Type, 0, len(a)) + strs = make([]string, 0, len(a)) + for k, v := range a { + // Nothing should be in there if not true, but doesn't hurt to validate + if !v { + continue + } + typs = append(typs, k) + strs = append(strs, k.String()) + } + return +} + // GrantTuple is simply a struct that can be reference from other code to return // a set of scopes and grants to parse type GrantTuple struct { @@ -37,17 +57,20 @@ type Scope struct { // Grant is a Go representation of a parsed grant type Grant struct { - // The scope ID, which will be a project ID or an org ID + // The scope, containing the ID and type scope Scope - // The ID in the grant, if provided. + // The ID of the grant, if provided. Deprecated in favor of ids. id string + // The IDs in the grant, if provided + ids []string + // The type, if provided typ resource.Type // The set of actions being granted - actions map[action.Type]bool + actions actionSet // The set of output fields granted OutputFields *OutputFields @@ -57,26 +80,25 @@ type Grant struct { actionsBeingParsed []string } +// Id returns the ID the grant refers to, if any func (g Grant) Id() string { return g.id } +// Ids returns the IDs the grant refers to, if any +func (g Grant) Ids() []string { + return g.ids +} + +// Type returns the type the grant refers to, or Unknown func (g Grant) Type() resource.Type { return g.typ } -func (g Grant) Actions() (typs []action.Type, strs []string) { - typs = make([]action.Type, 0, len(g.actions)) - strs = make([]string, 0, len(g.actions)) - for k, v := range g.actions { - // Nothing should be in there if not true, but doesn't hurt to validate - if !v { - continue - } - typs = append(typs, k) - strs = append(strs, k.String()) - } - return +// Actions returns the actions as a slice from the internal map, along with the +// string representations of those actions. +func (g Grant) Actions() ([]action.Type, []string) { + return g.actions.Actions() } // hasActionOrSubaction checks whether a grant's action set contains the given @@ -97,8 +119,13 @@ func (g Grant) clone() *Grant { ret := &Grant{ scope: g.scope, id: g.id, + ids: g.ids, typ: g.typ, } + if g.ids != nil { + ret.ids = make([]string, len(g.ids)) + copy(ret.ids, g.ids) + } if g.actionsBeingParsed != nil { ret.actionsBeingParsed = append(ret.actionsBeingParsed, g.actionsBeingParsed...) } @@ -126,6 +153,10 @@ func (g Grant) CanonicalString() string { builder = append(builder, fmt.Sprintf("id=%s", g.id)) } + if len(g.ids) > 0 { + builder = append(builder, fmt.Sprintf("ids=%s", strings.Join(g.ids, ","))) + } + if g.typ != resource.Unknown { builder = append(builder, fmt.Sprintf("type=%s", g.typ.String())) } @@ -153,6 +184,9 @@ func (g Grant) MarshalJSON() ([]byte, error) { if g.id != "" { res["id"] = g.id } + if len(g.ids) > 0 { + res["ids"] = g.ids + } if g.typ != resource.Unknown { res["type"] = g.typ.String() } @@ -190,6 +224,20 @@ func (g *Grant) unmarshalJSON(data []byte) error { } g.id = id } + if rawIds, ok := raw["ids"]; ok { + ids, ok := rawIds.([]any) + if !ok { + return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unable to interpret %q as array", "ids")) + } + g.ids = make([]string, len(ids)) + for i, id := range ids { + idStr, ok := id.(string) + if !ok { + return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unable to interpret %q element %q as string", "ids", id)) + } + g.ids[i] = idStr + } + } if rawType, ok := raw["type"]; ok { typ, ok := rawType.(string) if !ok { @@ -268,6 +316,9 @@ func (g *Grant) unmarshalText(grantString string) error { case "id": g.id = kv[1] + case "ids": + g.ids = strings.Split(kv[1], ",") + case "type": typeString := strings.ToLower(kv[1]) g.typ = resource.Map[typeString] @@ -307,13 +358,13 @@ func (g *Grant) unmarshalText(grantString string) error { // // The scope must be the org and project where this grant originated, not the // request. -func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { +func Parse(ctx context.Context, scopeId, grantString string, opt ...Option) (Grant, error) { const op = "perms.Parse" if len(grantString) == 0 { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing grant string") + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing grant string") } if scopeId == "" { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing scope id") + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope id") } grantString = strings.ToValidUTF8(grantString, string(unicode.ReplacementChar)) @@ -328,169 +379,234 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { case strings.HasPrefix(scopeId, scope.Project.Prefix()): grant.scope.Type = scope.Project default: - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, "invalid scope type") + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type") } switch { case grantString[0] == '{': if err := grant.unmarshalJSON([]byte(grantString)); err != nil { - return Grant{}, errors.WrapDeprecated(err, op, errors.WithMsg("unable to parse JSON grant string")) + return Grant{}, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse JSON grant string")) } default: if err := grant.unmarshalText(grantString); err != nil { - return Grant{}, errors.WrapDeprecated(err, op, errors.WithMsg("unable to parse grant string")) + return Grant{}, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse grant string")) } } + if grant.id != "" && len(grant.ids) > 0 { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both %q and %q fields", grantString, "id", "ids")) + } + if len(grant.ids) > 1 && slices.Contains(grant.ids, "*") { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains both wildcard and non-wildcard values in %q field", grantString, "ids")) + } + opts := getOpts(opt...) - // Check for templated values ID, and substitute in with the authenticated - // values if so. If we are using a dummy user or account ID, store the - // original ID and return it at the end; this is usually the case when - // validating grant formats. - var origId string - if grant.id != "" && strings.HasPrefix(grant.id, "{{") { - id := strings.TrimSuffix(strings.TrimPrefix(grant.id, "{{"), "}}") - id = strings.TrimSpace(id) - switch id { - case "user.id", ".User.Id": - if opts.withUserId != "" { - grant.id = strings.ToValidUTF8(opts.withUserId, string(unicode.ReplacementChar)) + var grantIds []string + var deprecatedId bool + switch { + case grant.id != "": + grantIds = []string{grant.id} + deprecatedId = true + case len(grant.ids) > 0: + grantIds = grant.ids + // Ensure we aren't seeing mixed types. We will have already filtered + // out the wildcard case above. + if len(grant.ids) > 1 { + var seenType resource.Type + for i, id := range grantIds { + if i == 0 { + seenType = globals.ResourceTypeFromPrefix(id) + continue + } + if seenType != globals.ResourceTypeFromPrefix(id) { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("input grant string %q contains ids of differently-typed resources", grantString)) + } } - // Otherwise, substitute in a dummy value - origId = grant.id - grant.id = "u_dummy" - case "account.id", ".Account.Id": - if opts.withAccountId != "" { - grant.id = strings.ToValidUTF8(opts.withAccountId, string(unicode.ReplacementChar)) - } - origId = grant.id - grant.id = "acctoidc_dummy" - default: - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unknown template %q in grant %q value", grant.id, "id")) } } - - if err := grant.validateType(); err != nil { - return Grant{}, errors.WrapDeprecated(err, op) + // It's possible that there is no id in a grant. In that case we still need + // to validate it and build up the parsed Grant. We insert an empty ID value + // in this case; the code below will check that it's non-empty before + // running any ID-specific logic on it. + if len(grantIds) == 0 { + grantIds = []string{""} } - - if err := grant.parseAndValidateActions(); err != nil { - return Grant{}, errors.WrapDeprecated(err, op) - } - - if !opts.withSkipFinalValidation { - switch { - case grant.id == "*": - // Matches - // id=*;type=sometype;actions=foo,bar - // or - // id=*;type=*;actions=foo,bar - // This can be a non-unknown type or wildcard - if grant.typ == resource.Unknown { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains wildcard id and no specified type", grant.CanonicalString())) - } - case grant.id != "": - // Non-wildcard but specified ID. This can match - // id=foo_bar;actions=foo,bar - // or - // id=foo_bar;type=sometype;actions=foo,bar - // or - // id=foo_bar;type=*;actions=foo,bar - // but notably the specified types have to actually make sense: in - // the second example the type corresponding to the ID must have the - // specified type as a child type; in the third the ID must be a - // type that has child types. - idType := globals.ResourceTypeFromPrefix(grant.id) - if idType == resource.Unknown { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains an id %q of an unknown resource type", grant.CanonicalString(), grant.id)) - } - switch grant.typ { - case resource.Unknown: - // This is fine as-is but we do not support collection actions - // without a type (either directly specified or wildcard) so - // check that - if grant.actions[action.Create] || - grant.actions[action.List] { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains create or list action in a format that does not allow these", grant.CanonicalString())) - } - case resource.All: - // Verify that the ID is a type that has child types - if !resource.HasChildTypes(idType) { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains an id that does not support child types", grant.CanonicalString())) - } - default: - // Specified resource type, verify it's a child - if resource.Parent(grant.typ) != idType { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains type %s that is not a child type of the type (%s) of the specified id", grant.CanonicalString(), grant.typ.String(), idType.String())) - } - } - default: // no specified id - switch grant.typ { - case resource.Unknown: - // Error -- no ID or type isn't valid (although we should never - // get to this point because original parsing should error) - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains no id or type", grant.CanonicalString())) - case resource.All: - // "type=*;actions=..." is not supported -- we require you to - // explicitly set a pin or set the ID to * - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains wildcard type with no id value", grant.CanonicalString())) - default: - // Here we have type=something,actions=. This - // means we're operating on collections and support only create - // or list. Note that wildcard actions are not okay here; that - // uses the format id=*;type=;actions=* - switch len(grant.actions) { - case 0: - // It's okay to have no actions if only output fields are being defined - if _, hasSetFields := grant.OutputFields.Fields(); !hasSetFields { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains no actions or output fields", grant.CanonicalString())) + for i, currId := range grantIds { + // Check for templated values ID, and substitute in with the authenticated + // values if so. If we are using a dummy user or account ID, store the + // original ID and return it at the end; this is usually the case when + // validating grant formats. + var origId string + if currId != "" { + if strings.HasPrefix(currId, "{{") { + id := strings.TrimSuffix(strings.TrimPrefix(currId, "{{"), "}}") + id = strings.TrimSpace(id) + switch id { + case "user.id", ".User.Id": + if opts.withUserId != "" { + grantIds[i] = strings.ToValidUTF8(opts.withUserId, string(unicode.ReplacementChar)) + } else { + // Otherwise, substitute in a dummy value + origId = currId + grantIds[i] = "u_dummy" } - case 1: - if !grant.hasActionOrSubaction(action.Create) && - !grant.hasActionOrSubaction(action.List) { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) - } - case 2: - if !grant.hasActionOrSubaction(action.Create) || !grant.hasActionOrSubaction(action.List) { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) + case "account.id", ".Account.Id": + if opts.withAccountId != "" { + grantIds[i] = strings.ToValidUTF8(opts.withAccountId, string(unicode.ReplacementChar)) + } else { + origId = currId + grantIds[i] = "acctoidc_dummy" } default: - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) + fieldName := "ids" + if deprecatedId { + fieldName = "id" + } + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown template %q in grant %q value", currId, fieldName)) } } } - // This might be zero if output fields is populated - if len(grant.actions) > 0 { - // Create a dummy resource and pass it through Allowed and ensure that - // we get allowed. - acl := NewACL(grant) - r := Resource{ - ScopeId: scopeId, - Id: grant.id, - Type: grant.typ, + + // We don't need to do these twice as they don't depend on IDs; they + // also clear state such as actionsBeingParsed + if i == 0 { + if err := grant.validateType(ctx); err != nil { + return Grant{}, errors.Wrap(ctx, err, op) } - if !resource.TopLevelType(grant.typ) { - r.Pin = grant.id + if err := grant.parseAndValidateActions(ctx); err != nil { + return Grant{}, errors.Wrap(ctx, err, op) } - var allowed bool - for k := range grant.actions { - results := acl.Allowed(r, k, globals.AnonymousUserId, WithSkipAnonymousUserRestrictions(true)) - if results.Authorized { - allowed = true - break + } + + if !opts.withSkipFinalValidation { + switch { + case grantIds[i] == "*": + // Matches + // id=*;type=sometype;actions=foo,bar + // or + // id=*;type=*;actions=foo,bar + // This can be a non-unknown type or wildcard + if grant.typ == resource.Unknown { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains wildcard id and no specified type", grant.CanonicalString())) + } + case grantIds[i] != "": + // Non-wildcard but specified ID. This can match + // id=foo_bar;actions=foo,bar + // or + // id=foo_bar;type=sometype;actions=foo,bar + // or + // id=foo_bar;type=*;actions=foo,bar + // but notably the specified types have to actually make sense: in + // the second example the type corresponding to the ID must have the + // specified type as a child type; in the third the ID must be a + // type that has child types. + idType := globals.ResourceTypeFromPrefix(grantIds[i]) + if idType == resource.Unknown { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains an id %q of an unknown resource type", grant.CanonicalString(), grantIds[i])) + } + switch grant.typ { + case resource.Unknown: + // This is fine as-is but we do not support collection actions + // without a type (either directly specified or wildcard) so + // check that + if grant.actions[action.Create] || + grant.actions[action.List] { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains create or list action in a format that does not allow these", grant.CanonicalString())) + } + case resource.All: + // Verify that the ID is a type that has child types + if !resource.HasChildTypes(idType) { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains an id that does not support child types", grant.CanonicalString())) + } + default: + // Specified resource type, verify it's a child + if resource.Parent(grant.typ) != idType { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains type %s that is not a child type of the type (%s) of the specified id", grant.CanonicalString(), grant.typ.String(), idType.String())) + } + } + default: // no specified id + switch grant.typ { + case resource.Unknown: + // Error -- no ID or type isn't valid (although we should never + // get to this point because original parsing should error) + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains no id or type", grant.CanonicalString())) + case resource.All: + // "type=*;actions=..." is not supported -- we require you to + // explicitly set a pin or set the ID to * + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains wildcard type with no id value", grant.CanonicalString())) + default: + // Here we have type=something,actions=. This + // means we're operating on collections and support only create + // or list. Note that wildcard actions are not okay here; that + // uses the format id=*;type=;actions=* + switch len(grant.actions) { + case 0: + // It's okay to have no actions if only output fields are being defined + if _, hasSetFields := grant.OutputFields.Fields(); !hasSetFields { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains no actions or output fields", grant.CanonicalString())) + } + case 1: + if !grant.hasActionOrSubaction(action.Create) && + !grant.hasActionOrSubaction(action.List) { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) + } + case 2: + if !grant.hasActionOrSubaction(action.Create) || !grant.hasActionOrSubaction(action.List) { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) + } + default: + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q contains non-create or non-list action in a format that only allows these", grant.CanonicalString())) + } } } - if !allowed { - return Grant{}, errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q would not result in any action being authorized", grant.CanonicalString())) + // This might be zero if output fields is populated + if len(grant.actions) > 0 { + // Create a dummy resource and pass it through Allowed and + // ensure that we get allowed. We need to use the templated + // grant, if any, so we send in a clone with an updated ID. + grantForValidation := grant.clone() + grantForValidation.id = grantIds[i] + acl := NewACL(*grantForValidation) + r := Resource{ + ScopeId: scopeId, + Id: grantIds[i], + Type: grant.typ, + } + if !resource.TopLevelType(grant.typ) { + r.Pin = grantIds[i] + } + var allowed bool + for k := range grant.actions { + results := acl.Allowed(r, k, globals.AnonymousUserId, WithSkipAnonymousUserRestrictions(true)) + if results.Authorized { + allowed = true + break + } + } + if !allowed { + return Grant{}, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("parsed grant string %q would not result in any action being authorized", grant.CanonicalString())) + } } } + + // If we substituted in a dummy value, replace with the original now + if origId != "" { + grantIds[i] = origId + } } - // If we substituted in a dummy value, replace with the original now - if origId != "" { - grant.id = origId + // See if we need to move grantIds back for the deprecated case. grantIds + // will always be at least size 1 since we add the empty string if no IDs + // were provided, so we can check to see if that was the case first. + switch { + case grantIds[0] == "": + // Nothing to do + case deprecatedId: + grant.id = grantIds[0] + default: + grant.ids = grantIds } return grant, nil @@ -500,16 +616,16 @@ func Parse(scopeId, grantString string, opt ...Option) (Grant, error) { // types. It does not explicitly check the resource string itself; that's the // job of the parsing functions to look up the string from the Map and ensure // it's not unknown. -func (g Grant) validateType() error { +func (g Grant) validateType(ctx context.Context) error { const op = "perms.(Grant).validateType" switch g.typ { case resource.Controller: - return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unknown type specifier %q", g.typ)) + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown type specifier %q", g.typ)) } return nil } -func (g *Grant) parseAndValidateActions() error { +func (g *Grant) parseAndValidateActions(ctx context.Context) error { const op = "perms.(Grant).parseAndValidateActions" if len(g.actionsBeingParsed) == 0 { g.actionsBeingParsed = nil @@ -518,25 +634,25 @@ func (g *Grant) parseAndValidateActions() error { if _, hasSetFields := g.OutputFields.Fields(); hasSetFields { return nil } - return errors.NewDeprecated(errors.InvalidParameter, op, "missing actions") + return errors.New(ctx, errors.InvalidParameter, op, "missing actions") } for _, a := range g.actionsBeingParsed { if a == "" { - return errors.NewDeprecated(errors.InvalidParameter, op, "empty action found") + return errors.New(ctx, errors.InvalidParameter, op, "empty action found") } if g.actions == nil { g.actions = make(map[action.Type]bool, len(g.actionsBeingParsed)) } if am := action.Map[a]; am == action.Unknown { - return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("unknown action %q", a)) + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown action %q", a)) } else { g.actions[am] = true } } if len(g.actions) > 1 && g.actions[action.All] { - return errors.NewDeprecated(errors.InvalidParameter, op, fmt.Sprintf("%q cannot be specified with other actions", action.All.String())) + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%q cannot be specified with other actions", action.All.String())) } g.actionsBeingParsed = nil diff --git a/internal/perms/grants_test.go b/internal/perms/grants_test.go index bd4114688..b77932038 100644 --- a/internal/perms/grants_test.go +++ b/internal/perms/grants_test.go @@ -4,6 +4,7 @@ package perms import ( + "context" "fmt" "testing" @@ -18,6 +19,8 @@ import ( func Test_ActionParsingValidation(t *testing.T) { t.Parallel() + ctx := context.Background() + type input struct { name string input Grant @@ -100,7 +103,7 @@ func Test_ActionParsingValidation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := test.input.parseAndValidateActions() + err := test.input.parseAndValidateActions(ctx) if test.errResult == "" { require.NoError(t, err) assert.Equal(t, test.result, test.input) @@ -114,13 +117,14 @@ func Test_ActionParsingValidation(t *testing.T) { func Test_ValidateType(t *testing.T) { t.Parallel() + ctx := context.Background() var g Grant - for i := resource.Unknown; i <= resource.Credential; i++ { + for i := resource.Unknown; i <= resource.StorageBucket; i++ { g.typ = i if i == resource.Controller { - assert.Error(t, g.validateType()) + assert.Error(t, g.validateType(ctx)) } else { - assert.NoError(t, g.validateType()) + assert.NoError(t, g.validateType(ctx)) } } } @@ -159,7 +163,31 @@ func Test_MarshalingAndCloning(t *testing.T) { canonicalString: `id=baz;type=group`, }, { - name: "output fields", + name: "type and ids", + input: Grant{ + ids: []string{"baz", "bop"}, + scope: Scope{ + Type: scope.Project, + }, + typ: resource.Group, + }, + jsonOutput: `{"ids":["baz","bop"],"type":"group"}`, + canonicalString: `ids=baz,bop;type=group`, + }, + { + name: "type and ids single id", + input: Grant{ + ids: []string{"baz"}, + scope: Scope{ + Type: scope.Project, + }, + typ: resource.Group, + }, + jsonOutput: `{"ids":["baz"],"type":"group"}`, + canonicalString: `ids=baz;type=group`, + }, + { + name: "output fields id", input: Grant{ id: "baz", scope: Scope{ @@ -178,7 +206,26 @@ func Test_MarshalingAndCloning(t *testing.T) { canonicalString: `id=baz;type=group;output_fields=id,name,version`, }, { - name: "everything", + name: "output fields ids", + input: Grant{ + ids: []string{"baz", "bop"}, + scope: Scope{ + Type: scope.Project, + }, + typ: resource.Group, + OutputFields: &OutputFields{ + fields: map[string]bool{ + "name": true, + "version": true, + "id": true, + }, + }, + }, + jsonOutput: `{"ids":["baz","bop"],"output_fields":["id","name","version"],"type":"group"}`, + canonicalString: `ids=baz,bop;type=group;output_fields=id,name,version`, + }, + { + name: "everything id", input: Grant{ id: "baz", scope: Scope{ @@ -201,6 +248,30 @@ func Test_MarshalingAndCloning(t *testing.T) { jsonOutput: `{"actions":["create","read"],"id":"baz","output_fields":["id","name","version"],"type":"group"}`, canonicalString: `id=baz;type=group;actions=create,read;output_fields=id,name,version`, }, + { + name: "everything ids", + input: Grant{ + ids: []string{"baz", "bop"}, + scope: Scope{ + Type: scope.Project, + }, + typ: resource.Group, + actions: map[action.Type]bool{ + action.Create: true, + action.Read: true, + }, + actionsBeingParsed: []string{"create", "read"}, + OutputFields: &OutputFields{ + fields: map[string]bool{ + "name": true, + "version": true, + "ids": true, + }, + }, + }, + jsonOutput: `{"actions":["create","read"],"ids":["baz","bop"],"output_fields":["ids","name","version"],"type":"group"}`, + canonicalString: `ids=baz,bop;type=group;actions=create,read;output_fields=ids,name,version`, + }, } for _, test := range tests { @@ -240,12 +311,19 @@ func Test_Unmarshaling(t *testing.T) { jsonErr: "perms.(Grant).unmarshalJSON: error occurred during decode, encoding issue: error #303: invalid character 'w' looking for beginning of value", }, { - name: "bad segment", + name: "bad segment id", jsonInput: `{"id":true}`, jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "id" as string: parameter violation: error #100`, textInput: `id=`, textErr: `perms.(Grant).unmarshalText: segment "id=" not formatted correctly, missing value: parameter violation: error #100`, }, + { + name: "bad segment ids", + jsonInput: `{"ids":true}`, + jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "ids" as array: parameter violation: error #100`, + textInput: `ids=`, + textErr: `perms.(Grant).unmarshalText: segment "ids=" not formatted correctly, missing value: parameter violation: error #100`, + }, { name: "good id", expected: Grant{ @@ -254,6 +332,14 @@ func Test_Unmarshaling(t *testing.T) { jsonInput: `{"id":"foobar"}`, textInput: `id=foobar`, }, + { + name: "good ids", + expected: Grant{ + ids: []string{"foobar"}, + }, + jsonInput: `{"ids":["foobar"]}`, + textInput: `ids=foobar`, + }, { name: "bad id", jsonInput: `{"id":true}`, @@ -261,6 +347,13 @@ func Test_Unmarshaling(t *testing.T) { textInput: `=id`, textErr: `perms.(Grant).unmarshalText: segment "=id" not formatted correctly, missing key: parameter violation: error #100`, }, + { + name: "bad ids", + jsonInput: `{"ids":true}`, + jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "ids" as array: parameter violation: error #100`, + textInput: `=ids`, + textErr: `perms.(Grant).unmarshalText: segment "=ids" not formatted correctly, missing key: parameter violation: error #100`, + }, { name: "good type", expected: Grant{ @@ -277,7 +370,7 @@ func Test_Unmarshaling(t *testing.T) { textErr: `perms.(Grant).unmarshalText: segment "type=host-catalog=id" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, }, { - name: "good output fields", + name: "good output fields id", expected: Grant{ OutputFields: &OutputFields{ fields: map[string]bool{ @@ -291,12 +384,33 @@ func Test_Unmarshaling(t *testing.T) { textInput: `output_fields=id,version,name`, }, { - name: "bad output fields", + name: "good output fields ids", + expected: Grant{ + OutputFields: &OutputFields{ + fields: map[string]bool{ + "name": true, + "version": true, + "ids": true, + }, + }, + }, + jsonInput: `{"output_fields":["ids","name","version"]}`, + textInput: `output_fields=ids,version,name`, + }, + { + name: "bad output fields id", jsonInput: `{"output_fields":true}`, jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "output_fields" as array: parameter violation: error #100`, textInput: `output_fields=id=version,name`, textErr: `perms.(Grant).unmarshalText: segment "output_fields=id=version,name" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, }, + { + name: "bad output fields ids", + jsonInput: `{"output_fields":true}`, + jsonErr: `perms.(Grant).unmarshalJSON: unable to interpret "output_fields" as array: parameter violation: error #100`, + textInput: `output_fields=ids=version,name`, + textErr: `perms.(Grant).unmarshalText: segment "output_fields=ids=version,name" not formatted correctly, wrong number of equal signs: parameter violation: error #100`, + }, { name: "good actions", expected: Grant{ @@ -359,6 +473,8 @@ func Test_Unmarshaling(t *testing.T) { func Test_Parse(t *testing.T) { t.Parallel() + ctx := context.Background() + type input struct { name string input string @@ -386,28 +502,38 @@ func Test_Parse(t *testing.T) { }, { name: "bad type", - input: "id=foobar;type=barfoo;actions=read", + input: "ids=s_foobar;type=barfoo;actions=read", err: `perms.Parse: unable to parse grant string: perms.(Grant).unmarshalText: unknown type specifier "barfoo": parameter violation: error #100`, }, { name: "bad actions", - input: "id=foobar;type=host-catalog;actions=createread", + input: "ids=hcst_foobar;type=host-catalog;actions=createread", err: `perms.Parse: perms.(Grant).parseAndValidateActions: unknown action "createread": parameter violation: error #100`, }, { name: "bad id type", - input: "id=foobar;actions=create", - err: `perms.Parse: parsed grant string "id=foobar;actions=create" contains an id "foobar" of an unknown resource type: parameter violation: error #100`, + input: "id=foobar;actions=read", + err: `perms.Parse: parsed grant string "id=foobar;actions=read" contains an id "foobar" of an unknown resource type: parameter violation: error #100`, }, { - name: "bad create action for id", - input: "id=u_foobar;actions=create", - err: `perms.Parse: parsed grant string "id=u_foobar;actions=create" contains create or list action in a format that does not allow these: parameter violation: error #100`, + name: "bad ids type first position", + input: "ids=foobar,hcst_foobar;actions=read", + err: `perms.Parse: input grant string "ids=foobar,hcst_foobar;actions=read" contains ids of differently-typed resources: parameter violation: error #100`, }, { - name: "bad create action for id with other perms", - input: "id=u_foobar;actions=read,create", - err: `perms.Parse: parsed grant string "id=u_foobar;actions=create,read" contains create or list action in a format that does not allow these: parameter violation: error #100`, + name: "bad ids type second position", + input: "ids=hcst_foobar,foobar;actions=read", + err: `perms.Parse: input grant string "ids=hcst_foobar,foobar;actions=read" contains ids of differently-typed resources: parameter violation: error #100`, + }, + { + name: "bad create action for ids", + input: "ids=u_foobar;actions=create", + err: `perms.Parse: parsed grant string "ids=u_foobar;actions=create" contains create or list action in a format that does not allow these: parameter violation: error #100`, + }, + { + name: "bad create action for ids with other perms", + input: "ids=u_foobar;actions=read,create", + err: `perms.Parse: parsed grant string "ids=u_foobar;actions=create,read" contains create or list action in a format that does not allow these: parameter violation: error #100`, }, { name: "bad list action for id", @@ -415,7 +541,7 @@ func Test_Parse(t *testing.T) { err: `perms.Parse: parsed grant string "id=u_foobar;actions=list" contains create or list action in a format that does not allow these: parameter violation: error #100`, }, { - name: "bad list action for id with other perms", + name: "bad list action for type with other perms", input: "type=host-catalog;actions=list,read", err: `perms.Parse: parsed grant string "type=host-catalog;actions=list,read" contains non-create or non-list action in a format that only allows these: parameter violation: error #100`, }, @@ -424,18 +550,38 @@ func Test_Parse(t *testing.T) { input: "id=*;actions=read", err: `perms.Parse: parsed grant string "id=*;actions=read" contains wildcard id and no specified type: parameter violation: error #100`, }, + { + name: "wildcard ids and actions without collection", + input: "ids=*;actions=read", + err: `perms.Parse: parsed grant string "ids=*;actions=read" contains wildcard id and no specified type: parameter violation: error #100`, + }, { name: "wildcard id and actions with list", input: "id=*;actions=read,list", err: `perms.Parse: parsed grant string "id=*;actions=list,read" contains wildcard id and no specified type: parameter violation: error #100`, }, { - name: "wildcard type with no id", + name: "wildcard ids and actions with list", + input: "ids=*;actions=read,list", + err: `perms.Parse: parsed grant string "ids=*;actions=list,read" contains wildcard id and no specified type: parameter violation: error #100`, + }, + { + name: "wildcard type with no ids", input: "type=*;actions=read,list", err: `perms.Parse: parsed grant string "type=*;actions=list,read" contains wildcard type with no id value: parameter violation: error #100`, }, { - name: "empty id and type", + name: "mixed wildcard and non wildcard ids first position", + input: "ids=*,u_foobar;actions=read,list", + err: `perms.Parse: input grant string "ids=*,u_foobar;actions=read,list" contains both wildcard and non-wildcard values in "ids" field: parameter violation: error #100`, + }, + { + name: "mixed wildcard and non wildcard ids second position", + input: "ids=u_foobar,*;actions=read,list", + err: `perms.Parse: input grant string "ids=u_foobar,*;actions=read,list" contains both wildcard and non-wildcard values in "ids" field: parameter violation: error #100`, + }, + { + name: "empty ids and type", input: "actions=create", err: `perms.Parse: parsed grant string "actions=create" contains no id or type: parameter violation: error #100`, }, @@ -445,10 +591,30 @@ func Test_Parse(t *testing.T) { err: `perms.Parse: parsed grant string "id=ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, }, { - name: "specified resource type non child", + name: "wildcard type non child ids first position", + input: "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create", + err: `perms.Parse: parsed grant string "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, + }, + { + name: "wildcard type non child ids second position", + input: "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create", + err: `perms.Parse: parsed grant string "ids=ttcp_1234567890,ttcp_1234567890;type=*;actions=create" contains an id that does not support child types: parameter violation: error #100`, + }, + { + name: "specified resource type non child id", input: "id=hcst_1234567890;type=account;actions=read", err: `perms.Parse: parsed grant string "id=hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, }, + { + name: "specified resource type non child ids first position", + input: "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read", + err: `perms.Parse: parsed grant string "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, + }, + { + name: "specified resource type non child ids second position", + input: "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read", + err: `perms.Parse: parsed grant string "ids=hcst_1234567890,hcst_1234567890;type=account;actions=read" contains type account that is not a child type of the type (host-catalog) of the specified id: parameter violation: error #100`, + }, { name: "no id with one bad action", input: "type=host-set;actions=read", @@ -518,6 +684,22 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "wildcard ids and type and actions with list", + input: "ids=*;type=*;actions=read,list", + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"*"}, + typ: resource.All, + actions: map[action.Type]bool{ + action.Read: true, + action.List: true, + }, + }, + }, { name: "good json type", input: `{"type":"host-catalog","actions":["create"]}`, @@ -548,7 +730,22 @@ func Test_Parse(t *testing.T) { }, }, { - name: "good json output fields", + name: "good json ids", + input: `{"ids":["hcst_foobar", "hcst_foobaz"],"actions":["read"]}`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + }, + }, + { + name: "good json output fields id", input: `{"id":"u_foobar","actions":["read"],"output_fields":["version","id","name"]}`, expected: Grant{ scope: Scope{ @@ -569,6 +766,28 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "good json output fields ids", + input: `{"ids":["u_foobar"],"actions":["read"],"output_fields":["version","ids","name"]}`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"u_foobar"}, + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + OutputFields: &OutputFields{ + fields: map[string]bool{ + "version": true, + "ids": true, + "name": true, + }, + }, + }, + }, { name: "good json output fields no action", input: `{"id":"u_foobar","output_fields":["version","id","name"]}`, @@ -618,7 +837,22 @@ func Test_Parse(t *testing.T) { }, }, { - name: "good output fields", + name: "good text ids", + input: `ids=hcst_foobar,hcst_foobaz;actions=read`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + }, + }, + { + name: "good output fields id", input: `id=u_foobar;actions=read;output_fields=version,id,name`, expected: Grant{ scope: Scope{ @@ -639,6 +873,28 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "good output fields ids", + input: `ids=hcst_foobar,hcst_foobaz;actions=read;output_fields=version,ids,name`, + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"hcst_foobar", "hcst_foobaz"}, + typ: resource.Unknown, + actions: map[action.Type]bool{ + action.Read: true, + }, + OutputFields: &OutputFields{ + fields: map[string]bool{ + "version": true, + "ids": true, + "name": true, + }, + }, + }, + }, { name: "default project scope", input: `id=hcst_foobar;actions=read`, @@ -693,6 +949,12 @@ func Test_Parse(t *testing.T) { userId: "u_abcd1234", err: `perms.Parse: unknown template "{{superman}}" in grant "id" value: parameter violation: error #100`, }, + { + name: "bad user ids template", + input: `ids={{superman}};actions=create,read`, + userId: "u_abcd1234", + err: `perms.Parse: unknown template "{{superman}}" in grant "ids" value: parameter violation: error #100`, + }, { name: "good user id template", input: `id={{ user.id}};actions=read,update`, @@ -715,6 +977,12 @@ func Test_Parse(t *testing.T) { accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix), err: `perms.Parse: unknown template "{{superman}}" in grant "id" value: parameter violation: error #100`, }, + { + name: "bad old account ids template", + input: `ids={{superman}};actions=read`, + accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPreviousPrefix), + err: `perms.Parse: unknown template "{{superman}}" in grant "ids" value: parameter violation: error #100`, + }, { name: "bad new account id template", input: `id={{superman}};actions=read`, @@ -753,13 +1021,30 @@ func Test_Parse(t *testing.T) { }, }, }, + { + name: "good ids template", + input: `ids={{ user.id}},{{ account.id}};actions=read,update`, + userId: "u_abcd1234", + accountId: fmt.Sprintf("%s_1234567890", globals.PasswordAccountPrefix), + expected: Grant{ + scope: Scope{ + Id: "o_scope", + Type: scope.Org, + }, + ids: []string{"u_abcd1234", "acctpw_1234567890"}, + actions: map[action.Type]bool{ + action.Update: true, + action.Read: true, + }, + }, + }, } - _, err := Parse("", "") + _, err := Parse(ctx, "", "") require.Error(t, err) assert.Equal(t, "perms.Parse: missing grant string: parameter violation: error #100", err.Error()) - _, err = Parse("", "{}") + _, err = Parse(ctx, "", "{}") require.Error(t, err) assert.Equal(t, "perms.Parse: missing scope id: parameter violation: error #100", err.Error()) @@ -771,7 +1056,7 @@ func Test_Parse(t *testing.T) { if test.scopeOverride != "" { scope = test.scopeOverride } - grant, err := Parse(scope, test.input, WithUserId(test.userId), WithAccountId(test.accountId)) + grant, err := Parse(ctx, scope, test.input, WithUserId(test.userId), WithAccountId(test.accountId)) if test.err != "" { require.Error(err) assert.Equal(test.err, err.Error()) @@ -833,20 +1118,27 @@ func TestHasActionOrSubaction(t *testing.T) { } func FuzzParse(f *testing.F) { + ctx := context.Background() + f.Add("type=host-catalog;actions=create") f.Add("type=*;actions=*") f.Add("id=*;type=*;actions=*") + f.Add("ids=*;type=*;actions=*") f.Add("id=*;type=*;actions=read,list") + f.Add("ids=*;type=*;actions=read,list") f.Add("id=foobar;actions=read;output_fields=version,id,name") + f.Add("ids=foobar,foobaz;actions=read;output_fields=version,id,name") f.Add("id={{account.id}};actions=update,read") - f.Add(`{id:"foobar","type":"host-catalog","actions":["create"]}`) + f.Add("ids={{account.id}},{{user.id}};actions=update,read") + f.Add(`{"id":"foobar","type":"host-catalog","actions":["create"]}`) + f.Add(`{"ids":["foobar"],"type":"host-catalog","actions":["create"]}`) f.Fuzz(func(t *testing.T, grant string) { - g, err := Parse("global", grant, WithSkipFinalValidation(true)) + g, err := Parse(ctx, "global", grant, WithSkipFinalValidation(true)) if err != nil { return } - g2, err := Parse("global", g.CanonicalString(), WithSkipFinalValidation(true)) + g2, err := Parse(ctx, "global", g.CanonicalString(), WithSkipFinalValidation(true)) if err != nil { t.Fatal("Failed to parse canonical string:", err) } @@ -857,7 +1149,7 @@ func FuzzParse(f *testing.F) { if err != nil { t.Error("Failed to marshal JSON:", err) } - g3, err := Parse("global", string(jsonBytes), WithSkipFinalValidation(true)) + g3, err := Parse(ctx, "global", string(jsonBytes), WithSkipFinalValidation(true)) if err != nil { t.Fatal("Failed to parse json string:", err) } diff --git a/internal/perms/output_fields_test.go b/internal/perms/output_fields_test.go index 24f0b204a..9aa0745fa 100644 --- a/internal/perms/output_fields_test.go +++ b/internal/perms/output_fields_test.go @@ -4,6 +4,7 @@ package perms import ( + "context" "testing" "github.com/hashicorp/boundary/globals" @@ -135,6 +136,8 @@ func Test_OutputFields(t *testing.T) { func Test_ACLOutputFields(t *testing.T) { t.Parallel() + ctx := context.Background() + type input struct { name string grants []string @@ -276,7 +279,7 @@ func Test_ACLOutputFields(t *testing.T) { t.Run(test.name, func(t *testing.T) { var grants []Grant for _, g := range test.grants { - grant, err := Parse("o_myorg", g) + grant, err := Parse(ctx, "o_myorg", g) require.NoError(t, err) grants = append(grants, grant) } diff --git a/internal/proto/controller/api/resources/roles/v1/role.proto b/internal/proto/controller/api/resources/roles/v1/role.proto index 102ff3724..d5318552e 100644 --- a/internal/proto/controller/api/resources/roles/v1/role.proto +++ b/internal/proto/controller/api/resources/roles/v1/role.proto @@ -25,7 +25,11 @@ message Principal { message GrantJson { // Output only. The ID, if set. - string id = 1; // @gotags: `class:"public"` + // Deprecated: use "ids" instead. + string id = 1 [deprecated = true]; // @gotags: `class:"public"` + + // Output only. The IDs, if set. + repeated string ids = 4; // @gotags: `class:"public"` // Output only. The type, if set. string type = 2; // @gotags: `class:"public"` diff --git a/sdk/pbs/controller/api/resources/roles/role.pb.go b/sdk/pbs/controller/api/resources/roles/role.pb.go index ad3ebac53..1a5ba9c6f 100644 --- a/sdk/pbs/controller/api/resources/roles/role.pb.go +++ b/sdk/pbs/controller/api/resources/roles/role.pb.go @@ -99,7 +99,12 @@ type GrantJson struct { unknownFields protoimpl.UnknownFields // Output only. The ID, if set. + // Deprecated: use "ids" instead. + // + // Deprecated: Marked as deprecated in controller/api/resources/roles/v1/role.proto. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty" class:"public"` // @gotags: `class:"public"` + // Output only. The IDs, if set. + Ids []string `protobuf:"bytes,4,rep,name=ids,proto3" json:"ids,omitempty" class:"public"` // @gotags: `class:"public"` // Output only. The type, if set. Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty" class:"public"` // @gotags: `class:"public"` // Output only. The actions. @@ -138,6 +143,7 @@ func (*GrantJson) Descriptor() ([]byte, []int) { return file_controller_api_resources_roles_v1_role_proto_rawDescGZIP(), []int{1} } +// Deprecated: Marked as deprecated in controller/api/resources/roles/v1/role.proto. func (x *GrantJson) GetId() string { if x != nil { return x.Id @@ -145,6 +151,13 @@ func (x *GrantJson) GetId() string { return "" } +func (x *GrantJson) GetIds() []string { + if x != nil { + return x.Ids + } + return nil +} + func (x *GrantJson) GetType() string { if x != nil { return x.Type @@ -414,77 +427,78 @@ var file_controller_api_resources_roles_v1_role_proto_rawDesc = []byte{ 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x22, 0x49, 0x0a, 0x09, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x05, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x12, - 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x61, - 0x77, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x12, - 0x40, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x52, 0x04, 0x6a, 0x73, 0x6f, - 0x6e, 0x22, 0xb9, 0x06, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, - 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, - 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, - 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x46, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, - 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, - 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3e, 0x0a, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x6c, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, - 0x5f, 0x69, 0x64, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, - 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x26, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, - 0x1e, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, - 0x64, 0x12, 0x0c, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x52, - 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, - 0x24, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x73, - 0x18, 0x64, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, - 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x4c, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, - 0x61, 0x6c, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, - 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, - 0x61, 0x6c, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x73, 0x18, 0x78, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, - 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x41, 0x0a, 0x06, 0x67, 0x72, 0x61, - 0x6e, 0x74, 0x73, 0x18, 0x82, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, - 0x72, 0x61, 0x6e, 0x74, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x12, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x4c, 0x5a, - 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, - 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, - 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x3b, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x22, 0x5f, 0x0a, 0x09, 0x47, + 0x72, 0x61, 0x6e, 0x74, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x69, 0x64, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x05, + 0x47, 0x72, 0x61, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x72, 0x61, 0x77, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x6e, 0x6f, 0x6e, + 0x69, 0x63, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x6e, 0x6f, + 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x12, 0x40, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, + 0x6f, 0x6c, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x4a, 0x73, 0x6f, + 0x6e, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xb9, 0x06, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, + 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, + 0x73, 0x63, 0x6f, 0x70, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x12, 0x46, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, + 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, + 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x3e, 0x0a, + 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x46, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, + 0x0c, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x6c, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, + 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x26, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1e, 0x0a, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, + 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x0c, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x53, 0x63, + 0x6f, 0x70, 0x65, 0x49, 0x64, 0x52, 0x0e, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x64, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, + 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x5f, 0x69, 0x64, 0x73, 0x12, 0x4c, 0x0a, 0x0a, 0x70, + 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2c, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x6c, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x52, 0x0a, 0x70, + 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x61, 0x6c, 0x73, 0x12, 0x24, 0x0a, 0x0d, 0x67, 0x72, 0x61, + 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x78, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x0d, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x73, 0x12, + 0x41, 0x0a, 0x06, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x73, 0x18, 0x82, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x72, 0x6f, 0x6c, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x74, 0x52, 0x06, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, + 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x3b, 0x72, 0x6f, 0x6c, 0x65, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/version/feature_manager.go b/version/feature_manager.go index 397c87d25..e6bce7803 100644 --- a/version/feature_manager.go +++ b/version/feature_manager.go @@ -21,6 +21,7 @@ const ( UseTargetIdForHostId RequireVersionInWorkerInfo SshSessionRecording + SupportIdInGrants ) var featureMap map[Feature]MetadataConstraint @@ -72,6 +73,12 @@ func init() { featureMap[SshSessionRecording] = MetadataConstraint{ Constraints: mustNewConstraints(">= 0.13.0"), } + + // Warn until 0.15 about using the now-deprecated id field in grants; after + // that disallow it + featureMap[SupportIdInGrants] = MetadataConstraint{ + Constraints: mustNewConstraints("< 0.15.0"), + } } func mustNewConstraints(v string) gvers.Constraints {