From b6ff4ff16c47d8243ce5b4f1133c57207a876eba Mon Sep 17 00:00:00 2001 From: Stefan Benz <46600784+stebenz@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:04:25 +0200 Subject: [PATCH] fix: generated project grant id (#10747) # Which Problems Are Solved Project Grant ID would have needed to be unique to be handled properly on the projections, but was defined as the organization ID the project was granted to, so could be non-unique. # How the Problems Are Solved Generate the Project Grant ID even in the v2 APIs, which also includes fixes in the integration tests. Additionally to that, the logic for some functionality had to be extended as the Project Grant ID is not provided anymore in the API, so had to be queried before creating events for Project Grants. # Additional Changes Included fix for authorizations, when an authorization was intended to be created for a project, without providing any organization information, which also showed some faulty integration tests. # Additional Context Partially closes #10745 --------- Co-authored-by: Livio Spring Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com> --- .../v2beta/integration_test/query_test.go | 53 ++++--- .../v2beta/administrator.go | 18 +-- .../integration_test/administrator_test.go | 87 ++++++++--- .../v2beta/integration_test/query_test.go | 11 +- internal/api/grpc/management/project_grant.go | 2 +- .../management/project_grant_converter.go | 18 +-- .../oidc/v2/integration_test/oidc_test.go | 16 +- .../oidc/v2beta/integration_test/oidc_test.go | 16 +- .../api/grpc/project/v2beta/project_grant.go | 3 +- .../saml/v2/integration_test/saml_test.go | 16 +- internal/command/permission_checks.go | 8 - internal/command/project_grant.go | 8 +- internal/command/project_grant_member.go | 54 ++++--- internal/command/project_grant_member_test.go | 137 ++++++++++++------ internal/command/project_grant_model.go | 50 ++++--- internal/command/project_grant_test.go | 124 ++++++++++++++++ internal/command/user_grant_model.go | 3 +- internal/integration/client.go | 27 +++- .../v2beta/internal_permission_service.proto | 8 +- 19 files changed, 465 insertions(+), 194 deletions(-) diff --git a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go index d623da829..cc342690c 100644 --- a/internal/api/grpc/authorization/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/authorization/v2beta/integration_test/query_test.go @@ -263,9 +263,9 @@ func TestServer_ListAuthorizations(t *testing.T) { userResp := Instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) resp := createAuthorization(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), true) - request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ - ProjectGrantId: &filter.IDFilter{ - Id: resp.GetProjectGrantId(), + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: resp.GetGrantedOrganizationId(), }, } response.Authorizations[0] = resp @@ -330,8 +330,8 @@ func TestServer_ListAuthorizations(t *testing.T) { }, } - response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) - response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + createAuthorizationWithProjectGrant(iamOwnerCtx, Instance, t, Instance.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) }, req: &authorization.ListAuthorizationsRequest{ Filters: []*authorization.AuthorizationsSearchFilter{{}}, @@ -343,7 +343,7 @@ func TestServer_ListAuthorizations(t *testing.T) { AppliedLimit: 100, }, Authorizations: []*authorization.Authorization{ - {}, {}, + {}, }, }, }, @@ -426,6 +426,10 @@ func TestServer_ListAuthorizations(t *testing.T) { // always first check length, otherwise its failed anyway if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) { for i := range tt.want.Authorizations { + // set as project grant id is generated + if grant := got.Authorizations[i].ProjectGrantId; grant != nil { + tt.want.Authorizations[i].ProjectGrantId = grant + } assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i]) } } @@ -454,15 +458,15 @@ func createAuthorizationForProject(ctx context.Context, instance *integration.In userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) require.NoError(t, err) - userGrantResp := instance.CreateProjectUserGrant(t, ctx, orgID, projectID, userID) + authResp := instance.CreateAuthorizationProject(t, ctx, projectID, userID) return &authorization.Authorization{ - Id: userGrantResp.GetUserGrantId(), + Id: authResp.GetId(), ProjectId: projectID, ProjectName: projectName, ProjectOrganizationId: orgID, OrganizationId: orgID, - CreationDate: userGrantResp.Details.GetCreationDate(), - ChangeDate: userGrantResp.Details.GetCreationDate(), + CreationDate: authResp.GetCreationDate(), + ChangeDate: authResp.GetCreationDate(), State: 1, User: &authorization.User{ Id: userID, @@ -486,17 +490,18 @@ func createAuthorizationForProjectGrant(ctx context.Context, instance *integrati userResp, err := instance.Client.UserV2.GetUserByID(ctx, &user.GetUserByIDRequest{UserId: userID}) require.NoError(t, err) - userGrantResp := instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, grantedOrgID, userID) + authResp := instance.CreateAuthorizationProjectGrant(t, ctx, projectID, grantedOrgID, userID) return &authorization.Authorization{ - Id: userGrantResp.GetUserGrantId(), + Id: authResp.GetId(), ProjectId: projectID, ProjectName: projectName, ProjectOrganizationId: orgID, - ProjectGrantId: gu.Ptr(grantedOrgID), + // empty as generated + ProjectGrantId: nil, GrantedOrganizationId: gu.Ptr(grantedOrgID), - OrganizationId: orgID, - CreationDate: userGrantResp.Details.GetCreationDate(), - ChangeDate: userGrantResp.Details.GetCreationDate(), + OrganizationId: grantedOrgID, + CreationDate: authResp.GetCreationDate(), + ChangeDate: authResp.GetCreationDate(), State: 1, User: &authorization.User{ Id: userID, @@ -795,9 +800,9 @@ func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { userResp := InstancePermissionV2.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) resp := createAuthorization(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), true) - request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_ProjectGrantId{ - ProjectGrantId: &filter.IDFilter{ - Id: resp.GetProjectGrantId(), + request.Filters[0].Filter = &authorization.AuthorizationsSearchFilter_OrganizationId{ + OrganizationId: &filter.IDFilter{ + Id: resp.GetGrantedOrganizationId(), }, } response.Authorizations[0] = resp @@ -862,8 +867,8 @@ func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { }, } - response.Authorizations[1] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) - response.Authorizations[0] = createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) + response.Authorizations[0] = createAuthorizationForProject(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), projectResp.GetName(), projectResp.GetId()) + createAuthorizationWithProjectGrant(iamOwnerCtx, InstancePermissionV2, t, InstancePermissionV2.DefaultOrg.GetId(), userResp.GetId(), grantedProjectResp.GetName(), grantedProjectResp.GetId()) }, req: &authorization.ListAuthorizationsRequest{ Filters: []*authorization.AuthorizationsSearchFilter{{}}, @@ -875,7 +880,7 @@ func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { AppliedLimit: 100, }, Authorizations: []*authorization.Authorization{ - {}, {}, + {}, }, }, }, @@ -958,6 +963,10 @@ func TestServer_ListAuthorizations_PermissionsV2(t *testing.T) { // always first check length, otherwise its failed anyway if assert.Len(ttt, got.Authorizations, len(tt.want.Authorizations)) { for i := range tt.want.Authorizations { + // set as project grant id is generated + if grant := got.Authorizations[i].ProjectGrantId; grant != nil { + tt.want.Authorizations[i].ProjectGrantId = grant + } assert.EqualExportedValues(ttt, tt.want.Authorizations[i], got.Authorizations[i]) } } diff --git a/internal/api/grpc/internal_permission/v2beta/administrator.go b/internal/api/grpc/internal_permission/v2beta/administrator.go index 86ee7d945..21f3ef1c6 100644 --- a/internal/api/grpc/internal_permission/v2beta/administrator.go +++ b/internal/api/grpc/internal_permission/v2beta/administrator.go @@ -85,10 +85,10 @@ func createAdministratorProjectToCommand(req *internal_permission.ResourceType_P func createAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.AddProjectGrantMember { return &command.AddProjectGrantMember{ - GrantID: req.ProjectGrant.ProjectGrantId, - ProjectID: req.ProjectGrant.ProjectId, - UserID: userID, - Roles: roles, + OrganizationID: req.ProjectGrant.OrganizationId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, } } @@ -165,10 +165,10 @@ func updateAdministratorProjectToCommand(req *internal_permission.ResourceType_P func updateAdministratorProjectGrantToCommand(req *internal_permission.ResourceType_ProjectGrant_, userID string, roles []string) *command.ChangeProjectGrantMember { return &command.ChangeProjectGrantMember{ - GrantID: req.ProjectGrant.ProjectGrantId, - ProjectID: req.ProjectGrant.ProjectId, - UserID: userID, - Roles: roles, + OrganizationID: req.ProjectGrant.OrganizationId, + ProjectID: req.ProjectGrant.ProjectId, + UserID: userID, + Roles: roles, } } @@ -203,7 +203,7 @@ func (s *Server) DeleteAdministrator(ctx context.Context, req *connect.Request[i deletionDate = timestamppb.New(member.EventDate) } case *internal_permission.ResourceType_ProjectGrant_: - member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, resource.ProjectGrant.ProjectGrantId) + member, err := s.command.RemoveProjectGrantMember(ctx, resource.ProjectGrant.ProjectId, req.Msg.UserId, "", resource.ProjectGrant.OrganizationId) if err != nil { return nil, err } diff --git a/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go index 3432f5fd3..f5537c12b 100644 --- a/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go +++ b/internal/api/grpc/internal_permission/v2beta/integration_test/administrator_test.go @@ -239,7 +239,7 @@ func TestServer_CreateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: "notexisting", + OrganizationId: "notexisting", }, }, } @@ -263,7 +263,7 @@ func TestServer_CreateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -289,7 +289,7 @@ func TestServer_CreateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -334,6 +334,13 @@ func TestServer_CreateAdministrator_Permission(t *testing.T) { patProjectGrantResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userProjectGrantResp.GetUserId()) projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patProjectGrantResp.Token) + grantedProjectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + userGrantedProjectResp := instance.CreateMachineUser(iamOwnerCtx) + instance.CreateProjectMembership(t, iamOwnerCtx, grantedProjectResp.GetId(), userGrantedProjectResp.GetUserId()) + patGrantedProjectResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userGrantedProjectResp.GetUserId()) + grantedProjectOwnerCtx := integration.WithAuthorizationToken(CTX, patGrantedProjectResp.Token) + instance.CreateProjectGrant(iamOwnerCtx, t, grantedProjectResp.GetId(), instance.DefaultOrg.GetId()) + type want struct { creationDate bool } @@ -508,6 +515,50 @@ func TestServer_CreateAdministrator_Permission(t *testing.T) { }, wantErr: true, }, + { + name: "project grant, org owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: grantedProjectResp.GetId(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + want: want{ + creationDate: true, + }, + }, + { + name: "project grant, project owner, error", + ctx: grantedProjectOwnerCtx, + prepare: func(request *internal_permission.CreateAdministratorRequest) { + userResp := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()) + + request.UserId = userResp.GetId() + }, + req: &internal_permission.CreateAdministratorRequest{ + Resource: &internal_permission.ResourceType{ + Resource: &internal_permission.ResourceType_ProjectGrant_{ + ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ + ProjectId: grantedProjectResp.GetId(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + }, + }, + Roles: []string{"PROJECT_GRANT_OWNER"}, + }, + wantErr: true, + }, { name: "project grant, project grant owner, ok", ctx: projectGrantOwnerCtx, @@ -521,7 +572,7 @@ func TestServer_CreateAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -544,7 +595,7 @@ func TestServer_CreateAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -869,7 +920,7 @@ func TestServer_UpdateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: "notexisting", + OrganizationId: "notexisting", }, }, } @@ -894,7 +945,7 @@ func TestServer_UpdateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -922,7 +973,7 @@ func TestServer_UpdateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -949,7 +1000,7 @@ func TestServer_UpdateAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -1192,7 +1243,7 @@ func TestServer_UpdateAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -1215,7 +1266,7 @@ func TestServer_UpdateAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -1236,7 +1287,7 @@ func TestServer_UpdateAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -1487,7 +1538,7 @@ func TestServer_DeleteAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: "notexisting", + OrganizationId: "notexisting", }, }, } @@ -1512,7 +1563,7 @@ func TestServer_DeleteAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -1538,7 +1589,7 @@ func TestServer_DeleteAdministrator(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, } @@ -1766,7 +1817,7 @@ func TestServer_DeleteAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -1788,7 +1839,7 @@ func TestServer_DeleteAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, @@ -1808,7 +1859,7 @@ func TestServer_DeleteAdministrator_Permission(t *testing.T) { Resource: &internal_permission.ResourceType_ProjectGrant_{ ProjectGrant: &internal_permission.ResourceType_ProjectGrant{ ProjectId: projectResp.GetId(), - ProjectGrantId: orgResp.GetOrganizationId(), + OrganizationId: orgResp.GetOrganizationId(), }, }, }, diff --git a/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go b/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go index 687d4de2f..8553fcc92 100644 --- a/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/internal_permission/v2beta/integration_test/query_test.go @@ -535,6 +535,10 @@ func TestServer_ListAdministrators(t *testing.T) { // always first check length, otherwise its failed anyway if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { for i := range tt.want.Administrators { + // need to set the project grant ID as it is generated + if grant := got.Administrators[i].GetProjectGrant(); grant != nil { + tt.want.Administrators[i].GetProjectGrant().Id = grant.Id + } assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) } } @@ -631,7 +635,8 @@ func createProjectGrantAdministrator(ctx context.Context, instance *integration. }, Resource: &internal_permission.Administrator_ProjectGrant{ ProjectGrant: &internal_permission.ProjectGrant{ - Id: grantedOrgID, + // left empty as generated + Id: "", ProjectId: projectID, ProjectName: projectName, OrganizationId: orgID, @@ -1162,6 +1167,10 @@ func TestServer_ListAdministrators_PermissionV2(t *testing.T) { // always first check length, otherwise its failed anyway if assert.Len(ttt, got.Administrators, len(tt.want.Administrators)) { for i := range tt.want.Administrators { + // set as project grant id is generated + if grant := got.Administrators[i].GetProjectGrant(); grant != nil { + tt.want.Administrators[i].GetProjectGrant().Id = grant.Id + } assert.EqualExportedValues(ttt, tt.want.Administrators[i], got.Administrators[i]) } } diff --git a/internal/api/grpc/management/project_grant.go b/internal/api/grpc/management/project_grant.go index 26f51f685..615991fc8 100644 --- a/internal/api/grpc/management/project_grant.go +++ b/internal/api/grpc/management/project_grant.go @@ -204,7 +204,7 @@ func (s *Server) UpdateProjectGrantMember(ctx context.Context, req *mgmt_pb.Upda } func (s *Server) RemoveProjectGrantMember(ctx context.Context, req *mgmt_pb.RemoveProjectGrantMemberRequest) (*mgmt_pb.RemoveProjectGrantMemberResponse, error) { - details, err := s.command.RemoveProjectGrantMember(ctx, req.ProjectId, req.UserId, req.GrantId) + details, err := s.command.RemoveProjectGrantMember(ctx, req.ProjectId, req.UserId, req.GrantId, "") if err != nil { return nil, err } diff --git a/internal/api/grpc/management/project_grant_converter.go b/internal/api/grpc/management/project_grant_converter.go index 0523eed13..58e359c36 100644 --- a/internal/api/grpc/management/project_grant_converter.go +++ b/internal/api/grpc/management/project_grant_converter.go @@ -147,19 +147,19 @@ func ListProjectGrantMembersRequestToModel(ctx context.Context, req *mgmt_pb.Lis func AddProjectGrantMemberRequestToCommand(req *mgmt_pb.AddProjectGrantMemberRequest, orgID string) *command.AddProjectGrantMember { return &command.AddProjectGrantMember{ - ResourceOwner: orgID, - ProjectID: req.ProjectId, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, + ResourceOwner: orgID, + ProjectID: req.ProjectId, + ProjectGrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } func UpdateProjectGrantMemberRequestToCommand(req *mgmt_pb.UpdateProjectGrantMemberRequest) *command.ChangeProjectGrantMember { return &command.ChangeProjectGrantMember{ - ProjectID: req.ProjectId, - GrantID: req.GrantId, - UserID: req.UserId, - Roles: req.Roles, + ProjectID: req.ProjectId, + ProjectGrantID: req.GrantId, + UserID: req.UserId, + Roles: req.Roles, } } diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index f4f99cd78..5cba4fc37 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -426,7 +426,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, Instance.DefaultOrg.GetId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -564,7 +564,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -940,26 +940,26 @@ func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, } func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateProjectUserGrant(t, ctx, orgID, projectID, userID) + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) } -func createProjectGrantUserGrant(ctx context.Context, t *testing.T, orgID, projectID, projectGrantID, userID string) { - resp := Instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, projectGrantID, userID) +func createProjectGrantUserGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID, userID string) { + resp := Instance.CreateAuthorizationProjectGrant(t, ctx, projectID, grantedOrgID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, grantedOrgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) diff --git a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go index 1ad63cea1..0f248513d 100644 --- a/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2beta/integration_test/oidc_test.go @@ -427,7 +427,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, @@ -564,7 +564,7 @@ func TestServer_CreateCallback_Permission(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndAuthRequestForCallback(ctx, t, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, user.GetUserId()) }, want: &oidc_pb.CreateCallbackResponse{ @@ -693,26 +693,26 @@ func createOIDCApplication(ctx context.Context, t *testing.T, projectRoleCheck, } func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateProjectUserGrant(t, ctx, orgID, projectID, userID) + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) } -func createProjectGrantUserGrant(ctx context.Context, t *testing.T, orgID, projectID, projectGrantID, userID string) { - resp := Instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, projectGrantID, userID) +func createProjectGrantUserGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID, userID string) { + resp := Instance.CreateAuthorizationProjectGrant(t, ctx, projectID, grantedOrgID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, grantedOrgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) diff --git a/internal/api/grpc/project/v2beta/project_grant.go b/internal/api/grpc/project/v2beta/project_grant.go index c2ccef175..802c68b99 100644 --- a/internal/api/grpc/project/v2beta/project_grant.go +++ b/internal/api/grpc/project/v2beta/project_grant.go @@ -32,7 +32,6 @@ func projectGrantCreateToCommand(req *project_pb.CreateProjectGrantRequest) *com ObjectRoot: models.ObjectRoot{ AggregateID: req.ProjectId, }, - GrantID: req.GrantedOrganizationId, GrantedOrgID: req.GrantedOrganizationId, RoleKeys: req.RoleKeys, } @@ -113,7 +112,7 @@ func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, gran if err != nil { return nil, err } - grantQuery, err := query.NewUserGrantGrantIDSearchQuery(grantedOrganizationID) + grantQuery, err := query.NewUserGrantWithGrantedQuery(grantedOrganizationID) if err != nil { return nil, err } diff --git a/internal/api/grpc/saml/v2/integration_test/saml_test.go b/internal/api/grpc/saml/v2/integration_test/saml_test.go index 2c0ccd7e1..d5c3e3d38 100644 --- a/internal/api/grpc/saml/v2/integration_test/saml_test.go +++ b/internal/api/grpc/saml/v2/integration_test/saml_test.go @@ -416,7 +416,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -550,7 +550,7 @@ func TestServer_CreateResponse_Permission(t *testing.T) { orgResp := Instance.CreateOrganization(ctx, integration.OrganizationName(), integration.Email()) Instance.CreateProjectGrant(ctx, t, projectID, orgResp.GetOrganizationId()) user := Instance.CreateHumanUserVerified(ctx, orgResp.GetOrganizationId(), integration.Email(), integration.Phone()) - createProjectGrantUserGrant(ctx, t, orgResp.GetOrganizationId(), projectID, orgResp.GetOrganizationId(), user.GetUserId()) + createProjectGrantUserGrant(ctx, t, projectID, orgResp.GetOrganizationId(), user.GetUserId()) return createSessionAndSmlRequestForCallback(ctx, t, sp, Instance.Users[integration.UserTypeLogin].ID, acsRedirect, user.GetUserId(), saml.HTTPRedirectBinding) }, @@ -694,26 +694,26 @@ func createSAMLApplication(ctx context.Context, t *testing.T, idpMetadata *saml. } func createProjectUserGrant(ctx context.Context, t *testing.T, orgID, projectID, userID string) { - resp := Instance.CreateProjectUserGrant(t, ctx, orgID, projectID, userID) + resp := Instance.CreateAuthorizationProject(t, ctx, projectID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) } -func createProjectGrantUserGrant(ctx context.Context, t *testing.T, orgID, projectID, projectGrantID, userID string) { - resp := Instance.CreateProjectGrantUserGrant(ctx, orgID, projectID, projectGrantID, userID) +func createProjectGrantUserGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID, userID string) { + resp := Instance.CreateAuthorizationProjectGrant(t, ctx, projectID, grantedOrgID, userID) retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) require.EventuallyWithT(t, func(collect *assert.CollectT) { - _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, orgID), &mgmt.GetUserGrantByIDRequest{ + _, err := Instance.Client.Mgmt.GetUserGrantByID(integration.SetOrgID(ctx, grantedOrgID), &mgmt.GetUserGrantByIDRequest{ UserId: userID, - GrantId: resp.GetUserGrantId(), + GrantId: resp.GetId(), }) assert.NoError(collect, err) }, retryDuration, tick) diff --git a/internal/command/permission_checks.go b/internal/command/permission_checks.go index 021107e6a..65ab3fa01 100644 --- a/internal/command/permission_checks.go +++ b/internal/command/permission_checks.go @@ -133,19 +133,11 @@ func (c *Commands) checkPermissionDeleteProjectMember(ctx context.Context, resou } func (c *Commands) checkPermissionUpdateProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { - // TODO: add permission check for project grant owners - //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(resourceOwner, projectGrantID); err != nil { return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberWrite, project.AggregateType)(grantedOrgID, projectGrantID) - //} - //return nil } func (c *Commands) checkPermissionDeleteProjectGrantMember(ctx context.Context, grantedOrgID, projectGrantID string) (err error) { - // TODO: add permission check for project grant owners - //if err := c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(resourceOwner, projectGrantID); err != nil { return c.newPermissionCheck(ctx, domain.PermissionProjectGrantMemberDelete, project.AggregateType)(grantedOrgID, projectGrantID) - //} - //return nil } func (c *Commands) newUserGrantPermissionCheck(ctx context.Context, permission string) UserGrantPermissionCheck { diff --git a/internal/command/project_grant.go b/internal/command/project_grant.go index 2a0e83a6e..2a1611e10 100644 --- a/internal/command/project_grant.go +++ b/internal/command/project_grant.go @@ -234,15 +234,15 @@ func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantI return writeModelToObjectDetails(&existingGrant.WriteModel), nil } -func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, error) { +func (c *Commands) checkProjectGrantExists(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (string, string, string, error) { existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner) if err != nil { - return "", "", err + return "", "", "", err } if !existingGrant.State.Exists() { - return "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") + return "", "", "", zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound") } - return existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil + return existingGrant.FoundGrantID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, nil } func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) { diff --git a/internal/command/project_grant_member.go b/internal/command/project_grant_member.go index 611b897c6..b7f2cf02f 100644 --- a/internal/command/project_grant_member.go +++ b/internal/command/project_grant_member.go @@ -13,15 +13,16 @@ import ( ) type AddProjectGrantMember struct { - ResourceOwner string - UserID string - GrantID string - ProjectID string - Roles []string + ResourceOwner string + UserID string + OrganizationID string + ProjectGrantID string + ProjectID string + Roles []string } func (i *AddProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { - if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + if i.ProjectID == "" || (i.OrganizationID == "" && i.ProjectGrantID == "") || i.UserID == "" || len(i.Roles) == 0 { return zerrors.ThrowInvalidArgument(nil, "PROJECT-8fi7G", "Errors.Project.Grant.Member.Invalid") } if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { @@ -41,14 +42,18 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProject if err != nil { return nil, err } - grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.GrantID, "", member.ProjectID, "") + projectGrantID, grantedOrgID, projectGrantResourceOwner, err := c.checkProjectGrantExists(ctx, member.ProjectGrantID, member.OrganizationID, member.ProjectID, "") if err != nil { return nil, err } if member.ResourceOwner == "" { member.ResourceOwner = projectGrantResourceOwner } - addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, member.ResourceOwner) + // if projectGrantID is not provided, organizationID has to be provided, but we still need the projectGrantID which we query + if member.ProjectGrantID == "" { + member.ProjectGrantID = projectGrantID + } + addedMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ProjectGrantID, member.ResourceOwner) if err != nil { return nil, err } @@ -69,7 +74,7 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProject project.NewProjectGrantMemberAddedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &addedMember.WriteModel), member.UserID, - member.GrantID, + member.ProjectGrantID, member.Roles..., )) if err != nil { @@ -84,14 +89,15 @@ func (c *Commands) AddProjectGrantMember(ctx context.Context, member *AddProject } type ChangeProjectGrantMember struct { - UserID string - GrantID string - ProjectID string - Roles []string + UserID string + ProjectGrantID string + OrganizationID string + ProjectID string + Roles []string } func (i *ChangeProjectGrantMember) IsValid(zitadelRoles []authz.RoleMapping) error { - if i.ProjectID == "" || i.GrantID == "" || i.UserID == "" || len(i.Roles) == 0 { + if i.ProjectID == "" || (i.ProjectGrantID == "" && i.OrganizationID == "") || i.UserID == "" || len(i.Roles) == 0 { return zerrors.ThrowInvalidArgument(nil, "PROJECT-109fs", "Errors.Project.Grant.Member.Invalid") } if len(domain.CheckForInvalidRoles(i.Roles, domain.ProjectGrantRolePrefix, zitadelRoles)) > 0 { @@ -105,11 +111,14 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeP if err := member.IsValid(c.zitadelRoles); err != nil { return nil, err } - existingGrant, err := c.projectGrantWriteModelByID(ctx, member.GrantID, "", member.ProjectID, "") + existingGrant, err := c.projectGrantWriteModelByID(ctx, member.ProjectGrantID, member.OrganizationID, member.ProjectID, "") if err != nil { return nil, err } - existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.GrantID, existingGrant.ResourceOwner) + if member.ProjectGrantID == "" { + member.ProjectGrantID = existingGrant.GrantID + } + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, member.ProjectID, member.UserID, member.ProjectGrantID, existingGrant.ResourceOwner) if err != nil { return nil, err } @@ -129,7 +138,7 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeP project.NewProjectGrantMemberChangedEvent(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), member.UserID, - member.GrantID, + member.ProjectGrantID, member.Roles..., )) if err != nil { @@ -143,15 +152,16 @@ func (c *Commands) ChangeProjectGrantMember(ctx context.Context, member *ChangeP return writeModelToObjectDetails(&existingMember.WriteModel), nil } -func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID string) (*domain.ObjectDetails, error) { - if projectID == "" || userID == "" || grantID == "" { +func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, userID, grantID, organizationID string) (*domain.ObjectDetails, error) { + if projectID == "" || userID == "" || (grantID == "" && organizationID == "") { return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-66mHd", "Errors.Project.Member.Invalid") } - existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, "") + existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, organizationID, projectID, "") if err != nil { return nil, err } - existingMember, err := c.projectGrantMemberWriteModelByID(ctx, projectID, userID, grantID, existingGrant.ResourceOwner) + + existingMember, err := c.projectGrantMemberWriteModelByID(ctx, existingGrant.AggregateID, userID, existingGrant.FoundGrantID, existingGrant.ResourceOwner) if err != nil { return nil, err } @@ -165,7 +175,7 @@ func (c *Commands) RemoveProjectGrantMember(ctx context.Context, projectID, user removeEvent := c.removeProjectGrantMember(ctx, ProjectAggregateFromWriteModelWithCTX(ctx, &existingMember.WriteModel), userID, - grantID, + existingGrant.FoundGrantID, false, ) pushedEvents, err := c.eventstore.Push(ctx, removeEvent) diff --git a/internal/command/project_grant_member_test.go b/internal/command/project_grant_member_test.go index 3cd8ecdd8..f6a48a5dc 100644 --- a/internal/command/project_grant_member_test.go +++ b/internal/command/project_grant_member_test.go @@ -57,10 +57,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, args: args{ member: &AddProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -82,10 +82,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, args: args{ member: &AddProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -142,10 +142,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, args: args{ member: &AddProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -202,10 +202,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, args: args{ member: &AddProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -257,10 +257,10 @@ func TestCommandSide_AddProjectGrantMember(t *testing.T) { }, args: args{ member: &AddProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -331,10 +331,10 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, args: args{ member: &ChangeProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_OWNER"}, }, }, res: res{ @@ -366,10 +366,10 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, args: args{ member: &ChangeProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -410,10 +410,10 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, args: args{ member: &ChangeProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER"}, }, }, res: res{ @@ -467,10 +467,10 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, args: args{ member: &ChangeProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, }, }, res: res{ @@ -516,10 +516,10 @@ func TestCommandSide_ChangeProjectGrantMember(t *testing.T) { }, args: args{ member: &ChangeProjectGrantMember{ - ProjectID: "project1", - GrantID: "projectgrant1", - UserID: "user1", - Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, + ProjectID: "project1", + ProjectGrantID: "projectgrant1", + UserID: "user1", + Roles: []string{"PROJECT_GRANT_OWNER", "PROJECT_GRANT_VIEWER"}, }, }, res: res{ @@ -554,10 +554,11 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { checkPermission domain.PermissionCheck } type args struct { - ctx context.Context - projectID string - grantID string - userID string + ctx context.Context + projectID string + grantID string + organizationID string + userID string } type res struct { want *domain.ObjectDetails @@ -693,6 +694,52 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { }, }, }, + { + name: "member remove, organizationID, ok", + fields: fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "org2", + []string{"rol1", "role2"}, + ), + ), + ), + expectFilter( + eventFromEventPusher( + project.NewProjectGrantMemberAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + []string{"PROJECT_OWNER"}..., + ), + ), + ), + expectPush( + project.NewProjectGrantMemberRemovedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "user1", + "projectgrant1", + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx: context.Background(), + projectID: "project1", + userID: "user1", + organizationID: "org2", + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "member remove, no permission", fields: fields{ @@ -737,7 +784,7 @@ func TestCommandSide_RemoveProjectGrantMember(t *testing.T) { eventstore: tt.fields.eventstore(t), checkPermission: tt.fields.checkPermission, } - got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID) + got, err := r.RemoveProjectGrantMember(tt.args.ctx, tt.args.projectID, tt.args.userID, tt.args.grantID, tt.args.organizationID) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/project_grant_model.go b/internal/command/project_grant_model.go index 15950d4f3..9ef466c0d 100644 --- a/internal/command/project_grant_model.go +++ b/internal/command/project_grant_model.go @@ -1,6 +1,8 @@ package command import ( + "slices" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" @@ -14,6 +16,8 @@ type ProjectGrantWriteModel struct { GrantedOrgID string RoleKeys []string State domain.ProjectGrantState + + FoundGrantID string } func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner string) *ProjectGrantWriteModel { @@ -22,6 +26,7 @@ func NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner s AggregateID: projectID, ResourceOwner: resourceOwner, }, + // Always either the grantID or the grantedOrgID is provided GrantID: grantID, GrantedOrgID: grantedOrgID, } @@ -31,28 +36,29 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { for _, event := range events { switch e := event.(type) { case *project.GrantAddedEvent: - if (wm.GrantID != "" && e.GrantID == wm.GrantID) || - (wm.GrantedOrgID != "" && e.GrantedOrgID == wm.GrantedOrgID) { + if projectGrantExists(wm.GrantID, wm.GrantedOrgID, e.GrantID, e.GrantedOrgID) { + wm.FoundGrantID = e.GrantID wm.WriteModel.AppendEvents(e) } case *project.GrantChangedEvent: - if wm.GrantID != "" && e.GrantID == wm.GrantID { + if projectGrantEqual(wm.FoundGrantID, e.GrantID) { wm.WriteModel.AppendEvents(e) } case *project.GrantCascadeChangedEvent: - if wm.GrantID != "" && e.GrantID == wm.GrantID { + if projectGrantEqual(wm.FoundGrantID, e.GrantID) { wm.WriteModel.AppendEvents(e) } case *project.GrantDeactivateEvent: - if wm.GrantID != "" && e.GrantID == wm.GrantID { + if projectGrantEqual(wm.FoundGrantID, e.GrantID) { wm.WriteModel.AppendEvents(e) } case *project.GrantReactivatedEvent: - if wm.GrantID != "" && e.GrantID == wm.GrantID { + if projectGrantEqual(wm.FoundGrantID, e.GrantID) { wm.WriteModel.AppendEvents(e) } case *project.GrantRemovedEvent: - if wm.GrantID != "" && e.GrantID == wm.GrantID { + if projectGrantEqual(wm.FoundGrantID, e.GrantID) { + wm.FoundGrantID = "" wm.WriteModel.AppendEvents(e) } case *project.ProjectRemovedEvent: @@ -61,6 +67,17 @@ func (wm *ProjectGrantWriteModel) AppendEvents(events ...eventstore.Event) { } } +func projectGrantExists(requiredGrantID, requiredGrantedOrgID, grantID, grantedOrgID string) bool { + // either grantID or grantedOrgID is provided and equal + return projectGrantEqual(requiredGrantID, grantID) || + (requiredGrantedOrgID != "" && grantedOrgID == requiredGrantedOrgID) +} + +func projectGrantEqual(requiredGrantID, grantID string) bool { + // grantID is provided and equal + return requiredGrantID != "" && grantID == requiredGrantID +} + func (wm *ProjectGrantWriteModel) Reduce() error { for _, event := range wm.Events { switch e := event.(type) { @@ -93,7 +110,8 @@ func (wm *ProjectGrantWriteModel) Reduce() error { } func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { - query := eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + ResourceOwner(wm.ResourceOwner). AddQuery(). AggregateTypes(project.AggregateType). AggregateIDs(wm.AggregateID). @@ -106,11 +124,6 @@ func (wm *ProjectGrantWriteModel) Query() *eventstore.SearchQueryBuilder { project.GrantRemovedType, project.ProjectRemovedType). Builder() - - if wm.ResourceOwner != "" { - query.ResourceOwner(wm.ResourceOwner) - } - return query } type ProjectGrantPreConditionReadModel struct { @@ -159,14 +172,9 @@ func (wm *ProjectGrantPreConditionReadModel) Reduce() error { if e.Aggregate().ResourceOwner != wm.ProjectResourceOwner { continue } - for i, key := range wm.ExistingRoleKeys { - if key == e.Key { - copy(wm.ExistingRoleKeys[i:], wm.ExistingRoleKeys[i+1:]) - wm.ExistingRoleKeys[len(wm.ExistingRoleKeys)-1] = "" - wm.ExistingRoleKeys = wm.ExistingRoleKeys[:len(wm.ExistingRoleKeys)-1] - continue - } - } + wm.ExistingRoleKeys = slices.DeleteFunc(wm.ExistingRoleKeys, func(key string) bool { + return key == e.Key + }) case *org.OrgAddedEvent: wm.GrantedOrgExists = true case *org.OrgRemovedEvent: diff --git a/internal/command/project_grant_test.go b/internal/command/project_grant_test.go index 7a3bb98e7..518249f0a 100644 --- a/internal/command/project_grant_test.go +++ b/internal/command/project_grant_test.go @@ -352,6 +352,61 @@ func TestCommandSide_AddProjectGrant(t *testing.T) { }, }, }, + { + name: "grant for project, only granted org, ok", + fields: fields{ + eventstore: expectEventstore( + 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", + "", + ), + ), + ), + expectPush( + project.NewGrantAddedEvent(context.Background(), + &project.NewAggregate("project1", "org1").Aggregate, + "projectgrant1", + "grantedorg1", + []string{"key1"}, + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "projectgrant1"), + }, + args: args{ + ctx: context.Background(), + projectGrant: &AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "project1", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -790,6 +845,75 @@ func TestCommandSide_ChangeProjectGrant(t *testing.T) { }, }, }, + { + name: "projectgrant only added roles,no resourceowner, 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", + }, + GrantedOrgID: "grantedorg1", + RoleKeys: []string{"key1", "key2"}, + }, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + }, { name: "projectgrant remove roles, usergrant not found, ok", fields: fields{ diff --git a/internal/command/user_grant_model.go b/internal/command/user_grant_model.go index b9d9d27a2..cc1062bf7 100644 --- a/internal/command/user_grant_model.go +++ b/internal/command/user_grant_model.go @@ -171,7 +171,8 @@ func projectExistsOnOrganization(requiredOrganization, projectResourceOwner stri func projectGrantExistsOnOrganization(requiredGrantID, requiredOrganization, projectGrantID, grantedOrganization string) bool { // Depending on the API, a request can either require a project grant (ID) and/or an organization (ID), // where the project must be granted to. - return (requiredGrantID == "" || requiredGrantID == projectGrantID) && + return (requiredGrantID != "" || requiredOrganization != "") && + (requiredGrantID == "" || requiredGrantID == projectGrantID) && (requiredOrganization == "" || requiredOrganization == grantedOrganization) } diff --git a/internal/integration/client.go b/internal/integration/client.go index 78f07425c..fae837c8e 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -959,6 +959,25 @@ func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, proje return resp } +func (i *Instance) CreateAuthorizationProject(t *testing.T, ctx context.Context, projectID, userID string) *authorization.CreateAuthorizationResponse { + resp, err := i.Client.AuthorizationV2Beta.CreateAuthorization(ctx, &authorization.CreateAuthorizationRequest{ + UserId: userID, + ProjectId: projectID, + }) + require.NoError(t, err) + return resp +} + +func (i *Instance) CreateAuthorizationProjectGrant(t *testing.T, ctx context.Context, projectID, orgID, userID string) *authorization.CreateAuthorizationResponse { + resp, err := i.Client.AuthorizationV2Beta.CreateAuthorization(ctx, &authorization.CreateAuthorizationRequest{ + UserId: userID, + ProjectId: projectID, + OrganizationId: gu.Ptr(orgID), + }) + require.NoError(t, err) + return resp +} + func (i *Instance) CreateProjectUserGrant(t *testing.T, ctx context.Context, orgID, projectID, userID string) *mgmt.AddUserGrantResponse { //nolint:staticcheck resp, err := i.Client.Mgmt.AddUserGrant(SetOrgID(ctx, orgID), &mgmt.AddUserGrantRequest{ @@ -1042,12 +1061,12 @@ func (i *Instance) DeleteProjectMembership(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) *internal_permission_v2beta.CreateAdministratorResponse { +func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Context, projectID, orgID, userID string) *internal_permission_v2beta.CreateAdministratorResponse { resp, err := i.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ Resource: &internal_permission_v2beta.ResourceType{ Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ ProjectId: projectID, - ProjectGrantId: grantID, + OrganizationId: orgID, }}, }, UserId: userID, @@ -1057,12 +1076,12 @@ func (i *Instance) CreateProjectGrantMembership(t *testing.T, ctx context.Contex return resp } -func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Context, projectID, grantID, userID string) { +func (i *Instance) DeleteProjectGrantMembership(t *testing.T, ctx context.Context, projectID, orgID, userID string) { _, err := i.Client.InternalPermissionv2Beta.DeleteAdministrator(ctx, &internal_permission_v2beta.DeleteAdministratorRequest{ Resource: &internal_permission_v2beta.ResourceType{ Resource: &internal_permission_v2beta.ResourceType_ProjectGrant_{ProjectGrant: &internal_permission_v2beta.ResourceType_ProjectGrant{ ProjectId: projectID, - ProjectGrantId: grantID, + OrganizationId: orgID, }}, }, UserId: userID, diff --git a/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto index 3a27b89a4..977d95fed 100644 --- a/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto +++ b/proto/zitadel/internal_permission/v2beta/internal_permission_service.proto @@ -313,10 +313,12 @@ message CreateAdministratorRequest { message ResourceType { message ProjectGrant { + reserved "project_grant_id"; + reserved 2; // ProjectID is required to grant administrator privileges for a specific project. string project_id = 1; - // ProjectGrantID is required to grant administrator privileges for a specific project grant. - string project_grant_id = 2; + // OrganizationID is required to grant administrator privileges for a specific project grant. + string organization_id = 3; } // Resource is the type of the resource the administrator roles should be granted for. @@ -381,4 +383,4 @@ message DeleteAdministratorResponse { // Note that the deletion date is only guaranteed to be set if the deletion was successful during the request. // In case the deletion occurred in a previous request, the deletion date might not be set. google.protobuf.Timestamp deletion_date = 1; -} \ No newline at end of file +}