// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package scopes_test import ( "context" "fmt" "slices" "strings" "testing" "unicode" "github.com/golang/protobuf/ptypes/wrappers" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/ldap" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/daemon/controller" "github.com/hashicorp/boundary/internal/daemon/controller/auth" "github.com/hashicorp/boundary/internal/daemon/controller/handlers" "github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/server" "github.com/hashicorp/boundary/internal/types/scope" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes" wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) var ( testAuthorizedOrgActions = []string{"no-op", "read", "update", "delete", "attach-storage-policy", "detach-storage-policy"} testAuthorizedPrjActions = []string{"no-op", "read", "update", "delete"} testAuthorizedGlobalActions = []string{"no-op", "read", "update", "attach-storage-policy", "detach-storage-policy"} ) func createDefaultScopesRepoAndKms(t *testing.T) (*iam.Scope, *iam.Scope, func() (*iam.Repository, error), *kms.Kms) { t.Helper() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } kms := kms.TestKms(t, conn, wrap) oRes, pRes := iam.TestScopes(t, iamRepo) oRes.Name = "defaultOrg" oRes.Description = "defaultOrg" repo, err := repoFn() require.NoError(t, err) oRes, _, err = repo.UpdateScope(context.Background(), oRes, 1, []string{"Name", "Description"}) require.NoError(t, err) pRes.Name = "defaultProj" pRes.Description = "defaultProj" repo, err = repoFn() require.NoError(t, err) pRes, _, err = repo.UpdateScope(context.Background(), pRes, 1, []string{"Name", "Description"}) require.NoError(t, err) return oRes, pRes, repoFn, kms } var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ "aliases": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "auth-methods": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "auth-tokens": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "groups": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "policies": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "roles": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "scopes": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("destroy-key-version"), structpb.NewStringValue("list"), structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), }, }, "session-recordings": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "storage-buckets": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "users": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "workers": { Values: []*structpb.Value{ structpb.NewStringValue("create:controller-led"), structpb.NewStringValue("create:worker-led"), structpb.NewStringValue("list"), structpb.NewStringValue("read-certificate-authority"), structpb.NewStringValue("reinitialize-certificate-authority"), }, }, } var orgAuthorizedCollectionActions = map[string]*structpb.ListValue{ "auth-methods": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "auth-tokens": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "groups": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "policies": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "roles": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "scopes": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("destroy-key-version"), structpb.NewStringValue("list"), structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), }, }, "session-recordings": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "storage-buckets": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "users": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, } var projectAuthorizedCollectionActions = map[string]*structpb.ListValue{ "credential-stores": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "groups": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "host-catalogs": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "roles": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, "sessions": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "scopes": { Values: []*structpb.Value{ structpb.NewStringValue("destroy-key-version"), structpb.NewStringValue("list-key-version-destruction-jobs"), structpb.NewStringValue("list-keys"), structpb.NewStringValue("rotate-keys"), }, }, "targets": { Values: []*structpb.Value{ structpb.NewStringValue("create"), structpb.NewStringValue("list"), }, }, } func TestGet(t *testing.T) { org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) toMerge := &pbs.GetScopeRequest{ Id: proj.GetPublicId(), } oScope := &pb.Scope{ Id: org.GetPublicId(), ScopeId: org.GetParentId(), Scope: &pb.ScopeInfo{Id: "global", Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: org.GetName()}, Description: &wrapperspb.StringValue{Value: org.GetDescription()}, CreatedTime: org.CreateTime.GetTimestamp(), UpdatedTime: org.UpdateTime.GetTimestamp(), Version: 2, Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, } pScope := &pb.Scope{ Id: proj.GetPublicId(), ScopeId: proj.GetParentId(), Scope: &pb.ScopeInfo{Id: oScope.Id, Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: proj.GetName()}, Description: &wrapperspb.StringValue{Value: proj.GetDescription()}, CreatedTime: proj.CreateTime.GetTimestamp(), UpdatedTime: proj.UpdateTime.GetTimestamp(), Version: 2, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, } cases := []struct { name string scopeId string req *pbs.GetScopeRequest res *pbs.GetScopeResponse err error }{ { name: "Get an existing org", scopeId: "global", req: &pbs.GetScopeRequest{Id: org.GetPublicId()}, res: &pbs.GetScopeResponse{Item: oScope}, }, { name: "Get a non existing org", scopeId: "global", req: &pbs.GetScopeRequest{Id: "o_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Get an existing project", scopeId: org.GetPublicId(), req: &pbs.GetScopeRequest{Id: proj.GetPublicId()}, res: &pbs.GetScopeResponse{Item: pScope}, }, { name: "Get a non existing project", scopeId: org.GetPublicId(), req: &pbs.GetScopeRequest{Id: "p_DoesntExist"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Wrong id prefix", scopeId: org.GetPublicId(), req: &pbs.GetScopeRequest{Id: "j_1234567890"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "space in id", scopeId: org.GetPublicId(), req: &pbs.GetScopeRequest{Id: "p_1 23456789"}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) req := proto.Clone(toMerge).(*pbs.GetScopeRequest) proto.Merge(req, tc.req) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(err, "Couldn't create new project service.") got, gErr := s.GetScope(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetScope(%+v) got error\n%v, wanted\n%v", req, gErr, tc.err) } assert.Empty(cmp.Diff( tc.res, got, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ), "GetScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } func TestList(t *testing.T) { ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") wrap := db.TestWrapper(t) iamRepo := iam.TestRepo(t, conn, wrap) repoFn := func() (*iam.Repository, error) { return iamRepo, nil } repo, err := repoFn() require.NoError(t, err) kms := kms.TestKms(t, conn, wrap) oNoProjects, p1 := iam.TestScopes(t, repo) _, err = repo.DeleteScope(context.Background(), p1.GetPublicId()) require.NoError(t, err) oWithProjects, p2 := iam.TestScopes(t, repo) _, err = repo.DeleteScope(context.Background(), p2.GetPublicId()) require.NoError(t, err) outputFields := new(perms.OutputFields).SelfOrDefaults(globals.AnyAuthenticatedUserId) var initialOrgs []*pb.Scope globalScope := &pb.ScopeInfo{Id: "global", Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"} oNoProjectsProto, err := scopes.ToProto(context.Background(), oNoProjects, handlers.WithOutputFields(outputFields)) require.NoError(t, err) oNoProjectsProto.Scope = globalScope oNoProjectsProto.AuthorizedActions = testAuthorizedOrgActions oNoProjectsProto.AuthorizedCollectionActions = orgAuthorizedCollectionActions oWithProjectsProto, err := scopes.ToProto(context.Background(), oWithProjects, handlers.WithOutputFields(outputFields)) require.NoError(t, err) oWithProjectsProto.Scope = globalScope oWithProjectsProto.AuthorizedActions = testAuthorizedOrgActions oWithProjectsProto.AuthorizedCollectionActions = orgAuthorizedCollectionActions initialOrgs = append(initialOrgs, oNoProjectsProto, oWithProjectsProto) // Reverse slice since we order by create time (newest first) slices.Reverse(initialOrgs) cases := []struct { name string scopeId string req *pbs.ListScopesRequest res *pbs.ListScopesResponse err error }{ { name: "List initial orgs", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: "global"}, res: &pbs.ListScopesResponse{ Items: initialOrgs, EstItemCount: 2, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List No Projects", scopeId: oNoProjects.GetPublicId(), req: &pbs.ListScopesRequest{ScopeId: oNoProjects.GetPublicId()}, res: &pbs.ListScopesResponse{ ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Cant List Project Scopes", scopeId: p1.GetPublicId(), req: &pbs.ListScopesRequest{ScopeId: p1.GetPublicId()}, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Filter To Single Org", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: "global", Filter: fmt.Sprintf(`"/item/id"==%q`, initialOrgs[1].GetId())}, res: &pbs.ListScopesResponse{ Items: initialOrgs[1:2], EstItemCount: 1, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(err, "Couldn't create new role service.") // Test with non-anonymous listing first got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListScopes(%+v) got error\n%v, wanted\n%v", tc.req, gErr, tc.err) return } assert.Empty( cmp.Diff( got, tc.res, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // Now test with anonymous listing got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(globals.AnonymousUserId)), tc.req) require.NoError(gErr) assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { assert.Nil(item.CreatedTime) assert.Nil(item.UpdatedTime) assert.Empty(item.Version) } }) } var wantOrgs []*pb.Scope for i := 0; i < 10; i++ { newO, err := iam.NewOrg(ctx) require.NoError(t, err) o, err := repo.CreateScope(ctx, newO, "") require.NoError(t, err) wantOrgs = append(wantOrgs, &pb.Scope{ Id: o.GetPublicId(), ScopeId: globalScope.GetId(), Scope: globalScope, CreatedTime: o.GetCreateTime().GetTimestamp(), UpdatedTime: o.GetUpdateTime().GetTimestamp(), Version: 1, Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }) } // Reverse slice since we order by create time (newest first) slices.Reverse(wantOrgs) wantOrgs = append(wantOrgs, initialOrgs...) var wantProjects []*pb.Scope for i := 0; i < 10; i++ { newP, err := iam.NewProject(ctx, oWithProjects.GetPublicId()) require.NoError(t, err) p, err := repo.CreateScope(ctx, newP, "") require.NoError(t, err) wantProjects = append(wantProjects, &pb.Scope{ Id: p.GetPublicId(), ScopeId: oWithProjects.GetPublicId(), Scope: &pb.ScopeInfo{Id: oWithProjects.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, CreatedTime: p.GetCreateTime().GetTimestamp(), UpdatedTime: p.GetUpdateTime().GetTimestamp(), Version: 1, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }) } // Reverse slice since we order by create time (newest first) slices.Reverse(wantProjects) totalScopes := append(wantOrgs, wantProjects...) cases = []struct { name string scopeId string req *pbs.ListScopesRequest res *pbs.ListScopesResponse err error }{ { name: "List Many Orgs", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: "global"}, res: &pbs.ListScopesResponse{ Items: wantOrgs, EstItemCount: 12, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List Many Projects", scopeId: oWithProjects.GetPublicId(), req: &pbs.ListScopesRequest{ScopeId: oWithProjects.GetPublicId()}, res: &pbs.ListScopesResponse{ Items: wantProjects, EstItemCount: 10, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "List Global Recursively", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true}, res: &pbs.ListScopesResponse{ Items: totalScopes, EstItemCount: 22, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter To Orgs", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/type"==%q`, scope.Global.String())}, res: &pbs.ListScopesResponse{ Items: wantOrgs, EstItemCount: 12, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter To Projects", scopeId: scope.Global.String(), req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Recursive: true, Filter: fmt.Sprintf(`"/item/scope/type"==%q`, scope.Org.String())}, res: &pbs.ListScopesResponse{ Items: wantProjects, EstItemCount: 10, ResponseType: "complete", SortBy: "created_time", SortDir: "desc", }, }, { name: "Filter Bad Format", req: &pbs.ListScopesRequest{ScopeId: scope.Global.String(), Filter: `"//id/"=="bad"`}, err: handlers.InvalidArgumentErrorf("bad format", nil), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(err, "Couldn't create new scope service.") // Test with non-anonymous listing first got, gErr := s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListScopes(%+v) got error\n%v, wanted\n%v", tc.req, gErr, tc.err) return } require.NoError(gErr) assert.Empty( cmp.Diff( got, tc.res, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // Now test with anonymous listing got, gErr = s.ListScopes(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(globals.AnonymousUserId)), tc.req) require.NoError(gErr) assert.Len(got.Items, len(tc.res.Items)) for _, item := range got.GetItems() { assert.Nil(item.CreatedTime) assert.Nil(item.UpdatedTime) assert.Empty(item.Version) } }) } } func TestListPagination(t *testing.T) { // Set database read timeout to avoid duplicates in response oldReadTimeout := globals.RefreshReadLookbackDuration globals.RefreshReadLookbackDuration = 0 t.Cleanup(func() { globals.RefreshReadLookbackDuration = oldReadTimeout }) ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") sqlDb, err := conn.SqlDB(ctx) require.NoError(t, err) wrapper := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrapper) rw := db.New(conn) repoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } repo, err := repoFn() require.NoError(t, err) iamRepoFn := func() (*iam.Repository, error) { return repo, nil } iamRepo, err := iamRepoFn() require.NoError(t, err) tokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(ctx, rw, rw, kms) } tokenRepo, err := tokenRepoFn() require.NoError(t, err) serversRepoFn := func() (*server.Repository, error) { return server.NewRepository(ctx, rw, rw, kms) } oWithProjects, p2 := iam.TestScopes(t, repo, iam.WithSkipDefaultRoleCreation(true)) _, err = repo.DeleteScope(context.Background(), p2.GetPublicId()) require.NoError(t, err) paginationAuthorizedCollectionActions := map[string]*structpb.ListValue{ "sessions": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, "targets": { Values: []*structpb.Value{ structpb.NewStringValue("list"), }, }, } var wantProjects []*pb.Scope for i := 0; i < 10; i++ { newP, err := iam.NewProject(ctx, oWithProjects.GetPublicId()) require.NoError(t, err) p, err := repo.CreateScope(ctx, newP, "") require.NoError(t, err) wantProjects = append(wantProjects, &pb.Scope{ Id: p.GetPublicId(), ScopeId: oWithProjects.GetPublicId(), Scope: &pb.ScopeInfo{Id: oWithProjects.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, CreatedTime: p.GetCreateTime().GetTimestamp(), UpdatedTime: p.GetUpdateTime().GetTimestamp(), Version: 1, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: paginationAuthorizedCollectionActions, }) } // Reverse slice since we order by create time (newest first) slices.Reverse(wantProjects) // Run analyze to update scope estimate _, err = sqlDb.ExecContext(ctx, "analyze") require.NoError(t, err) authMethod := ldap.TestAuthMethod(t, conn, wrapper, oWithProjects.PublicId, []string{"ldaps://no-managed-groups"}) acct := ldap.TestAccount(t, conn, authMethod, "test-login-last") u := iam.TestUser(t, iamRepo, oWithProjects.GetPublicId(), iam.WithAccountIds(acct.PublicId)) // privProjRole := iam.TestRole(t, conn, pwt.GetPublicId()) // iam.TestRoleGrant(t, conn, privProjRole.GetPublicId(), "ids=*;type=*;actions=*") // iam.TestUserRole(t, conn, privProjRole.GetPublicId(), u.GetPublicId()) privOrgRole := iam.TestRole(t, conn, oWithProjects.GetPublicId()) iam.TestRoleGrant(t, conn, privOrgRole.GetPublicId(), "ids=*;type=*;actions=*") iam.TestUserRole(t, conn, privOrgRole.GetPublicId(), u.GetPublicId()) at, _ := tokenRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) requestInfo := authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), Token: at.GetToken(), } requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) req := &pbs.ListScopesRequest{ ScopeId: oWithProjects.GetPublicId(), Filter: "", ListToken: "", PageSize: 2, } s, err := scopes.NewServiceFn(ctx, repoFn, kms, 1000) require.NoError(t, err) got, err := s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 2) // all comparisons will be done without refresh token assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: wantProjects[0:2], ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // second page req.ListToken = got.ListToken got, err = s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 2) assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: wantProjects[2:4], ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // remainder of results req.ListToken = got.ListToken req.PageSize = 6 got, err = s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 6) assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: wantProjects[4:], ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // create another scope newP, err := iam.NewProject(ctx, oWithProjects.GetPublicId()) require.NoError(t, err) p, err := repo.CreateScope(ctx, newP, "") require.NoError(t, err) newScope := &pb.Scope{ Id: p.GetPublicId(), ScopeId: oWithProjects.GetPublicId(), Scope: &pb.ScopeInfo{Id: oWithProjects.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, CreatedTime: p.GetCreateTime().GetTimestamp(), UpdatedTime: p.GetUpdateTime().GetTimestamp(), Version: 1, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: paginationAuthorizedCollectionActions, } // Add to the front of the slice since it's the most recently updated wantProjects = append([]*pb.Scope{newScope}, wantProjects...) // delete different scope _, err = repo.DeleteScope(ctx, wantProjects[len(wantProjects)-1].Id) wantProjects = wantProjects[:len(wantProjects)-1] require.NoError(t, err) // Run analyze to update postgres estimates _, err = sqlDb.ExecContext(ctx, "analyze") require.NoError(t, err) // request updated results // since both creating and deleting scopes will affect the grantsHash // we expect this to error req.ListToken = got.ListToken _, err = s.ListScopes(ctx, req) require.Error(t, err) require.True(t, errors.Match(errors.T(errors.InvalidListToken), err)) // clear the refresh token req.ListToken = "" req.PageSize = 2 got, err = s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 2) assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: wantProjects[0:2], ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // Request new page with filter requiring looping // to fill the page. req.ListToken = "" req.PageSize = 1 req.Filter = fmt.Sprintf(`"/item/id"==%q or "/item/id"==%q`, wantProjects[len(wantProjects)-2].Id, wantProjects[len(wantProjects)-1].Id) got, err = s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: []*pb.Scope{wantProjects[len(wantProjects)-2]}, ResponseType: "delta", ListToken: "", SortBy: "created_time", SortDir: "desc", // Should be empty again RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) req.ListToken = got.ListToken // Get the second page got, err = s.ListScopes(ctx, req) require.NoError(t, err) require.Len(t, got.GetItems(), 1) assert.Empty(t, cmp.Diff( got, &pbs.ListScopesResponse{ Items: []*pb.Scope{wantProjects[len(wantProjects)-1]}, ResponseType: "complete", ListToken: "", SortBy: "created_time", SortDir: "desc", RemovedIds: nil, EstItemCount: 12, }, cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), protocmp.Transform(), protocmp.IgnoreFields(&pbs.ListScopesResponse{}, "list_token"), ), ) // Create unauthenticated user unauthAt := authtoken.TestAuthToken(t, conn, kms, oWithProjects.GetPublicId()) unauthR := iam.TestRole(t, conn, p.GetPublicId()) _ = iam.TestUserRole(t, conn, unauthR.GetPublicId(), unauthAt.GetIamUserId()) // Make a request with the unauthenticated user, // ensure the response contains the pagination parameters. requestInfo = authpb.RequestInfo{ TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: unauthAt.GetPublicId(), Token: unauthAt.GetToken(), } requestContext = context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{}) ctx = auth.NewVerifierContext(requestContext, iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) _, err = s.ListScopes(ctx, &pbs.ListScopesRequest{ ScopeId: "global", Recursive: true, }) require.Error(t, err) assert.ErrorIs(t, handlers.ForbiddenError(), err) } func TestDelete(t *testing.T) { org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(t, err, "Error when getting new project service.") cases := []struct { name string scopeId string req *pbs.DeleteScopeRequest res *pbs.DeleteScopeResponse err error }{ { name: "Delete an Existing Project", scopeId: org.GetPublicId(), req: &pbs.DeleteScopeRequest{ Id: proj.GetPublicId(), }, }, { name: "Delete bad project id Project", scopeId: org.GetPublicId(), req: &pbs.DeleteScopeRequest{ Id: "p_DoesntExist", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Bad Project Id formatting", scopeId: org.GetPublicId(), req: &pbs.DeleteScopeRequest{ Id: "bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Delete an Existing Org", scopeId: scope.Global.String(), req: &pbs.DeleteScopeRequest{ Id: org.GetPublicId(), }, }, { name: "Delete bad org id Org", scopeId: scope.Global.String(), req: &pbs.DeleteScopeRequest{ Id: "p_DoesntExist", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Bad Org Id formatting", scopeId: scope.Global.String(), req: &pbs.DeleteScopeRequest{ Id: "bad_format", }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) got, gErr := s.DeleteScope(auth.DisabledAuthTestContext(repoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "DeleteScope(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) } assert.EqualValuesf(tc.res, got, "DeleteScope(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } func TestDelete_twice(t *testing.T) { assert, require := assert.New(t), require.New(t) org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(err, "Error when getting new scopes service") ctx := auth.DisabledAuthTestContext(repoFn, org.GetPublicId()) req := &pbs.DeleteScopeRequest{ Id: proj.GetPublicId(), } _, gErr := s.DeleteScope(ctx, req) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteScope(ctx, req) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected not found for the second delete.") ctx = auth.DisabledAuthTestContext(repoFn, scope.Global.String()) req = &pbs.DeleteScopeRequest{ Id: org.GetPublicId(), } _, gErr = s.DeleteScope(ctx, req) assert.NoError(gErr, "First attempt") _, gErr = s.DeleteScope(ctx, req) assert.Error(gErr, "Second attempt") assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected not found for the second delete.") } func TestCreate(t *testing.T) { ctx := context.Background() defaultOrg, defaultProj, repoFn, kms := createDefaultScopesRepoAndKms(t) defaultProjCreated := defaultProj.GetCreateTime().GetTimestamp().AsTime() toMerge := &pbs.CreateScopeRequest{} repo, err := repoFn() require.NoError(t, err) globalUser, err := iam.NewUser(ctx, scope.Global.String()) require.NoError(t, err) globalUser, err = repo.CreateUser(ctx, globalUser) require.NoError(t, err) orgUser, err := iam.NewUser(ctx, defaultOrg.GetPublicId()) require.NoError(t, err) orgUser, err = repo.CreateUser(ctx, orgUser) require.NoError(t, err) cases := []struct { name string scopeId string req *pbs.CreateScopeRequest res *pbs.CreateScopeResponse err error }{ { name: "Create a valid Project", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: defaultOrg.GetPublicId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.CreateScopeResponse{ Uri: "scopes/p_", Item: &pb.Scope{ ScopeId: defaultOrg.GetPublicId(), Scope: &pb.ScopeInfo{Id: defaultOrg.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Create a valid Org", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.CreateScopeResponse{ Uri: "scopes/o_", Item: &pb.Scope{ ScopeId: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }, }, }, { name: "Create a valid Project with type specified", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: defaultOrg.GetPublicId(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Project.String(), }, }, res: &pbs.CreateScopeResponse{ Uri: "scopes/p_", Item: &pb.Scope{ ScopeId: defaultOrg.GetPublicId(), Scope: &pb.ScopeInfo{Id: defaultOrg.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Create a valid Org with type specified", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Org.String(), }, }, res: &pbs.CreateScopeResponse{ Uri: "scopes/o_", Item: &pb.Scope{ ScopeId: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Description: &wrapperspb.StringValue{Value: "desc"}, Version: 1, Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }, }, }, { name: "Create a valid org with leading and trailing whitespace on name and description", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Description: &wrapperspb.StringValue{Value: " test description with whitespace "}, Type: scope.Org.String(), Name: &wrapperspb.StringValue{Value: " test org name with whitespace "}, }, }, res: &pbs.CreateScopeResponse{ Uri: "scopes/o_", Item: &pb.Scope{ ScopeId: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Description: &wrapperspb.StringValue{Value: "test description with whitespace"}, // assert the whitespace is trimmed Name: &wrapperspb.StringValue{Value: "test org name with whitespace"}, // assert the whitespace is trimmed Version: 1, Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }, }, }, { name: "Create a global type scope", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Global.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Create with a custom version number", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Org.String(), Version: 5, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Project with bad type specified", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: defaultOrg.GetPublicId(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Org.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Org with bad type specified", scopeId: scope.Global.String(), req: &pbs.CreateScopeRequest{ Item: &pb.Scope{ ScopeId: scope.Global.String(), Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Project.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Id", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{Item: &pb.Scope{ Id: "not allowed to be set", }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Created Time", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{Item: &pb.Scope{ CreatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Can't specify Update Time", scopeId: defaultOrg.GetPublicId(), req: &pbs.CreateScopeRequest{Item: &pb.Scope{ UpdatedTime: timestamppb.Now(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { for _, withUserId := range []bool{false, true} { t.Run(fmt.Sprintf("%s-userid-%t", tc.name, withUserId), func(t *testing.T) { assert, require := assert.New(t), require.New(t) var name string if tc.req != nil && tc.req.GetItem() != nil && tc.req.GetItem().GetName() != nil { name = tc.req.GetItem().GetName().GetValue() localName := name defer func() { tc.res.GetItem().GetName().Value = localName }() } req := proto.Clone(toMerge).(*pbs.CreateScopeRequest) proto.Merge(req, tc.req) s, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(err, "Error when getting new project service.") if name != "" { // Test cases can specify names with leading and trailing // whitespace to test for Boundary's whitespace removal. // Since we're adding to the name before we send the request // off, we need to make sure we keep it as we found it. // Find where leading whitespace ends, add withUserId there. idx := 0 for i, r := range name { if !unicode.IsSpace(r) { idx = i break } } if idx == 0 { name = fmt.Sprintf("%t-%s", withUserId, name) } else { name = fmt.Sprintf("%s%t-%s", name[:idx], withUserId, name[idx:]) } req.GetItem().GetName().Value = name } var userId string if withUserId { if tc.scopeId == scope.Global.String() { userId = globalUser.GetPublicId() } else { userId = orgUser.GetPublicId() } assert.NotEmpty(userId) } got, gErr := s.CreateScope(auth.DisabledAuthTestContext(repoFn, tc.scopeId, auth.WithUserId(userId)), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "CreateScope(%+v) got error %v, wanted %v", req, gErr, tc.err) } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) switch tc.scopeId { case "global": assert.True(strings.HasPrefix(got.GetItem().GetId(), "o_")) default: assert.True(strings.HasPrefix(got.GetItem().GetId(), "p_")) } gotCreateTime := got.GetItem().GetCreatedTime().AsTime() gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a project created after the test setup's default project assert.True(gotCreateTime.After(defaultProjCreated), "New scope should have been created after default project. Was created %v, which is after %v", gotCreateTime, defaultProjCreated) assert.True(gotUpdateTime.After(defaultProjCreated), "New scope should have been updated after default project. Was updated %v, which is after %v", gotUpdateTime, defaultProjCreated) if withUserId { repo, err := repoFn() require.NoError(err) noopFilter := func(ctx context.Context, item *iam.Role) (bool, error) { return true, nil } roles, err := iam.ListRoles(ctx, []byte("test"), globals.DefaultMaxPageSize, noopFilter, repo, []string{got.GetItem().GetId()}) require.NoError(err) switch tc.scopeId { case defaultOrg.PublicId: require.Len(roles.Items, 2) case "global": require.Len(roles.Items, 2) } for _, role := range roles.Items { switch role.GetName() { case "Administration": assert.Equal(fmt.Sprintf("Role created for administration of scope %s by user %s at its creation time", got.GetItem().GetId(), userId), role.GetDescription()) case "Login and Default Grants": assert.Equal(fmt.Sprintf("Role created for login capability, account self-management, and other default grants for users of scope %s at its creation time", got.GetItem().GetId()), role.GetDescription()) case "Default Grants": assert.Equal(fmt.Sprintf("Role created to provide default grants to users of scope %s at its creation time", got.GetItem().GetId()), role.GetDescription()) default: t.Fatal("unexpected role name", role.GetName()) } } } // Clear all values which are hard to compare against. assert.Equal(strings.TrimSpace(name), got.GetItem().GetName().GetValue()) got.Item.Name = tc.res.GetItem().GetName() got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil } assert.Empty(cmp.Diff( tc.res, got, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ), "CreateScope(%q) got response %q, wanted %q", req, got, tc.res) }) } } } func TestUpdate(t *testing.T) { org, proj, repoFn, kms := createDefaultScopesRepoAndKms(t) tested, err := scopes.NewServiceFn(context.Background(), repoFn, kms, 1000) require.NoError(t, err, "Error when getting new project service.") iamRepo, err := repoFn() require.NoError(t, err) global, err := iamRepo.LookupScope(context.Background(), "global") require.NoError(t, err) var orgVersion uint32 = 2 var projVersion uint32 = 2 globalVersion := global.Version resetOrg := func() { orgVersion++ repo, err := repoFn() require.NoError(t, err, "Couldn't get a new repo") org, _, err = repo.UpdateScope(context.Background(), org, orgVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the org") orgVersion++ } resetProject := func() { projVersion++ repo, err := repoFn() require.NoError(t, err, "Couldn't get a new repo") proj, _, err = repo.UpdateScope(context.Background(), proj, projVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the project") projVersion++ } resetGlobal := func() { repo, err := repoFn() require.NoError(t, err, "Couldn't get a new repo") globalScope := iam.AllocScope() globalScope.PublicId = "global" global, _, err = repo.UpdateScope(context.Background(), &globalScope, globalVersion, []string{"Name", "Description"}) require.NoError(t, err, "Failed to reset the global scope") globalVersion = global.Version } projCreated := proj.GetCreateTime().GetTimestamp().AsTime() projToMerge := &pbs.UpdateScopeRequest{ Id: proj.GetPublicId(), } orgToMerge := &pbs.UpdateScopeRequest{ Id: org.GetPublicId(), } globalToMerge := &pbs.UpdateScopeRequest{ Id: "global", } cases := []struct { name string scopeId string req *pbs.UpdateScopeRequest res *pbs.UpdateScopeResponse err error }{ { name: "Update an Existing Project", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Project.String(), }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Update an Existing Org", scopeId: scope.Global.String(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Org.String(), }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: org.GetPublicId(), ScopeId: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: org.GetCreateTime().GetTimestamp(), Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }, }, }, { name: "Update org name and description with whitespace", scopeId: scope.Global.String(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: " new name "}, Description: &wrapperspb.StringValue{Value: " new desc "}, Type: scope.Org.String(), }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: org.GetPublicId(), ScopeId: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "new name"}, Description: &wrapperspb.StringValue{Value: "new desc"}, CreatedTime: org.GetCreateTime().GetTimestamp(), Type: scope.Org.String(), AuthorizedActions: testAuthorizedOrgActions, AuthorizedCollectionActions: orgAuthorizedCollectionActions, }, }, }, { name: "Update global", scopeId: scope.Global.String(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: scope.Global.String(), }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: scope.Global.String(), Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: global.GetCreateTime().GetTimestamp(), Type: scope.Global.String(), AuthorizedActions: testAuthorizedGlobalActions, AuthorizedCollectionActions: globalAuthorizedCollectionActions, }, }, }, { name: "Multiple Paths in single string", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name,description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "No Update Mask", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant modify type", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "type"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Type: scope.Org.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "No Paths in Mask", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalidly formatted scope id - org scope", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: "o_!@£$*", UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalidly formatted scope id - proj scope", scopeId: proj.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: "p_!@£$*", UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Invalidly formatted scope id - unknown scope", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: "bla_123", UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unknown scope type - org scope", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: org.GetPublicId(), UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Type: "test", Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unknown scope type - proj scope", scopeId: proj.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: proj.GetPublicId(), UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Type: "test", Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unknown primary auth method id", scopeId: proj.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: proj.GetPublicId(), UpdateMask: &field_mask.FieldMask{Paths: []string{}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, PrimaryAuthMethodId: &wrapperspb.StringValue{Value: "test_123"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Update org name and description with whitespace only", scopeId: scope.Global.String(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: " "}, Description: &wrapperspb.StringValue{Value: " "}, Type: scope.Org.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Update org name and description with non-printable characters", scopeId: scope.Global.String(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name", "description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: " na\u200Bme "}, Description: &wrapperspb.StringValue{Value: " desc\u200Bription "}, Type: scope.Org.String(), }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Only non-existent paths in Mask", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistent_field"}}, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated name"}, Description: &wrapperspb.StringValue{Value: "updated desc"}, }, }, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Unset Name", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Scope{ Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Description: &wrapperspb.StringValue{Value: "defaultProj"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Unset Description", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Scope{ Name: &wrappers.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrappers.StringValue{Value: "defaultProj"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Update Only Name", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"name"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "ignored"}, }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "updated"}, Description: &wrapperspb.StringValue{Value: "defaultProj"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Update Only Description", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "ignored"}, Description: &wrapperspb.StringValue{Value: "notignored"}, }, }, res: &pbs.UpdateScopeResponse{ Item: &pb.Scope{ Id: proj.GetPublicId(), ScopeId: org.GetPublicId(), Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "defaultProj"}, Description: &wrapperspb.StringValue{Value: "notignored"}, CreatedTime: proj.GetCreateTime().GetTimestamp(), Type: scope.Project.String(), AuthorizedActions: testAuthorizedPrjActions, AuthorizedCollectionActions: projectAuthorizedCollectionActions, }, }, }, { name: "Update a Non Existing Project", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: "p_DoesntExist", UpdateMask: &field_mask.FieldMask{ Paths: []string{"description"}, }, Item: &pb.Scope{ Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "desc"}, }, }, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Cant change Id", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ Id: proj.GetPublicId(), UpdateMask: &field_mask.FieldMask{ Paths: []string{"id"}, }, Item: &pb.Scope{ Id: "p_somethinge", Scope: &pb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String(), Name: "defaultOrg", Description: "defaultOrg"}, Name: &wrapperspb.StringValue{Value: "new"}, Description: &wrapperspb.StringValue{Value: "new desc"}, }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Created Time", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"created_time"}, }, Item: &pb.Scope{ CreatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "Cant specify Updated Time", scopeId: org.GetPublicId(), req: &pbs.UpdateScopeRequest{ UpdateMask: &field_mask.FieldMask{ Paths: []string{"updated_time"}, }, Item: &pb.Scope{ UpdatedTime: timestamppb.Now(), }, }, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { ver := orgVersion if tc.req.Id == proj.PublicId { ver = projVersion } tc.req.Item.Version = ver assert, require := assert.New(t), require.New(t) var req *pbs.UpdateScopeRequest switch { case tc.scopeId == scope.Global.String() && tc.req.Item.GetType() == scope.Global.String(): tc.req.Item.Version = globalVersion ver = globalVersion req = proto.Clone(globalToMerge).(*pbs.UpdateScopeRequest) if tc.err == nil { defer resetGlobal() } case tc.scopeId == scope.Global.String(): req = proto.Clone(orgToMerge).(*pbs.UpdateScopeRequest) if tc.err == nil { defer resetOrg() } default: ver = projVersion tc.req.Item.Version = ver req = proto.Clone(projToMerge).(*pbs.UpdateScopeRequest) if tc.err == nil { defer resetProject() } } proto.Merge(req, tc.req) got, gErr := tested.UpdateScope(auth.DisabledAuthTestContext(repoFn, tc.scopeId), req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "UpdateScope(%+v) got error\n%v, wanted\n%v", req, gErr, tc.err) } if got != nil { assert.NotNilf(tc.res, "Expected UpdateScope response to be nil, but was %v", got) gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() // Verify it is a project updated after it was created assert.True(gotUpdateTime.After(projCreated), "Updated project should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, projCreated) // Clear all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil assert.Equal(ver+1, got.GetItem().GetVersion()) tc.res.Item.Version = ver + 1 } assert.Empty(cmp.Diff( got, tc.res, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ), "UpdateScope(%q) got response\n%q, wanted\n%q", req, got, tc.res) }) } } func TestListKeys(t *testing.T) { tc := controller.NewTestController(t, nil) aToken := tc.Token() uToken := tc.UnprivilegedToken() iamRepoFn := func() (*iam.Repository, error) { return tc.IamRepo(), nil } serversRepoFn := func() (*server.Repository, error) { return tc.ServersRepo(), nil } authTokenRepoFn := func() (*authtoken.Repository, error) { return tc.AuthTokenRepo(), nil } privCtx := auth.NewVerifierContext( context.Background(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: aToken.Id, EncryptedToken: strings.Split(aToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }) unprivCtx := auth.NewVerifierContext( context.Background(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: uToken.Id, EncryptedToken: strings.Split(uToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }) org, proj := iam.TestScopes(t, tc.IamRepo()) org.Name = "defaultOrg" org.Description = "defaultOrg" org, _, err := tc.IamRepo().UpdateScope(context.Background(), org, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for listing keys in org listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) _, err = tc.IamRepo().AddRoleGrants(context.Background(), listKeysRole.PublicId, 1, []string{"ids=*;type=*;actions=list-keys"}) require.NoError(t, err) _, err = tc.IamRepo().AddPrincipalRoles(context.Background(), listKeysRole.PublicId, 2, []string{aToken.UserId}) require.NoError(t, err) proj.Name = "defaultProj" proj.Description = "defaultProj" proj, _, err = tc.IamRepo().UpdateScope(context.Background(), proj, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for listing keys in project listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) _, err = tc.IamRepo().AddRoleGrants(context.Background(), listKeysRole.PublicId, 1, []string{"ids=*;type=*;actions=list-keys"}) require.NoError(t, err) _, err = tc.IamRepo().AddPrincipalRoles(context.Background(), listKeysRole.PublicId, 2, []string{aToken.UserId}) require.NoError(t, err) cases := []struct { name string req *pbs.ListKeysRequest res *pbs.ListKeysResponse authCtx context.Context err error }{ { name: "List keys in the global scope", req: &pbs.ListKeysRequest{Id: "global"}, res: &pbs.ListKeysResponse{ Items: []*pb.Key{ { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "tokens", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "oplog", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "database", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "audit", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "oidc", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "sessions", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Purpose: "rootKey", Type: "kek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, }, }, authCtx: privCtx, }, { name: "List keys in an existing org", req: &pbs.ListKeysRequest{Id: org.GetPublicId()}, res: &pbs.ListKeysResponse{ Items: []*pb.Key{ { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "tokens", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "oplog", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "database", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "audit", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "oidc", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "sessions", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: org.PublicId, ParentScopeId: "global", Type: "org", Name: org.Name, Description: org.Description, }, Purpose: "rootKey", Type: "kek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, }, }, authCtx: privCtx, }, { name: "List keys in a non existing org", req: &pbs.ListKeysRequest{Id: "o_DoesntExis"}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "List keys in an existing project", req: &pbs.ListKeysRequest{Id: proj.GetPublicId()}, res: &pbs.ListKeysResponse{ Items: []*pb.Key{ { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "tokens", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "oplog", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "database", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "audit", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "oidc", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "sessions", Type: "dek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, { Scope: &pb.ScopeInfo{ Id: proj.PublicId, ParentScopeId: org.PublicId, Type: "project", Name: proj.Name, Description: proj.Description, }, Purpose: "rootKey", Type: "kek", Versions: []*pb.KeyVersion{ { Version: 1, }, }, }, }, }, authCtx: privCtx, }, { name: "List keys in a non existing project", req: &pbs.ListKeysRequest{Id: "p_DoesntExist"}, err: handlers.ApiErrorWithCode(codes.NotFound), authCtx: privCtx, }, { name: "Wrong id prefix", req: &pbs.ListKeysRequest{Id: "j_1234567890"}, err: handlers.ApiErrorWithCode(codes.InvalidArgument), authCtx: privCtx, }, { name: "space in id", req: &pbs.ListKeysRequest{Id: "p_1 23456789"}, err: handlers.ApiErrorWithCode(codes.InvalidArgument), authCtx: privCtx, }, { name: "unauthorized", req: &pbs.ListKeysRequest{Id: "global"}, err: handlers.ApiErrorWithCode(codes.PermissionDenied), authCtx: unprivCtx, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), iamRepoFn, tc.Kms(), 1000) require.NoError(err, "Couldn't create new project service.") got, gErr := s.ListKeys(tt.authCtx, tt.req) if tt.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tt.err), "ListKeys(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) } else { require.NoError(gErr) } assert.Empty( cmp.Diff( tt.res, got, protocmp.Transform(), // Sort by purpose for comparison since it is the only unique and predictable field protocmp.SortRepeated(func(i, j *pb.Key) bool { return i.GetPurpose() < j.GetPurpose() }), protocmp.IgnoreFields(&pb.Key{}, "id", "created_time"), protocmp.IgnoreFields(&pb.KeyVersion{}, "id", "created_time"), cmpopts.SortSlices(func(a, b string) bool { return a < b }), ), "ListKeys(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, ) }) } } func TestRotateKeys(t *testing.T) { tc := controller.NewTestController(t, nil) aToken := tc.Token() uToken := tc.UnprivilegedToken() iamRepoFn := func() (*iam.Repository, error) { return tc.IamRepo(), nil } serversRepoFn := func() (*server.Repository, error) { return tc.ServersRepo(), nil } authTokenRepoFn := func() (*authtoken.Repository, error) { return tc.AuthTokenRepo(), nil } privCtx := auth.NewVerifierContext( context.Background(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: aToken.Id, EncryptedToken: strings.Split(aToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }) unprivCtx := auth.NewVerifierContext( context.Background(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: uToken.Id, EncryptedToken: strings.Split(uToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }) org, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(aToken.UserId)) // Add new role for listing+rotating keys in org rotateKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) iam.TestRoleGrant(t, tc.DbConn(), rotateKeysRole.PublicId, "ids=*;type=*;actions=rotate-keys,list-keys") _ = iam.TestUserRole(t, tc.DbConn(), rotateKeysRole.PublicId, aToken.UserId) // Add new role for listing+rotating keys in project rotateKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) iam.TestRoleGrant(t, tc.DbConn(), rotateKeysRole.PublicId, "ids=*;type=*;actions=rotate-keys,list-keys") _ = iam.TestUserRole(t, tc.DbConn(), rotateKeysRole.PublicId, aToken.UserId) cases := []struct { name string req *pbs.RotateKeysRequest res *pbs.RotateKeysResponse authCtx context.Context err error }{ { name: "unauthorized", req: &pbs.RotateKeysRequest{ScopeId: "global", Rewrap: false}, err: handlers.ApiErrorWithCode(codes.PermissionDenied), authCtx: unprivCtx, }, { name: "Rotate keys in a non existing org", req: &pbs.RotateKeysRequest{ScopeId: "o_DoesntExis", Rewrap: false}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "successfully Rotate keys in org", req: &pbs.RotateKeysRequest{ScopeId: org.GetPublicId(), Rewrap: false}, authCtx: privCtx, }, { name: "successfully Rotate and rewrap keys in org", req: &pbs.RotateKeysRequest{ScopeId: org.GetPublicId(), Rewrap: true}, authCtx: privCtx, }, { name: "successfully Rotate keys in project", req: &pbs.RotateKeysRequest{ScopeId: proj.GetPublicId(), Rewrap: false}, authCtx: privCtx, }, { name: "successfully Rotate and rewrap keys in project", req: &pbs.RotateKeysRequest{ScopeId: proj.GetPublicId(), Rewrap: true}, authCtx: privCtx, }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), iamRepoFn, tc.Kms(), 1000) require.NoError(err, "Couldn't create new project service.") prevKeyVersions := map[uint32]int{} var prevLatest uint32 = 0 // checking key versions before rotation if tt.err == nil { keys, gErr := s.ListKeys(privCtx, &pbs.ListKeysRequest{Id: tt.req.ScopeId}) require.NoError(gErr) for _, key := range keys.Items { for _, keyVersion := range key.Versions { if keyVersion.Version > prevLatest { prevLatest = keyVersion.Version } _, ok := prevKeyVersions[keyVersion.Version] if !ok { prevKeyVersions[keyVersion.Version] = 0 } prevKeyVersions[keyVersion.Version]++ } } } // RotateKeys returns nocontent response _, kErr := s.RotateKeys(tt.authCtx, tt.req) if tt.err != nil { require.Error(kErr) assert.True(errors.Is(kErr, tt.err), "RotateKeys(%+v) got error\n%v, wanted\n%v", tt.req, kErr, tt.err) } else { require.NoError(kErr) keys, gErr := s.ListKeys(privCtx, &pbs.ListKeysRequest{Id: tt.req.ScopeId}) require.NoError(gErr) keyVersions := map[uint32]int{} var latest uint32 = 0 for _, key := range keys.Items { for _, keyVersion := range key.Versions { if keyVersion.Version > latest { latest = keyVersion.Version } _, ok := keyVersions[keyVersion.Version] if !ok { keyVersions[keyVersion.Version] = 0 } keyVersions[keyVersion.Version]++ } } // there should only be one new key version assert.Equal(len(prevKeyVersions)+1, len(keyVersions)) // since we just rotated them, there should be the same number of version 1 and version 2 assert.Equal(prevKeyVersions[prevLatest], keyVersions[latest]) } }) } } func TestListKeyVersionDestructionJobs(t *testing.T) { tc := controller.NewTestController(t, nil) aToken := tc.Token() uToken := tc.UnprivilegedToken() iamRepoFn := func() (*iam.Repository, error) { return tc.IamRepo(), nil } serversRepoFn := func() (*server.Repository, error) { return tc.ServersRepo(), nil } authTokenRepoFn := func() (*authtoken.Repository, error) { return tc.AuthTokenRepo(), nil } privCtx := auth.NewVerifierContext( tc.Context(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: aToken.Id, EncryptedToken: strings.Split(aToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }, ) unprivCtx := auth.NewVerifierContext( tc.Context(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: uToken.Id, EncryptedToken: strings.Split(uToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }, ) sqldb, err := tc.DbConn().SqlDB(tc.Context()) require.NoError(t, err) org, proj := iam.TestScopes(t, tc.IamRepo()) org.Name = "defaultOrg" org.Description = "defaultOrg" org, _, err = tc.IamRepo().UpdateScope(tc.Context(), org, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for listing key version destructions in org listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "ids=*;type=*;actions=list-key-version-destruction-jobs") _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) // Create a oidc auth method to create an encrypted value in this scope databaseWrapper, err := tc.Kms().GetWrapper(tc.Context(), org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, org.PublicId, oidc.InactiveState, "noAccounts", "fido") proj.Name = "defaultProj" proj.Description = "defaultProj" proj, _, err = tc.IamRepo().UpdateScope(tc.Context(), proj, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for listing key version destructions in project listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "ids=*;type=*;actions=list-key-version-destruction-jobs") _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) // Create a oidc auth method to create an encrypted value in this scope databaseWrapper, err = tc.Kms().GetWrapper(tc.Context(), proj.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, proj.PublicId, oidc.InactiveState, "noAccounts", "fido") for _, scope := range []string{"global", org.PublicId, proj.PublicId} { err = tc.Kms().RotateKeys(tc.Context(), scope) require.NoError(t, err) keys, err := tc.Kms().ListKeys(tc.Context(), scope) require.NoError(t, err) var kvToDestroy wrappingKms.KeyVersion for _, key := range keys { if key.Purpose == wrappingKms.KeyPurpose(kms.KeyPurposeDatabase.String()) { kvToDestroy = key.Versions[0] } } destroyed, err := tc.Kms().DestroyKeyVersion(tc.Context(), scope, kvToDestroy.Id) require.NoError(t, err) assert.False(t, destroyed) t.Cleanup(func() { _, err = sqldb.ExecContext(tc.Context(), "delete from kms_data_key_version_destruction_job where key_id=$1", kvToDestroy.Id) require.NoError(t, err) }) } cases := []struct { name string req *pbs.ListKeyVersionDestructionJobsRequest res *pbs.ListKeyVersionDestructionJobsResponse authCtx context.Context err error }{ { name: "List key version destruction jobs in the global scope", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "global"}, res: &pbs.ListKeyVersionDestructionJobsResponse{ Items: []*pb.KeyVersionDestructionJob{ { Scope: &pb.ScopeInfo{ Id: "global", Type: "global", Name: "global", Description: "Global Scope", }, Status: "pending", CompletedCount: 0, TotalCount: 7, }, }, }, authCtx: privCtx, }, { name: "List key version destruction jobs in an existing org", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: org.GetPublicId()}, res: &pbs.ListKeyVersionDestructionJobsResponse{ Items: []*pb.KeyVersionDestructionJob{ { Scope: &pb.ScopeInfo{ Id: org.GetPublicId(), ParentScopeId: "global", Type: "org", Name: org.GetName(), Description: org.GetDescription(), }, Status: "pending", CompletedCount: 0, TotalCount: 1, }, }, }, authCtx: privCtx, }, { name: "List key version destruction jobs in a non existing org", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "o_DoesntExis"}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "List key version destruction jobs in an existing project", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: proj.GetPublicId()}, res: &pbs.ListKeyVersionDestructionJobsResponse{ Items: []*pb.KeyVersionDestructionJob{ { Scope: &pb.ScopeInfo{ Id: proj.GetPublicId(), ParentScopeId: org.GetPublicId(), Type: "project", Name: proj.GetName(), Description: proj.GetDescription(), }, Status: "pending", CompletedCount: 0, TotalCount: 1, }, }, }, authCtx: privCtx, }, { name: "List key version destruction jobs in a non existing project", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "p_DoesntExist"}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Wrong id prefix", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "j_1234567890"}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "space in id", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "p_1 23456789"}, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "unauthorized", req: &pbs.ListKeyVersionDestructionJobsRequest{ScopeId: "global"}, authCtx: unprivCtx, err: handlers.ApiErrorWithCode(codes.PermissionDenied), }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), iamRepoFn, tc.Kms(), 1000) require.NoError(err, "Couldn't create new project service.") got, gErr := s.ListKeyVersionDestructionJobs(tt.authCtx, tt.req) if tt.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tt.err), "ListKeyVersionDestructionJobs(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) } else { require.NoError(gErr) } assert.Empty( cmp.Diff( tt.res, got, protocmp.Transform(), protocmp.SortRepeated(func(i, j *pb.KeyVersionDestructionJob) bool { return i.GetTotalCount() < j.GetTotalCount() }), protocmp.IgnoreFields(&pb.KeyVersionDestructionJob{}, "key_version_id", "created_time"), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ), "ListKeyVersionDestructionJobs(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res, ) }) } } func TestDestroyKeyVersion(t *testing.T) { tc := controller.NewTestController(t, nil) aToken := tc.Token() uToken := tc.UnprivilegedToken() iamRepoFn := func() (*iam.Repository, error) { return tc.IamRepo(), nil } serversRepoFn := func() (*server.Repository, error) { return tc.ServersRepo(), nil } authTokenRepoFn := func() (*authtoken.Repository, error) { return tc.AuthTokenRepo(), nil } privCtx := auth.NewVerifierContext( tc.Context(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: aToken.Id, EncryptedToken: strings.Split(aToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }, ) unprivCtx := auth.NewVerifierContext( tc.Context(), iamRepoFn, authTokenRepoFn, serversRepoFn, tc.Kms(), &authpb.RequestInfo{ PublicId: uToken.Id, EncryptedToken: strings.Split(uToken.Token, "_")[2], TokenFormat: uint32(auth.AuthTokenTypeBearer), }, ) org, proj := iam.TestScopes(t, tc.IamRepo()) org.Name = "defaultOrg" org.Description = "defaultOrg" org, _, err := tc.IamRepo().UpdateScope(tc.Context(), org, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for destroying key versions in org listKeysRole := iam.TestRole(t, tc.DbConn(), org.PublicId) _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "ids=*;type=*;actions=destroy-key-version") _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) // Create a oidc auth method to create an encrypted value in this scope databaseWrapper, err := tc.Kms().GetWrapper(tc.Context(), org.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, org.PublicId, oidc.InactiveState, "noAccounts", "fido") proj.Name = "defaultProj" proj.Description = "defaultProj" proj, _, err = tc.IamRepo().UpdateScope(tc.Context(), proj, 1, []string{"Name", "Description"}) require.NoError(t, err) // Add new role for destroying key versions in project listKeysRole = iam.TestRole(t, tc.DbConn(), proj.PublicId) _ = iam.TestRoleGrant(t, tc.DbConn(), listKeysRole.PublicId, "ids=*;type=*;actions=destroy-key-version") _ = iam.TestUserRole(t, tc.DbConn(), listKeysRole.PublicId, aToken.UserId) // Create a oidc auth method to create an encrypted value in this scope databaseWrapper, err = tc.Kms().GetWrapper(tc.Context(), proj.PublicId, kms.KeyPurposeDatabase) require.NoError(t, err) _ = oidc.TestAuthMethod(t, tc.DbConn(), databaseWrapper, proj.PublicId, oidc.InactiveState, "noAccounts", "fido") scopeToImmediatelyDestroyedKeyVersionId := map[string]string{} scopeToPendingDestructionKeyVersionId := map[string]string{} for _, scope := range []string{"global", org.PublicId, proj.PublicId} { err = tc.Kms().RotateKeys(tc.Context(), scope) require.NoError(t, err) keys, err := tc.Kms().ListKeys(tc.Context(), scope) require.NoError(t, err) for _, key := range keys { switch key.Purpose { case wrappingKms.KeyPurpose(kms.KeyPurposeDatabase.String()): scopeToPendingDestructionKeyVersionId[scope] = key.Versions[0].Id case wrappingKms.KeyPurpose(kms.KeyPurposeRootKey.String()): scopeToImmediatelyDestroyedKeyVersionId[scope] = key.Versions[0].Id } } } cases := []struct { name string req *pbs.DestroyKeyVersionRequest res *pbs.DestroyKeyVersionResponse authCtx context.Context err error }{ { name: "Errors when specifying a non existing key version Id", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "global", KeyVersionId: "krkv_DoesntExist", }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Creates a key version destruction job in the global scope", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "global", KeyVersionId: scopeToPendingDestructionKeyVersionId["global"], }, res: &pbs.DestroyKeyVersionResponse{ State: "pending", }, authCtx: privCtx, }, { name: "Immediately destroys a root key version in the global scope", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "global", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], }, res: &pbs.DestroyKeyVersionResponse{ State: "completed", }, authCtx: privCtx, }, { name: "Creates a key version destruction job in an existing org", req: &pbs.DestroyKeyVersionRequest{ ScopeId: org.GetPublicId(), KeyVersionId: scopeToPendingDestructionKeyVersionId[org.GetPublicId()], }, res: &pbs.DestroyKeyVersionResponse{ State: "pending", }, authCtx: privCtx, }, { name: "Immediately destroys a root key version in an existing org", req: &pbs.DestroyKeyVersionRequest{ ScopeId: org.GetPublicId(), KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[org.GetPublicId()], }, res: &pbs.DestroyKeyVersionResponse{ State: "completed", }, authCtx: privCtx, }, { name: "Creates a key version destruction job in an existing project", req: &pbs.DestroyKeyVersionRequest{ ScopeId: proj.GetPublicId(), KeyVersionId: scopeToPendingDestructionKeyVersionId[proj.GetPublicId()], }, res: &pbs.DestroyKeyVersionResponse{ State: "pending", }, authCtx: privCtx, }, { name: "Immediately destroys a root key version in an existing project", req: &pbs.DestroyKeyVersionRequest{ ScopeId: proj.GetPublicId(), KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[proj.GetPublicId()], }, res: &pbs.DestroyKeyVersionResponse{ State: "completed", }, authCtx: privCtx, }, { name: "Errors when specifying a key version that doesn't exist in the scope", req: &pbs.DestroyKeyVersionRequest{ ScopeId: org.GetPublicId(), KeyVersionId: scopeToPendingDestructionKeyVersionId[proj.GetPublicId()], }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Errors when specifying a non existing org", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "o_DoesntExist", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[org.GetPublicId()], }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Errors when specifying a non existing project", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "p_DoesntExist", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId[proj.GetPublicId()], }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.NotFound), }, { name: "Wrong id prefix", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "j_1234567890", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "space in id", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "p_1 23456789", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], }, authCtx: privCtx, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { name: "unauthorized", req: &pbs.DestroyKeyVersionRequest{ ScopeId: "global", KeyVersionId: scopeToImmediatelyDestroyedKeyVersionId["global"], }, authCtx: unprivCtx, err: handlers.ApiErrorWithCode(codes.PermissionDenied), }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) s, err := scopes.NewServiceFn(context.Background(), iamRepoFn, tc.Kms(), 1000) require.NoError(err, "Couldn't create new project service.") got, gErr := s.DestroyKeyVersion(tt.authCtx, tt.req) if tt.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tt.err), "DestroyKeyVersion(%+v) got error\n%v, wanted\n%v", tt.req, gErr, tt.err) } else { require.NoError(gErr) } assert.Empty(cmp.Diff( tt.res, got, protocmp.Transform(), cmpopts.SortSlices(func(a, b string) bool { return a < b }), cmpopts.SortSlices(func(a, b protocmp.Message) bool { return a.String() < b.String() }), ), "DestroyKeyVersion(%q) got response\n%q, wanted\n%q", tt.req, got, tt.res) }) } } func TestAttachStoragePolicy(t *testing.T) { t.Run("unimplemented", func(t *testing.T) { service := &scopes.Service{} _, err := service.AttachStoragePolicy(context.Background(), &pbs.AttachStoragePolicyRequest{}) require.Error(t, err) gotStatus, ok := status.FromError(err) require.True(t, ok) assert.Equal(t, gotStatus.Code(), codes.Unimplemented) assert.Equal(t, gotStatus.Message(), "Policies are an Enterprise-only feature") }) } func TestDetachStoragePolicy(t *testing.T) { t.Run("unimplemented", func(t *testing.T) { service := &scopes.Service{} _, err := service.DetachStoragePolicy(context.Background(), &pbs.DetachStoragePolicyRequest{}) require.Error(t, err) gotStatus, ok := status.FromError(err) require.True(t, ok) assert.Equal(t, gotStatus.Code(), codes.Unimplemented) assert.Equal(t, gotStatus.Message(), "Policies are an Enterprise-only feature") }) }