From 85e3b7449c417a238e12bac2b312ce4d58905804 Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:46:10 +0200 Subject: [PATCH] fix: correct permissions for projects on v2 api (#9973) # Which Problems Are Solved Permission checks in project v2beta API did not cover projects and granted projects correctly. # How the Problems Are Solved Add permission checks v1 correctly to the list queries, add correct permission checks v2 for projects. # Additional Changes Correct Pre-Checks for project grants that the right resource owner is used. # Additional Context Permission checks v2 for project grants is still outstanding under #9972. --- internal/api/grpc/management/project_grant.go | 4 +- .../v2beta/integration/project_grant_test.go | 65 ++- .../v2beta/integration/project_test.go | 65 +++ .../project/v2beta/integration/query_test.go | 182 ++++++- .../api/grpc/project/v2beta/project_grant.go | 10 +- internal/command/permission_checks.go | 18 +- internal/command/project_grant.go | 116 +++-- internal/command/project_grant_model.go | 62 +-- internal/command/project_grant_test.go | 444 +++++++++++++++++- internal/command/project_old.go | 2 +- internal/domain/roles.go | 1 + internal/integration/client.go | 18 + internal/query/project.go | 48 +- internal/query/project_grant.go | 15 +- internal/query/project_role.go | 2 +- 15 files changed, 950 insertions(+), 102 deletions(-) diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index e9313c1327..d84375818d 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -109,7 +109,7 @@ func (s *Server) UpdateProjectGrant(ctx context.Context, req *mgmt_pb.UpdateProj } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.DeactivateProjectGrantRequest) (*mgmt_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } @@ -119,7 +119,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *mgmt_pb.Deacti } func (s *Server) ReactivateProjectGrant(ctx context.Context, req *mgmt_pb.ReactivateProjectGrantRequest) (*mgmt_pb.ReactivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, authz.GetCtxData(ctx).OrgID) + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantId, "", authz.GetCtxData(ctx).OrgID) if err != nil { return nil, err } diff --git a/internal/api/grpc/project/v2beta/integration/project_grant_test.go b/internal/api/grpc/project/v2beta/integration/project_grant_test.go index 8500f24d56..34fa10e3de 100644 --- a/internal/api/grpc/project/v2beta/integration/project_grant_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_grant_test.go @@ -169,6 +169,12 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type want struct { creationDate bool } @@ -206,6 +212,33 @@ func TestServer_CreateProjectGrant_Permission(t *testing.T) { req: &project.CreateProjectGrantRequest{}, wantErr: true, }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), @@ -405,6 +438,13 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectGrant(iamOwnerCtx, t, projectID, orgResp.GetOrganizationId()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectID, orgResp.GetOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectGrantRequest @@ -458,6 +498,25 @@ func TestServer_UpdateProjectGrant_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{gofakeit.Animal(), gofakeit.Animal(), gofakeit.Animal()} + request.ProjectId = projectID + request.GrantedOrganizationId = orgResp.GetOrganizationId() + + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectID, role, role, "") + } + + request.RoleKeys = roles + }, + args: args{ + ctx: projectGrantOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, { name: "organization owner, other org", prepare: func(request *project.UpdateProjectGrantRequest) { @@ -598,7 +657,7 @@ func TestServer_DeleteProjectGrant(t *testing.T) { ProjectId: "notexisting", GrantedOrganizationId: "notexisting", }, - wantErr: true, + wantDeletionDate: false, }, { name: "delete", @@ -650,8 +709,8 @@ func TestServer_DeleteProjectGrant(t *testing.T) { instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) return creationDate, time.Now().UTC() }, - req: &project.DeleteProjectGrantRequest{}, - wantErr: true, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, }, } for _, tt := range tests { diff --git a/internal/api/grpc/project/v2beta/integration/project_test.go b/internal/api/grpc/project/v2beta/integration/project_test.go index 6c0a5c96f6..5412f6eb58 100644 --- a/internal/api/grpc/project/v2beta/integration/project_test.go +++ b/internal/api/grpc/project/v2beta/integration/project_test.go @@ -300,6 +300,12 @@ func TestServer_UpdateProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context req *project.UpdateProjectRequest @@ -343,6 +349,36 @@ func TestServer_UpdateProject_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.Id = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(gofakeit.AppName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, { name: "missing permission, other organization", prepare: func(request *project.UpdateProjectRequest) { @@ -499,6 +535,12 @@ func TestServer_DeleteProject_Permission(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + tests := []struct { name string ctx context.Context @@ -531,6 +573,29 @@ func TestServer_DeleteProject_Permission(t *testing.T) { req: &project.DeleteProjectRequest{}, wantErr: true, }, + { + name: "project owner, no permission", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), gofakeit.AppName(), false, false).GetId() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "project owner, ok", + ctx: projectOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + request.Id = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, { name: "organization owner, other org", ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), diff --git a/internal/api/grpc/project/v2beta/integration/query_test.go b/internal/api/grpc/project/v2beta/integration/query_test.go index 517f103628..fc153f0130 100644 --- a/internal/api/grpc/project/v2beta/integration/query_test.go +++ b/internal/api/grpc/project/v2beta/integration/query_test.go @@ -148,6 +148,14 @@ func TestServer_GetProject(t *testing.T) { func TestServer_ListProjects(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + instance.CreateProjectMembership(t, iamOwnerCtx, projectResp.GetId(), userResp.GetUserId()) + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) @@ -370,6 +378,39 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list multiple id, limited permissions, project owner", + args: args{ + ctx: projectOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + resp1 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instance, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{resp1.GetId(), resp2.GetId(), resp3.GetId(), projectResp.GetId()}, + }, + } + response.Projects[0] = grantedProjectResp + response.Projects[1] = projectResp + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 5, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, { name: "list project and granted projects", args: args{ @@ -462,6 +503,51 @@ func TestServer_ListProjects(t *testing.T) { }, }, }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instance.DefaultOrg.GetName()), + GrantedState: 1, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -791,6 +877,53 @@ func TestServer_ListProjects_PermissionV2(t *testing.T) { }, }, }, + // TODO: correct when permission check is added for project grants https://github.com/zitadel/zitadel/issues/9972 + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorization(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := gofakeit.AppName() + projectName := gofakeit.AppName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, gofakeit.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), projectName, true, true) + // projectGrantResp := + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), orgID) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ProjectIds: []string{projectResp.GetId()}}, + } + /* + response.Projects[0] = &project.Project{ + Id: projectResp.GetId(), + Name: projectName, + OrganizationId: orgResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(orgID), + GrantedOrganizationName: gu.Ptr(instancePermissionV2.DefaultOrg.GetName()), + GrantedState: 1, + } + */ + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -865,6 +998,14 @@ func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationRes func TestServer_ListProjectGrants(t *testing.T) { iamOwnerCtx := instance.WithAuthorization(CTX, integration.UserTypeIAMOwner) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectResp := createProject(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), false, false) + projectGrantResp := createProjectGrant(iamOwnerCtx, instance, t, instance.DefaultOrg.GetId(), projectResp.GetId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + type args struct { ctx context.Context dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) @@ -1071,7 +1212,46 @@ func TestServer_ListProjectGrants(t *testing.T) { {}, }, }, - }, { + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := gofakeit.AppName() + name2 := gofakeit.AppName() + name3 := gofakeit.AppName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, gofakeit.AppName(), gofakeit.Email()) + project1Resp := instance.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &project.InProjectIDsFilter{ + ProjectIds: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + response.ProjectGrants[0] = projectGrantResp + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { name: "list single id with role", args: args{ ctx: iamOwnerCtx, diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go index c1c20d9cbc..6c3b195c66 100644 --- a/internal/api/grpc/project/v2beta/project_grant.go +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -56,13 +56,13 @@ func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *com ObjectRoot: models.ObjectRoot{ AggregateID: req.ProjectId, }, - GrantID: req.GrantedOrganizationId, - RoleKeys: req.RoleKeys, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, } } func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.DeactivateProjectGrantRequest) (*project_pb.DeactivateProjectGrantResponse, error) { - details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.DeactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (s *Server) DeactivateProjectGrant(ctx context.Context, req *project_pb.Dea } func (s *Server) ActivateProjectGrant(ctx context.Context, req *project_pb.ActivateProjectGrantRequest) (*project_pb.ActivateProjectGrantResponse, error) { - details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "") + details, err := s.command.ReactivateProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "") if err != nil { return nil, err } @@ -94,7 +94,7 @@ func (s *Server) DeleteProjectGrant(ctx context.Context, req *project_pb.DeleteP if err != nil { return nil, err } - details, err := s.command.RemoveProjectGrant(ctx, req.ProjectId, req.GrantedOrganizationId, "", userGrantIDs...) + details, err := s.command.DeleteProjectGrant(ctx, req.ProjectId, "", req.GrantedOrganizationId, "", userGrantIDs...) if err != nil { return nil, err } diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 253b6ee72a..6bfeaae219 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -68,6 +68,20 @@ func (c *Commands) checkPermissionUpdateProject(ctx context.Context, resourceOwn return c.newPermissionCheck(ctx, domain.PermissionProjectWrite, project.AggregateType)(resourceOwner, projectID) } -func (c *Commands) checkPermissionWriteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID) +func (c *Commands) checkPermissionUpdateProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantWrite, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil +} + +func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectID, projectGrantID string) (err error) { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { + if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantDelete, project.AggregateType)(resourceOwner, projectID); err != nil { + return err + } + } + return nil } diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 763ea7ab67..b613974b7e 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -58,11 +58,11 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) if grant.ResourceOwner == "" { grant.ResourceOwner = projectResourceOwner } - if err := c.checkPermissionWriteProjectGrant(ctx, grant.ResourceOwner, grant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil { return nil, err } - wm := NewProjectGrantWriteModel(grant.GrantID, grant.AggregateID, grant.ResourceOwner) + wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) // error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid") @@ -83,19 +83,24 @@ func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) type ChangeProjectGrant struct { es_models.ObjectRoot - GrantID string - RoleKeys []string + GrantID string + GrantedOrgID string + RoleKeys []string } func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) { - if grant.GrantID == "" { + if grant.GrantID == "" && grant.GrantedOrgID == "" { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.AggregateID, grant.ResourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner) if err != nil { return nil, err } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } + + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys) @@ -152,12 +157,12 @@ func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectG } func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) { - existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, projectID, "") + existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "") if err != nil { return nil, nil, err } - if existingProjectGrant.State == domain.ProjectGrantStateUnspecified || existingProjectGrant.State == domain.ProjectGrantStateRemoved { - return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.Grant.NotFound") + if !existingProjectGrant.State.Exists() { + return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } keyExists := false for i, key := range existingProjectGrant.RoleKeys { @@ -172,7 +177,7 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e if !keyExists { return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound") } - changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, existingProjectGrant.ResourceOwner) + changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner) if cascade { return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil @@ -181,8 +186,8 @@ func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *e return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil } -func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -191,10 +196,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid") @@ -207,13 +215,13 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateActive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantDeactivateEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -226,8 +234,8 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string) (details *domain.ObjectDetails, err error) { - if grantID == "" || projectID == "" { +func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing") } @@ -236,10 +244,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { return details, err } + if !existingGrant.State.Exists() { + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + } // error if provided resourceowner is not equal to the resourceowner of the project if projectResourceOwner != existingGrant.ResourceOwner { return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid") @@ -252,13 +263,13 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI if existingGrant.State != domain.ProjectGrantStateInactive { return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive") } - if err := c.checkPermissionWriteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } pushedEvents, err := c.eventstore.Push(ctx, project.NewGrantReactivatedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, ), ) if err != nil { @@ -271,25 +282,25 @@ func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } +// Deprecated: use commands.DeleteProjectGrant func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { if grantID == "" || projectID == "" { return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, projectID, resourceOwner) + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner) if err != nil { return details, err } - // return if project grant does not exist, or was removed already if !existingGrant.State.Exists() { - return writeModelToObjectDetails(&existingGrant.WriteModel), nil + return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.GrantID); err != nil { + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { return nil, err } events := make([]eventstore.Command, 0) events = append(events, project.NewGrantRemovedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), - grantID, + existingGrant.GrantID, existingGrant.GrantedOrgID, ), ) @@ -297,7 +308,7 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r for _, userGrantID := range cascadeUserGrantIDs { event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) if err != nil { - logging.LogWithFields("COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") continue } events = append(events, event) @@ -313,24 +324,57 @@ func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, r return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) checkPermissionDeleteProjectGrant(ctx context.Context, resourceOwner, projectGrantID string) error { - return c.checkPermission(ctx, domain.PermissionProjectGrantDelete, resourceOwner, projectGrantID) +func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) { + if (grantID == "" && grantedOrgID == "") || projectID == "" { + return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing") + } + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) + if err != nil { + return details, err + } + // return if project grant does not exist, or was removed already + if !existingGrant.State.Exists() { + return writeModelToObjectDetails(&existingGrant.WriteModel), nil + } + if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil { + return nil, err + } + events := make([]eventstore.Command, 0) + events = append(events, project.NewGrantRemovedEvent(ctx, + ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel), + existingGrant.GrantID, + existingGrant.GrantedOrgID, + ), + ) + + for _, userGrantID := range cascadeUserGrantIDs { + event, _, err := c.removeUserGrant(ctx, userGrantID, "", true) + if err != nil { + logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant") + continue + } + events = append(events, event) + } + pushedEvents, err := c.eventstore.Push(ctx, events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(existingGrant, pushedEvents...) + if err != nil { + return nil, err + } + return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { +func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - writeModel := NewProjectGrantWriteModel(grantID, projectID, resourceOwner) + writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner) err = c.eventstore.FilterToQueryReducer(ctx, writeModel) if err != nil { return nil, err } - - if writeModel.State == domain.ProjectGrantStateUnspecified || writeModel.State == domain.ProjectGrantStateRemoved { - return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") - } - return writeModel, nil } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index a8c1fe2850..15950d4f3d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -16,13 +16,14 @@ type ProjectGrantWriteModel struct { State domain.ProjectGrantState } -func NewProjectGrantWriteModel(grantID, projectID, resourceOwner string) *ProjectGrantWriteModel { +func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { return &ProjectGrantWriteModel{ WriteModel: eventstore.WriteModel{ AggregateID: projectID, ResourceOwner: resourceOwner, }, - GrantID: grantID, + GrantID: grantID, + GrantedOrgID: grantedOrgID, } } @@ -30,27 +31,28 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if e.GrantID == wm.GrantID { + if (wm.GrantID != "" && e.GrantID == wm.GrantID) || + (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if e.GrantID == wm.GrantID { + if wm.GrantID != "" && e.GrantID == wm.GrantID { wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -114,18 +116,20 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { type ProjectGrantPreConditionReadModel struct { eventstore.WriteModel - ProjectID string - GrantedOrgID string - ProjectExists bool - GrantedOrgExists bool - ExistingRoleKeys []string + ProjectResourceOwner string + ProjectID string + GrantedOrgID string + ProjectExists bool + GrantedOrgExists bool + ExistingRoleKeys []string } func NewProjectGrantPreConditionReadModel(projectID, grantedOrgID, resourceOwner string) *ProjectGrantPreConditionReadModel { return &ProjectGrantPreConditionReadModel{ - WriteModel: eventstore.WriteModel{ResourceOwner: resourceOwner}, - ProjectID: projectID, - GrantedOrgID: grantedOrgID, + WriteModel: eventstore.WriteModel{}, + ProjectResourceOwner: resourceOwner, + ProjectID: projectID, + GrantedOrgID: grantedOrgID, } } @@ -133,26 +137,26 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { case *project.ProjectAddedEvent: - if wm.ResourceOwner == "" { - wm.ResourceOwner = e.Aggregate().ResourceOwner + if wm.ProjectResourceOwner == "" { + wm.ProjectResourceOwner = e.Aggregate().ResourceOwner } - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } wm.ProjectExists = true case *project.ProjectRemovedEvent: - if wm.ResourceOwner != e.Aggregate().ResourceOwner { + if wm.ProjectResourceOwner != e.Aggregate().ResourceOwner { continue } - wm.ResourceOwner = "" + wm.ProjectResourceOwner = "" wm.ProjectExists = false case *project.RoleAddedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } wm.ExistingRoleKeys = append(wm.ExistingRoleKeys, e.Key) case *project.RoleRemovedEvent: - if e.Aggregate().ResourceOwner != wm.ResourceOwner { + if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } for i, key := range wm.ExistingRoleKeys { @@ -175,12 +179,6 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuilder { query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). AddQuery(). - AggregateTypes(org.AggregateType). - AggregateIDs(wm.GrantedOrgID). - EventTypes( - org.OrgAddedEventType, - org.OrgRemovedEventType). - Or(). AggregateTypes(project.AggregateType). AggregateIDs(wm.ProjectID). EventTypes( @@ -188,6 +186,12 @@ func (wm *ProjectGrantPreConditionReadModel) Query() *eventstore.SearchQueryBuil project.ProjectRemovedType, project.RoleAddedType, project.RoleRemovedType). + Or(). + AggregateTypes(org.AggregateType). + AggregateIDs(wm.GrantedOrgID). + EventTypes( + org.OrgAddedEventType, + org.OrgRemovedEventType). Builder() return query diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index f1befa0de2..7a3bb98e7d 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -720,6 +720,76 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant only added roles, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + eventFromEventPusher( + org.NewOrgAddedEvent(context.Background(), + &org.NewAggregate("grantedorg1").Aggregate, + "granted org", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key1", + "key", + "", + ), + ), + eventFromEventPusher( + project.NewRoleAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "key2", + "key2", + "", + ), + ), + ), + expectPush( + project.NewGrantChangedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + []string{"key1", "key2"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectGrant: &ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + ResourceOwner: "org1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ @@ -907,6 +977,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1076,6 +1147,48 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant deactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1083,7 +1196,7 @@ func TestCommandSide_DeactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.DeactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1106,6 +1219,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { ctx context.Context projectID string grantID string + grantedOrgID string resourceOwner string } type res struct { @@ -1275,6 +1389,52 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant reactivate, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewProjectAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", true, true, true, + domain.PrivateLabelingSettingUnspecified, + ), + ), + ), + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher(project.NewGrantDeactivateEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + )), + ), + expectPush( + project.NewGrantReactivatedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1282,7 +1442,7 @@ func TestCommandSide_ReactivateProjectGrant(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.resourceOwner) + got, err := r.ReactivateProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner) if tt.res.err == nil { assert.NoError(t, err) } @@ -1536,3 +1696,283 @@ func TestCommandSide_RemoveProjectGrant(t *testing.T) { }) } } + +func TestCommandSide_DeleteProjectGrant(t *testing.T) { + type fields struct { + eventstore func(t *testing.T) *eventstore.Eventstore + checkPermission domain.PermissionCheck + } + type args struct { + ctx context.Context + projectID string + grantID string + grantedOrgID string + resourceOwner string + cascadeUserGrantIDs []string + } + type res struct { + want *domain.ObjectDetails + err func(error) bool + } + tests := []struct { + name string + fields fields + args args + res res + }{ + { + name: "missing projectid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "missing grantid, invalid error", + fields: fields{ + eventstore: expectEventstore(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + resourceOwner: "org1", + }, + res: res{ + err: zerrors.IsErrorInvalidArgument, + }, + }, + { + name: "project already removed, precondition failed error", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + eventFromEventPusher( + project.NewProjectRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectname1", + nil, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant not existing, precondition error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, grantedOrgID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantedOrgID: "grantedorg1", + resourceOwner: "org1", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove, cascading usergrant not found, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter(), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + { + name: "projectgrant remove with cascading usergrants, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher(project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + )), + ), + expectFilter( + eventFromEventPusher(usergrant.NewUserGrantAddedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + []string{"key1"}))), + expectPush( + project.NewGrantRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + ), + usergrant.NewUserGrantCascadeRemovedEvent(context.Background(), + &usergrant.NewAggregate("usergrant1", "org1").Aggregate, + "user1", + "project1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + grantID: "projectgrant1", + resourceOwner: "org1", + cascadeUserGrantIDs: []string{"usergrant1"}, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Commands{ + eventstore: tt.fields.eventstore(t), + checkPermission: tt.fields.checkPermission, + } + got, err := r.DeleteProjectGrant(tt.args.ctx, tt.args.projectID, tt.args.grantID, tt.args.grantedOrgID, tt.args.resourceOwner, tt.args.cascadeUserGrantIDs...) + if tt.res.err == nil { + assert.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assertObjectDetails(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/command/project_old.go b/internal/command/project_old.go index e1b6f02721..99d7dd2e34 100644 --- a/internal/command/project_old.go +++ b/internal/command/project_old.go @@ -94,5 +94,5 @@ func (c *Commands) checkProjectGrantPreConditionOld(ctx context.Context, project if domain.HasInvalidRoles(preConditions.ExistingRoleKeys, roles) { return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound") } - return preConditions.ResourceOwner, nil + return preConditions.ProjectResourceOwner, nil } diff --git a/internal/domain/roles.go b/internal/domain/roles.go index b6bf2ffadd..c40eef6120 100644 --- a/internal/domain/roles.go +++ b/internal/domain/roles.go @@ -16,6 +16,7 @@ const ( RoleIAMOwner = "IAM_OWNER" RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" + RoleProjectGrantOwner = "PROJECT_GRANT_OWNER" RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL" ) diff --git a/internal/integration/client.go b/internal/integration/client.go index 3bf794f5f6..838a728faf 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -269,6 +269,14 @@ func (i *Instance) CreateUserTypeMachine(ctx context.Context) *user_v2.CreateUse return resp } +func (i *Instance) CreatePersonalAccessToken(ctx context.Context, userID string) *user_v2.AddPersonalAccessTokenResponse { + resp, err := i.Client.UserV2.AddPersonalAccessToken(ctx, &user_v2.AddPersonalAccessTokenRequest{ + UserId: userID, + }) + logging.OnError(err).Panic("create pat") + return resp +} + // TriggerUserByID makes sure the user projection gets triggered after creation. func (i *Instance) TriggerUserByID(ctx context.Context, users ...string) { var wg sync.WaitGroup @@ -903,6 +911,16 @@ func (i *Instance) CreateProjectMembership(t *testing.T, ctx context.Context, pr require.NoError(t, err) } +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { + _, err := i.Client.Mgmt.AddProjectGrantMember(ctx, &mgmt.AddProjectGrantMemberRequest{ + ProjectId: projectID, + GrantId: grantID, + UserId: userID, + Roles: []string{domain.RoleProjectGrantOwner}, + }) + require.NoError(t, err) +} + func (i *Instance) CreateTarget(ctx context.Context, t *testing.T, name, endpoint string, ty domain.TargetType, interrupt bool) *action.CreateTargetResponse { if name == "" { name = gofakeit.Name() diff --git a/internal/query/project.go b/internal/query/project.go index ab58bd11a8..728731f7cb 100644 --- a/internal/query/project.go +++ b/internal/query/project.go @@ -126,6 +126,10 @@ var ( name: "project_grant_resource_owner", table: grantedProjectsAlias, } + grantedProjectColumnGrantID = Column{ + name: projection.ProjectGrantColumnGrantID, + table: grantedProjectsAlias, + } grantedProjectColumnGrantedOrganization = Column{ name: projection.ProjectGrantColumnGrantedOrgID, table: grantedProjectsAlias, @@ -157,20 +161,6 @@ func projectCheckPermission(ctx context.Context, resourceOwner string, projectID return permissionCheck(ctx, domain.PermissionProjectRead, resourceOwner, projectID) } -func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { - if !enabled { - return query - } - join, args := PermissionClause( - ctx, - grantedProjectColumnResourceOwner, - domain.PermissionProjectRead, - SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(GrantedProjectColumnID), - ) - return query.JoinClause(join, args...) -} - type Project struct { ID string CreationDate time.Time @@ -277,6 +267,20 @@ func (q *ProjectAndGrantedProjectSearchQueries) toQuery(query sq.SelectBuilder) return query } +func projectPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectAndGrantedProjectSearchQueries) sq.SelectBuilder { + if !enabled { + return query + } + join, args := PermissionClause( + ctx, + grantedProjectColumnResourceOwner, + domain.PermissionProjectRead, + SingleOrgPermissionOption(queries.Queries), + WithProjectsPermissionOption(GrantedProjectColumnID), + ) + return query.JoinClause(join, args...) +} + func (q *Queries) SearchGrantedProjects(ctx context.Context, queries *ProjectAndGrantedProjectSearchQueries, permissionCheck domain.PermissionCheck) (*GrantedProjects, error) { permissionCheckV2 := PermissionV2(ctx, permissionCheck) projects, err := q.searchGrantedProjects(ctx, queries, permissionCheckV2) @@ -328,11 +332,11 @@ func NewGrantedProjectIDSearchQuery(ids []string) (SearchQuery, error) { } func NewGrantedProjectOrganizationIDSearchQuery(value string) (SearchQuery, error) { - project, err := NewTextQuery(grantedProjectColumnResourceOwner, value, TextEquals) + project, err := NewGrantedProjectResourceOwnerSearchQuery(value) if err != nil { return nil, err } - grant, err := NewTextQuery(grantedProjectColumnGrantedOrganization, value, TextEquals) + grant, err := NewGrantedProjectGrantedOrganizationIDSearchQuery(value) if err != nil { return nil, err } @@ -494,6 +498,9 @@ type GrantedProjects struct { func grantedProjectsCheckPermission(ctx context.Context, grantedProjects *GrantedProjects, permissionCheck domain.PermissionCheck) { grantedProjects.GrantedProjects = slices.DeleteFunc(grantedProjects.GrantedProjects, func(grantedProject *GrantedProject) bool { + if grantedProject.GrantedOrgID != "" { + return projectGrantCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, grantedProject.GrantID, grantedProject.GrantedOrgID, permissionCheck) != nil + } return projectCheckPermission(ctx, grantedProject.ResourceOwner, grantedProject.ProjectID, permissionCheck) != nil }, ) @@ -513,6 +520,7 @@ type GrantedProject struct { HasProjectCheck bool PrivateLabelingSetting domain.PrivateLabelingSetting + GrantID string GrantedOrgID string OrgName string ProjectGrantState domain.ProjectGrantState @@ -531,6 +539,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP grantedProjectColumnProjectRoleCheck.identifier(), grantedProjectColumnHasProjectCheck.identifier(), grantedProjectColumnPrivateLabelingSetting.identifier(), + grantedProjectColumnGrantID.identifier(), grantedProjectColumnGrantedOrganization.identifier(), grantedProjectColumnGrantedOrganizationName.identifier(), grantedProjectColumnGrantState.identifier(), @@ -541,6 +550,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP projects := make([]*GrantedProject, 0) var ( count uint64 + grantID = sql.NullString{} orgID = sql.NullString{} orgName = sql.NullString{} projectGrantState = sql.NullInt16{} @@ -559,6 +569,7 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP &grantedProject.ProjectRoleCheck, &grantedProject.HasProjectCheck, &grantedProject.PrivateLabelingSetting, + &grantID, &orgID, &orgName, &projectGrantState, @@ -567,6 +578,9 @@ func prepareGrantedProjectsQuery() (sq.SelectBuilder, func(*sql.Rows) (*GrantedP if err != nil { return nil, err } + if grantID.Valid { + grantedProject.GrantID = grantID.String + } if orgID.Valid { grantedProject.GrantedOrgID = orgID.String } @@ -614,6 +628,7 @@ func prepareProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, "NULL::TEXT AS "+grantedProjectColumnGrantResourceOwner.name, + "NULL::TEXT AS "+grantedProjectColumnGrantID.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganization.name, "NULL::TEXT AS "+grantedProjectColumnGrantedOrganizationName.name, "NULL::SMALLINT AS "+grantedProjectColumnGrantState.name, @@ -641,6 +656,7 @@ func prepareGrantedProjects() string { ProjectColumnHasProjectCheck.identifier()+" AS "+grantedProjectColumnHasProjectCheck.name, ProjectColumnPrivateLabelingSetting.identifier()+" AS "+grantedProjectColumnPrivateLabelingSetting.name, ProjectGrantColumnResourceOwner.identifier()+" AS "+grantedProjectColumnGrantResourceOwner.name, + ProjectGrantColumnGrantID.identifier()+" AS "+grantedProjectColumnGrantID.name, ProjectGrantColumnGrantedOrgID.identifier()+" AS "+grantedProjectColumnGrantedOrganization.name, ProjectGrantColumnGrantedOrgName.identifier()+" AS "+grantedProjectColumnGrantedOrganizationName.name, ProjectGrantColumnState.identifier()+" AS "+grantedProjectColumnGrantState.name, diff --git a/internal/query/project_grant.go b/internal/query/project_grant.go index a0dbd7c121..3093d26f30 100644 --- a/internal/query/project_grant.go +++ b/internal/query/project_grant.go @@ -108,15 +108,23 @@ type ProjectGrantSearchQueries struct { func projectGrantsCheckPermission(ctx context.Context, projectGrants *ProjectGrants, permissionCheck domain.PermissionCheck) { projectGrants.ProjectGrants = slices.DeleteFunc(projectGrants.ProjectGrants, func(projectGrant *ProjectGrant) bool { - return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.GrantID, permissionCheck) != nil + return projectGrantCheckPermission(ctx, projectGrant.ResourceOwner, projectGrant.ProjectID, projectGrant.GrantID, projectGrant.GrantedOrgID, permissionCheck) != nil }, ) } -func projectGrantCheckPermission(ctx context.Context, resourceOwner string, grantID string, permissionCheck domain.PermissionCheck) error { - return permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID) +func projectGrantCheckPermission(ctx context.Context, resourceOwner, projectID, grantID, grantedOrgID string, permissionCheck domain.PermissionCheck) error { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, grantedOrgID, grantID); err != nil { + if err := permissionCheck(ctx, domain.PermissionProjectGrantRead, resourceOwner, projectID); err != nil { + return err + } + } + } + return nil } +// TODO: add permission check on project grant level func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, enabled bool, queries *ProjectGrantSearchQueries) sq.SelectBuilder { if !enabled { return query @@ -126,7 +134,6 @@ func projectGrantPermissionCheckV2(ctx context.Context, query sq.SelectBuilder, ProjectGrantColumnResourceOwner, domain.PermissionProjectGrantRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectGrantColumnGrantID), ) return query.JoinClause(join, args...) } diff --git a/internal/query/project_role.go b/internal/query/project_role.go index 15ae806cd4..e70fcf277e 100644 --- a/internal/query/project_role.go +++ b/internal/query/project_role.go @@ -103,7 +103,7 @@ func projectRolePermissionCheckV2(ctx context.Context, query sq.SelectBuilder, e ProjectRoleColumnResourceOwner, domain.PermissionProjectRoleRead, SingleOrgPermissionOption(queries.Queries), - OwnedRowsPermissionOption(ProjectRoleColumnKey), + WithProjectsPermissionOption(ProjectRoleColumnProjectID), ) return query.JoinClause(join, args...) }