diff --git a/cmd/start/start.go b/cmd/start/start.go index d886d31995e..034544e6082 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -54,6 +54,7 @@ import ( oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" + project_v2 "github.com/zitadel/zitadel/internal/api/grpc/project/v2" project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta" "github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events" user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha" @@ -535,6 +536,9 @@ func startAPIs( if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } + if err := apis.RegisterService(ctx, project_v2.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { + return nil, err + } if err := apis.RegisterService(ctx, internal_permission_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil { return nil, err } diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 61b87b648d4..79ae10fb1fe 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -391,9 +391,9 @@ module.exports = { categoryLinkSource: "auto", }, }, - project_v2beta: { + project_v2: { specPath: - ".artifacts/openapi3/zitadel/project/v2beta/project_service.openapi.yaml", + ".artifacts/openapi3/zitadel/project/v2/project_service.openapi.yaml", outputDir: "docs/apis/resources/project_service_v2", sidebarOptions: { groupPathsBy: "tag", diff --git a/docs/sidebars.js b/docs/sidebars.js index 6b39fb62290..c53fa980fc0 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -843,15 +843,13 @@ module.exports = { }, { type: "category", - label: "Project (Beta)", + label: "Project", link: { type: "generated-index", - title: "Project Service API (Beta)", + title: "Project Service API", slug: "/apis/resources/project_service_v2", description: - "This API is intended to manage projects and subresources for ZITADEL. \n" + - "\n" + - "This service is in beta state. It can AND will continue breaking until a stable version is released.", + "This API is intended to manage projects and subresources for ZITADEL." }, items: sidebar_api_project_service_v2, }, diff --git a/internal/api/grpc/project/v2/integration_test/project_grant_test.go b/internal/api/grpc/project/v2/integration_test/project_grant_test.go new file mode 100644 index 00000000000..25df8cb46bb --- /dev/null +++ b/internal/api/grpc/project/v2/integration_test/project_grant_test.go @@ -0,0 +1,1350 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func TestServer_CreateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "empty projectID", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty granted organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectGrantRequest{ + ProjectId: "something", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "project not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + request.ProjectId = "something" + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "org not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = "something" + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "same organization, error", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + request.GrantedOrganizationId = orgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "with roles, not existing", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "with roles, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + request.RoleKeys = roles + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_CreateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.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.GetProjectId(), userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(request *project.CreateProjectGrantRequest) + req *project.CreateProjectGrantRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "project owner, other project", + ctx: projectOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.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.GetProjectId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.CreateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + grantedOrgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + request.ProjectId = projectResp.GetId() + request.GrantedOrganizationId = grantedOrgResp.GetOrganizationId() + }, + req: &project.CreateProjectGrantRequest{}, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.CreateProjectGrant(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectGrantResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertCreateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.CreateProjectGrantResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"notexisting"}, + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change roles, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change roles, not existing", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + request.RoleKeys = []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.ProjectV2.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), 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 + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "project grant owner, no permission", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + 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) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{ + RoleKeys: []string{"nopermission"}, + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectGrantRequest) { + roles := []string{integration.RoleKey(), integration.RoleKey(), integration.RoleKey()} + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + for _, role := range roles { + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), role, role, "") + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId(), roles...) + request.RoleKeys = roles + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + if tt.prepare != nil { + tt.prepare(tt.args.req) + } + + got, err := instance.Client.ProjectV2.UpdateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty project id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "", + }, + wantErr: true, + }, + { + name: "empty grantedorg id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notempty", + GrantedOrganizationId: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectGrantRequest{ + ProjectId: "notexisting", + GrantedOrganizationId: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete deactivated", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeleteProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ProjectV2.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) + req *project.DeleteProjectGrantRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return time.Time{}, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, ok", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectGrantRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + return creationDate, time.Time{} + }, + req: &project.DeleteProjectGrantRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ProjectV2.DeleteProjectGrant(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectGrantResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectGrantResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectGrantResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.DeactivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.DeactivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.DeactivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProjectGrant(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.ActivateProjectGrantRequest) { + request.ProjectId = "notexisting" + request.GrantedOrganizationId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProjectGrant_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectGrantRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectGrantRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.ActivateProjectGrantRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.GrantedOrganizationId = grantedOrg.GetOrganizationId() + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + instance.DeactivateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectGrantRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.ActivateProjectGrant(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectGrantResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectGrantResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectGrantResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2/integration_test/project_role_test.go b/internal/api/grpc/project/v2/integration_test/project_role_test.go new file mode 100644 index 00000000000..d3de04e17f2 --- /dev/null +++ b/internal/api/grpc/project/v2/integration_test/project_role_test.go @@ -0,0 +1,816 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/management" + "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func TestServer_AddProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.ProjectName(), integration.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + alreadyExistingProjectRoleName := integration.RoleDisplayName() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + tests := []struct { + name string + ctx context.Context + prepare func(t *testing.T, request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + }{ + { + name: "empty key", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: "", + DisplayName: integration.ProjectName(), + }, + wantErr: true, + }, + { + name: "empty displayname", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + request.ProjectId = alreadyExistingProject.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: alreadyExistingProjectRoleName, + DisplayName: alreadyExistingProjectRoleName, + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + want: want{ + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(t, tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func TestServer_AddProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + alreadyExistingProject := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + alreadyExistingProjectRoleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, alreadyExistingProject.GetId(), alreadyExistingProjectRoleName, alreadyExistingProjectRoleName, "") + + type want struct { + creationDate bool + } + + type test struct { + name string + ctx context.Context + prepare func(t *testing.T, request *project.AddProjectRoleRequest) + req *project.AddProjectRoleRequest + want + wantErr bool + } + tests := []*test{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + want: want{ + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.AddProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + want: want{ + creationDate: true, + }, + }, + func() *test { + out := test{ + name: "add project role as a added project admin, ok", + req: &project.AddProjectRoleRequest{ + RoleKey: integration.RoleKey(), + DisplayName: integration.RoleDisplayName(), + }, + want: want{ + creationDate: true, + }, + } + + out.prepare = func(t *testing.T, request *project.AddProjectRoleRequest) { + // create project + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.Id, integration.ProjectName(), false, false) + // create user + userID := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()).GetId() + loginCTX := instance.WithAuthorizationToken(iamOwnerCtx, integration.UserTypeLogin) + instance.RegisterUserPasskey(iamOwnerCtx, userID) + _, token, _, _ := instance.CreateVerifiedWebAuthNSession(t, loginCTX, userID) + // assign user as project admin + _, err := instance.Client.Mgmt.AddProjectMember(iamOwnerCtx, &management.AddProjectMemberRequest{ + ProjectId: projectResp.GetId(), + UserId: userID, + Roles: []string{"PROJECT_OWNER_GLOBAL"}, + }) + assert.NoError(t, err) + + // set context + out.ctx = integration.WithAuthorizationToken(context.Background(), token) + + request.ProjectId = projectResp.GetId() + } + + return &out + }(), + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.prepare != nil { + tt.prepare(t, tt.req) + } + + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.AddProjectRole(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertAddProjectRoleResponse(t, creationDate, changeDate, tt.want.creationDate, got) + }) + } +} + +func assertAddProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, actualResp *project.AddProjectRoleResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } +} + +func TestServer_UpdateProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(t *testing.T, request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + request.RoleKey = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + request.DisplayName = gu.Ptr(roleName) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change display name, ok", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(integration.RoleKey()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(integration.RoleKey()), + Group: gu.Ptr(integration.RoleKey()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(t, tt.args.req) + + got, err := instance.Client.ProjectV2.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRoleRequest + } + type want struct { + change bool + changeDate bool + } + type test struct { + name string + prepare func(t *testing.T, request *project.UpdateProjectRoleRequest) + args args + want want + wantErr bool + } + tests := []*test{ + { + name: "unauthenicated", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "no permission", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr("changed"), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(integration.RoleKey()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(t *testing.T, request *project.UpdateProjectRoleRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(integration.RoleKey()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + func() *test { + out := test{ + name: "change project role as a added project admin, ok", + args: args{ + req: &project.UpdateProjectRoleRequest{ + DisplayName: gu.Ptr(integration.RoleKey()), + Group: gu.Ptr(integration.RoleKey()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + } + + out.prepare = func(t *testing.T, request *project.UpdateProjectRoleRequest) { + // create project + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.Id, integration.ProjectName(), false, false) + // create user + userID := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()).GetId() + loginCTX := instance.WithAuthorizationToken(iamOwnerCtx, integration.UserTypeLogin) + instance.RegisterUserPasskey(iamOwnerCtx, userID) + _, token, _, _ := instance.CreateVerifiedWebAuthNSession(t, loginCTX, userID) + // assign user as project admin + _, err := instance.Client.Mgmt.AddProjectMember(iamOwnerCtx, &management.AddProjectMemberRequest{ + ProjectId: projectResp.GetId(), + UserId: userID, + Roles: []string{"PROJECT_OWNER_GLOBAL"}, + }) + assert.NoError(t, err) + + // set context + out.args.ctx = integration.WithAuthorizationToken(context.Background(), token) + + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + } + + return &out + }(), + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creationDate := time.Now().UTC() + tt.prepare(t, tt.args.req) + + got, err := instance.Client.ProjectV2.UpdateProjectRole(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectRoleResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectRoleResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectRoleResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProjectRole(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "", + RoleKey: "notexisting", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.RemoveProjectRoleRequest{ + ProjectId: "notexisting", + RoleKey: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName) + return creationDate, time.Now().UTC() + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(t, tt.req) + } + got, err := instance.Client.ProjectV2.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProjectRole_Permission(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type test struct { + name string + ctx context.Context + prepare func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) + req *project.RemoveProjectRoleRequest + wantDeletionDate bool + wantErr bool + } + tests := []*test{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "no permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantErr: true, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + prepare: func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + }, + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + }, + func() *test { + out := test{ + name: "delete project role as a added project admin, ok", + req: &project.RemoveProjectRoleRequest{}, + wantDeletionDate: true, + } + + out.prepare = func(t *testing.T, request *project.RemoveProjectRoleRequest) (time.Time, time.Time) { + // create project + creationDate := time.Now().UTC() + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.Id, integration.ProjectName(), false, false) + // create user + userID := instance.CreateUserTypeHuman(iamOwnerCtx, integration.Email()).GetId() + loginCTX := instance.WithAuthorizationToken(iamOwnerCtx, integration.UserTypeLogin) + instance.RegisterUserPasskey(iamOwnerCtx, userID) + _, token, _, _ := instance.CreateVerifiedWebAuthNSession(t, loginCTX, userID) + // assign user as project admin + _, err := instance.Client.Mgmt.AddProjectMember(iamOwnerCtx, &management.AddProjectMemberRequest{ + ProjectId: projectResp.GetId(), + UserId: userID, + Roles: []string{"PROJECT_OWNER_GLOBAL"}, + }) + assert.NoError(t, err) + + // set context + out.ctx = integration.WithAuthorizationToken(context.Background(), token) + + roleName := integration.RoleKey() + instance.AddProjectRole(iamOwnerCtx, t, projectResp.GetId(), roleName, roleName, "") + request.ProjectId = projectResp.GetId() + request.RoleKey = roleName + return creationDate, time.Time{} + } + + return &out + }(), + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(t, tt.req) + } + got, err := instance.Client.ProjectV2.RemoveProjectRole(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertRemoveProjectRoleResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertRemoveProjectRoleResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.RemoveProjectRoleResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetRemovalDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.RemovalDate) + } +} diff --git a/internal/api/grpc/project/v2/integration_test/project_test.go b/internal/api/grpc/project/v2/integration_test/project_test.go new file mode 100644 index 00000000000..b43effa0150 --- /dev/null +++ b/internal/api/grpc/project/v2/integration_test/project_test.go @@ -0,0 +1,1154 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/integration" + internal_permission_v2beta "github.com/zitadel/zitadel/pkg/grpc/internal_permission/v2beta" + "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func TestServer_CreateProject(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + alreadyExistingProjectName := integration.ProjectName() + instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), alreadyExistingProjectName, false, false) + customID := integration.ID() + + type want struct { + id func(id string) + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "empty name", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: "", + }, + wantErr: true, + }, + { + name: "empty organization", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: "", + }, + wantErr: true, + }, + { + name: "already existing, error", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: alreadyExistingProjectName, + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "empty, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: func(id string) { assert.NotEmpty(t, id) }, + creationDate: true, + }, + }, + { + name: "with custom id, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + ProjectId: gu.Ptr(customID), + }, + want: want{ + id: func(id string) { assert.Equal(t, customID, id) }, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, tt.want.id, got) + }) + } +} + +func TestServer_CreateProject_Permission(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type want struct { + id bool + creationDate bool + } + tests := []struct { + name string + ctx context.Context + req *project.CreateProjectRequest + want + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "missing permission, other organization", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + wantErr: true, + }, + { + name: "with ORG_PROJECT_CREATOR permission, same organization, ok", + ctx: integration.WithAuthorizationToken(CTX, getOrgProjectCreatorToken(t, iamOwnerCtx, orgResp.GetOrganizationId(), orgResp.GetOrganizationId())), + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "with ORG_PROJECT_CREATOR permission, other organization, ok", + ctx: integration.WithAuthorizationToken(CTX, getOrgProjectCreatorToken(t, iamOwnerCtx, orgResp.GetOrganizationId(), instance.DefaultOrg.GetId())), + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "organization owner, ok", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: instance.DefaultOrg.GetId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + { + name: "instance owner, ok", + ctx: iamOwnerCtx, + req: &project.CreateProjectRequest{ + Name: integration.ProjectName(), + OrganizationId: orgResp.GetOrganizationId(), + }, + want: want{ + id: true, + creationDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + got, err := instance.Client.ProjectV2.CreateProject(tt.ctx, tt.req) + changeDate := time.Now().UTC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + id := func(id string) { + if tt.want.id { + assert.NotEmpty(t, id) + } else { + assert.Empty(t, id) + } + } + assertCreateProjectResponse(t, creationDate, changeDate, tt.want.creationDate, id, got) + }) + } +} + +func getOrgProjectCreatorToken(t *testing.T, ctx context.Context, orgId1, orgId2 string) string { + // create a machine user in Org 1 + userResp := instance.CreateUserTypeMachine(ctx, orgId1) + + // assign ORG_PROJECT_CREATOR role in Org 2 + _, err := instance.Client.InternalPermissionv2Beta.CreateAdministrator(ctx, &internal_permission_v2beta.CreateAdministratorRequest{ + Resource: &internal_permission_v2beta.ResourceType{ + Resource: &internal_permission_v2beta.ResourceType_OrganizationId{OrganizationId: orgId2}, + }, + UserId: userResp.GetId(), + Roles: []string{domain.RoleOrgProjectCreator}, + }) + require.NoError(t, err) + + return instance.CreatePersonalAccessToken(ctx, userResp.GetId()).Token +} + +func assertCreateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate bool, assertID func(string), actualResp *project.CreateProjectResponse) { + if expectedCreationDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetCreationDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.CreationDate) + } + + assertID(actualResp.ProjectId) +} + +func TestServer_UpdateProject(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.UpdateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.UpdateProjectRequest) { + request.ProjectId = "notexisting" + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.UpdateProjectRequest) { + name := integration.ProjectName() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false).GetId() + request.ProjectId = projectID + request.Name = gu.Ptr(name) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{}, + }, + want: want{ + change: false, + changeDate: true, + }, + }, + { + name: "change name, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "change full, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + ProjectRoleAssertion: gu.Ptr(true), + AuthorizationRequired: gu.Ptr(true), + ProjectAccessRequired: gu.Ptr(true), + PrivateLabelingSetting: gu.Ptr(project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_UpdateProject_Permission(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), 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 + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.UpdateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + wantErr: true, + }, + { + name: "project owner, no permission", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + wantErr: true, + }, + { + name: " roject owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + request.ProjectId = projectID + }, + args: args{ + ctx: projectOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "missing permission, other organization", + prepare: func(request *project.UpdateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + wantErr: true, + }, + { + name: "organization owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner, ok", + prepare: func(request *project.UpdateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.UpdateProjectRequest{ + Name: gu.Ptr(integration.ProjectName()), + }, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.UpdateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertUpdateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertUpdateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.UpdateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_DeleteProject(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "empty id", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + ProjectId: "", + }, + wantErr: true, + }, + { + name: "delete, not existing", + ctx: iamOwnerCtx, + req: &project.DeleteProjectRequest{ + ProjectId: "notexisting", + }, + wantDeletionDate: false, + }, + { + name: "delete", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "delete, already removed", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + instance.DeleteProject(iamOwnerCtx, t, projectID) + return creationDate, time.Now().UTC() + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ProjectV2.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func TestServer_DeleteProject_Permission(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + userResp := instance.CreateMachineUser(iamOwnerCtx) + patResp := instance.CreatePersonalAccessToken(iamOwnerCtx, userResp.GetUserId()) + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + instance.CreateProjectMembership(t, iamOwnerCtx, projectID, userResp.GetUserId()) + projectOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + tests := []struct { + name string + ctx context.Context + prepare func(request *project.DeleteProjectRequest) (time.Time, time.Time) + req *project.DeleteProjectRequest + wantDeletionDate bool + wantErr bool + }{ + { + name: "unauthenticated", + ctx: CTX, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "missing permission", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + 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(), integration.ProjectName(), false, false).GetId() + request.ProjectId = 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.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "organization owner, other org", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantErr: true, + }, + { + name: "organization owner", + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + { + name: "instance owner", + ctx: iamOwnerCtx, + prepare: func(request *project.DeleteProjectRequest) (time.Time, time.Time) { + creationDate := time.Now().UTC() + projectID := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + return creationDate, time.Time{} + }, + req: &project.DeleteProjectRequest{}, + wantDeletionDate: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var creationDate, deletionDate time.Time + if tt.prepare != nil { + creationDate, deletionDate = tt.prepare(tt.req) + } + got, err := instance.Client.ProjectV2.DeleteProject(tt.ctx, tt.req) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assertDeleteProjectResponse(t, creationDate, deletionDate, tt.wantDeletionDate, got) + }) + } +} + +func assertDeleteProjectResponse(t *testing.T, creationDate, deletionDate time.Time, expectedDeletionDate bool, actualResp *project.DeleteProjectResponse) { + if expectedDeletionDate { + if !deletionDate.IsZero() { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, deletionDate) + } else { + assert.WithinRange(t, actualResp.GetDeletionDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.DeletionDate) + } +} + +func TestServer_DeactivateProject(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "not existing", + prepare: func(request *project.DeactivateProjectRequest) { + request.ProjectId = "notexisting" + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.DeactivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_DeactivateProject_Permission(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.DeactivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.DeactivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: CTX, + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.DeactivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.DeactivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.DeactivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertDeactivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertDeactivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.DeactivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} + +func TestServer_ActivateProject(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not existing", + prepare: func(request *project.ActivateProjectRequest) { + request.ProjectId = "notexisting" + return + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "no change, ok", + prepare: func(request *project.ActivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "change, ok", + prepare: func(request *project.ActivateProjectRequest) { + projectID := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false).GetId() + request.ProjectId = projectID + instance.DeactivateProject(iamOwnerCtx, t, projectID) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func TestServer_ActivateProject_Permission(t *testing.T) { + t.Parallel() + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + + type args struct { + ctx context.Context + req *project.ActivateProjectRequest + } + type want struct { + change bool + changeDate bool + } + tests := []struct { + name string + prepare func(request *project.ActivateProjectRequest) + args args + want want + wantErr bool + }{ + { + name: "unauthenticated", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: CTX, + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner, other org", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + wantErr: true, + }, + { + name: "organization owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + { + name: "instance owner", + prepare: func(request *project.ActivateProjectRequest) { + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.ProjectId = projectResp.GetId() + instance.DeactivateProject(iamOwnerCtx, t, projectResp.GetId()) + }, + args: args{ + ctx: iamOwnerCtx, + req: &project.ActivateProjectRequest{}, + }, + want: want{ + change: true, + changeDate: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + creationDate := time.Now().UTC() + tt.prepare(tt.args.req) + + got, err := instance.Client.ProjectV2.ActivateProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(t, err) + return + } + changeDate := time.Time{} + if tt.want.change { + changeDate = time.Now().UTC() + } + assert.NoError(t, err) + assertActivateProjectResponse(t, creationDate, changeDate, tt.want.changeDate, got) + }) + } +} + +func assertActivateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedChangeDate bool, actualResp *project.ActivateProjectResponse) { + if expectedChangeDate { + if !changeDate.IsZero() { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, changeDate) + } else { + assert.WithinRange(t, actualResp.GetChangeDate().AsTime(), creationDate, time.Now().UTC()) + } + } else { + assert.Nil(t, actualResp.ChangeDate) + } +} diff --git a/internal/api/grpc/project/v2/integration_test/query_test.go b/internal/api/grpc/project/v2/integration_test/query_test.go new file mode 100644 index 00000000000..9d4ee58ce65 --- /dev/null +++ b/internal/api/grpc/project/v2/integration_test/query_test.go @@ -0,0 +1,2021 @@ +//go:build integration + +package project_test + +import ( + "context" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func TestServer_GetProject(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.GetProjectRequest, *project.GetProjectResponse) + req *project.GetProjectRequest + } + tests := []struct { + name string + args args + want *project.GetProjectResponse + wantErr bool + }{ + { + name: "missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.ProjectId = resp.GetProjectId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "missing permission, other org owner", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + + request.ProjectId = resp.GetId() + }, + req: &project.GetProjectRequest{}, + }, + wantErr: true, + }, + { + name: "not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.GetProjectRequest{ProjectId: "notexisting"}, + }, + wantErr: true, + }, + { + name: "get, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.ProjectId = resp.GetProjectId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{ + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + { + name: "get, ok, org owner", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, false, false) + + request.ProjectId = resp.GetProjectId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + { + name: "get, full, ok", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.GetProjectRequest, response *project.GetProjectResponse) { + orgID := instance.DefaultOrg.GetId() + resp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + request.ProjectId = resp.GetProjectId() + response.Project = resp + }, + req: &project.GetProjectRequest{}, + }, + want: &project.GetProjectResponse{ + Project: &project.Project{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, 2*time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, err := instance.Client.ProjectV2.GetProject(tt.args.ctx, tt.args.req) + if tt.wantErr { + assert.Error(ttt, err) + return + } + assert.NoError(ttt, err) + assert.EqualExportedValues(ttt, tt.want, got) + }, retryDuration, tick, "timeout waiting for expected project result") + }) + } +} + +func TestServer_ListProjects(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(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.GetProjectId(), 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) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + resp := instance.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + resp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + { + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: false, + AuthorizationRequired: false, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + }, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + response.Projects[2] = createProject(iamOwnerCtx, instance, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instance, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetProjectId(), response.Projects[1].GetProjectId(), response.Projects[2].GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.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: &filter.InIDsFilter{ + Ids: []string{resp1.GetProjectId(), resp2.GetProjectId(), resp3.GetProjectId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + 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, integration.OrganizationName(), integration.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: &filter.InIDsFilter{ + Ids: []string{resp1.GetProjectId(), resp2.GetProjectId(), resp3.GetProjectId(), projectResp.GetProjectId()}}, + } + 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{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetProjectId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instance, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, organization", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{OrganizationId: *grantedProjectResp.GrantedOrganizationId}, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, own projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := createProject(iamOwnerCtx, instance, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instance, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instance, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: *grantedProjectResp.GrantedOrganizationId, + Type: project.ProjectOrganizationIDFilter_OWNED, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list project and granted projects, granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + organizationName := integration.OrganizationName() + grantedOrganizationName := integration.OrganizationName() + projectName := integration.ProjectName() + organizationResp := instance.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + grantedOrganizationResp := instance.CreateOrganization(iamOwnerCtx, grantedOrganizationName, integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, organizationResp.GetOrganizationId(), projectName, true, true) + projectGrantResp := instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrganizationResp.GetOrganizationId()) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: grantedOrganizationResp.GetOrganizationId(), + Type: project.ProjectOrganizationIDFilter_GRANTED, + }, + } + response.Projects[0] = &project.Project{ + ProjectId: projectResp.GetId(), + Name: projectName, + OrganizationId: organizationResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: project.ProjectState_PROJECT_STATE_ACTIVE, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrganizationResp.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrganizationName), + GrantedState: project.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list granted project, project id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instance.DefaultOrg.GetId() + + orgName := integration.OrganizationName() + projectName := integration.ProjectName() + orgResp := instance.CreateOrganization(iamOwnerCtx, orgName, integration.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: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + response.Projects[0] = &project.Project{ + ProjectId: 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) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ProjectV2.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjects_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + orgID := instancePermissionV2.DefaultOrg.GetId() + + type args struct { + ctx context.Context + dep func(*project.ListProjectsRequest, *project.ListProjectsResponse) + req *project.ListProjectsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + resp := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp.GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{}, + }, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{ + {Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list single name", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + request.Filters[0].Filter = &project.ProjectSearchFilter_ProjectNameFilter{ + ProjectNameFilter: &project.ProjectNameFilter{ + ProjectName: response.Projects[0].Name, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + response.Projects[2] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, false) + response.Projects[1] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, orgID, false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{response.Projects[0].GetProjectId(), response.Projects[1].GetProjectId(), response.Projects[2].GetProjectId()}, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + response.Projects[3] = projectResp + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetProjectId()}, + }, + } + response.Projects[2] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 4, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, owned and granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[1] = grantedProjectResp + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: *grantedProjectResp.GrantedOrganizationId, + Type: project.ProjectOrganizationIDFilter_OWNED_OR_GRANTED, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + {}, + }, + }, + }, + { + name: "list project and granted projects, own projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, true) + + grantedProjectResp := createGrantedProject(iamOwnerCtx, instancePermissionV2, t, projectResp) + response.Projects[0] = createProject(iamOwnerCtx, instancePermissionV2, t, *grantedProjectResp.GrantedOrganizationId, true, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: *grantedProjectResp.GrantedOrganizationId, + Type: project.ProjectOrganizationIDFilter_OWNED, + }, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list project and granted projects, granted projects", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + organizationName := integration.OrganizationName() + organizationResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, organizationName, integration.Email()) + projectResp := createProject(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), true, true) + projectGrantResp := createProjectGrant(iamOwnerCtx, instancePermissionV2, t, organizationResp.GetOrganizationId(), projectResp.GetProjectId(), projectResp.GetName()) + request.Filters[0].Filter = &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{ + OrganizationId: projectGrantResp.GetGrantedOrganizationId(), + Type: project.ProjectOrganizationIDFilter_GRANTED, + }, + } + response.Projects[0] = &project.Project{ + ProjectId: projectResp.GetProjectId(), + Name: projectResp.GetName(), + OrganizationId: organizationResp.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: project.ProjectState_PROJECT_STATE_ACTIVE, + ProjectRoleAssertion: false, + ProjectAccessRequired: true, + AuthorizationRequired: true, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(projectGrantResp.GrantedOrganizationId), + GrantedOrganizationName: gu.Ptr(projectGrantResp.GrantedOrganizationName), + GrantedState: project.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE, + } + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + resp1 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, false) + resp2 := createProject(iamOwnerCtx, instancePermissionV2, t, orgID, true, false) + resp3 := createProject(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), false, true) + request.Filters[0].Filter = &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{resp1.GetProjectId(), resp2.GetProjectId(), resp3.GetProjectId()}, + }, + } + + response.Projects[0] = resp2 + }, + req: &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{}}, + }, + }, + want: &project.ListProjectsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + Projects: []*project.Project{ + {}, + }, + }, + }, + { + name: "list granted project, project id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectsRequest, response *project.ListProjectsResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + + orgName := integration.OrganizationName() + projectName := integration.ProjectName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, orgName, integration.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: &filter.InIDsFilter{Ids: []string{projectResp.GetId()}}, + } + + response.Projects[0] = &project.Project{ + ProjectId: 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: 2, + AppliedLimit: 100, + }, + Projects: []*project.Project{{}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.ProjectV2.ListProjects(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.Projects, len(tt.want.Projects)) { + for i := range tt.want.Projects { + assert.EqualExportedValues(ttt, tt.want.Projects[i], got.Projects[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProject(ctx context.Context, instance *integration.Instance, t *testing.T, orgID string, projectRoleCheck, hasProjectCheck bool) *project.Project { + name := integration.ProjectName() + resp := instance.CreateProject(ctx, t, orgID, name, projectRoleCheck, hasProjectCheck) + return &project.Project{ + ProjectId: resp.GetId(), + Name: name, + OrganizationId: orgID, + CreationDate: resp.GetCreationDate(), + ChangeDate: resp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: hasProjectCheck, + AuthorizationRequired: projectRoleCheck, + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + } +} + +func createGrantedProject(ctx context.Context, instance *integration.Instance, t *testing.T, projectToGrant *project.Project) *project.Project { + grantedOrgName := integration.OrganizationName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, integration.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectToGrant.GetProjectId(), grantedOrg.GetOrganizationId()) + + return &project.Project{ + ProjectId: projectToGrant.GetProjectId(), + Name: projectToGrant.GetName(), + OrganizationId: projectToGrant.GetOrganizationId(), + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + State: 1, + ProjectRoleAssertion: false, + ProjectAccessRequired: projectToGrant.GetProjectAccessRequired(), + AuthorizationRequired: projectToGrant.GetAuthorizationRequired(), + PrivateLabelingSetting: project.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED, + GrantedOrganizationId: gu.Ptr(grantedOrg.GetOrganizationId()), + GrantedOrganizationName: gu.Ptr(grantedOrgName), + GrantedState: 1, + } +} + +func assertPaginationResponse(t *assert.CollectT, expected *filter.PaginationResponse, actual *filter.PaginationResponse) { + assert.Equal(t, expected.AppliedLimit, actual.AppliedLimit) + assert.Equal(t, expected.TotalResult, actual.TotalResult) +} + +func TestServer_ListProjectGrants(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(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.GetProjectId(), projectResp.GetName()) + instance.CreateProjectGrantMembership(t, iamOwnerCtx, projectResp.GetProjectId(), projectGrantResp.GetGrantedOrganizationId(), userResp.GetUserId()) + projectGrantOwnerCtx := integration.WithAuthorizationToken(CTX, patResp.Token) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{{ + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instance.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{ + {Filter: &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{"notfound"}, + }, + }, + }, + }, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list by id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instance, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := integration.ProjectName() + name2 := integration.ProjectName() + name3 := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.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: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = 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) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list multiple id, limited permissions, project grant owner", + args: args{ + ctx: projectGrantOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := integration.ProjectName() + name2 := integration.ProjectName() + name3 := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.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: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId(), projectResp.GetProjectId()}}, + } + + 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, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list single id with removed role", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + projectRoleResp := addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instance, t, orgID, projectResp.GetId(), name, projectRoleResp.Key) + + removeRoleResp := instance.RemoveProjectRole(iamOwnerCtx, t, projectResp.GetId(), projectRoleResp.Key) + response.ProjectGrants[0].GrantedRoleKeys = nil + response.ProjectGrants[0].ChangeDate = removeRoleResp.GetRemovalDate() + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ProjectV2.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectGrants_PermissionV2(t *testing.T) { + // removed as permission v2 is not implemented yet for project grant level permissions + // ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectGrantsRequest, *project.ListProjectGrantsResponse) + req *project.ListProjectGrantsRequest + } + tests := []struct { + name string + args args + want *project.ListProjectGrantsResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), integration.ProjectName(), false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + grantedOrg := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + request.Filters[1].Filter = &project.ProjectGrantSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &filter.IDFilter{Id: grantedOrg.GetOrganizationId()}, + } + + instancePermissionV2.CreateProjectGrant(iamOwnerCtx, t, projectResp.GetId(), grantedOrg.GetOrganizationId()) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}, {}}, + }, + }, + wantErr: true, + }, + { + name: "list by id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + { + name: "list by id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{}, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name := integration.ProjectName() + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{projectResp.GetId()}, + }, + } + + response.ProjectGrants[2] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[1] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, projectResp.GetId(), name) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, {}, {}, + }, + }, + }, + { + name: "list multiple id, limited permissions", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectGrantsRequest, response *project.ListProjectGrantsResponse) { + name1 := integration.ProjectName() + name2 := integration.ProjectName() + name3 := integration.ProjectName() + orgID := instancePermissionV2.DefaultOrg.GetId() + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + project1Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, name1, false, false) + project2Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name2, false, false) + project3Resp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), name3, false, false) + request.Filters[0].Filter = &project.ProjectGrantSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{ + Ids: []string{project1Resp.GetId(), project2Resp.GetId(), project3Resp.GetId()}, + }, + } + + response.ProjectGrants[0] = createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgID, project1Resp.GetId(), name1) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project2Resp.GetId(), name2) + createProjectGrant(iamOwnerCtx, instancePermissionV2, t, orgResp.GetOrganizationId(), project3Resp.GetId(), name3) + }, + req: &project.ListProjectGrantsRequest{ + Filters: []*project.ProjectGrantSearchFilter{{}}, + }, + }, + want: &project.ListProjectGrantsResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectGrants: []*project.ProjectGrant{ + {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.ProjectV2.ListProjectGrants(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectGrants, len(tt.want.ProjectGrants)) { + for i := range tt.want.ProjectGrants { + assert.EqualExportedValues(ttt, tt.want.ProjectGrants[i], got.ProjectGrants[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func createProjectGrant(ctx context.Context, instance *integration.Instance, t *testing.T, orgID, projectID, projectName string, roles ...string) *project.ProjectGrant { + grantedOrgName := integration.ProjectName() + grantedOrg := instance.CreateOrganization(ctx, grantedOrgName, integration.Email()) + projectGrantResp := instance.CreateProjectGrant(ctx, t, projectID, grantedOrg.GetOrganizationId(), roles...) + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, time.Minute) + require.EventuallyWithT(t, func(collect *assert.CollectT) { + projects, err := instance.Client.ProjectV2.ListProjects(ctx, &project.ListProjectsRequest{ + Filters: []*project.ProjectSearchFilter{{ + Filter: &project.ProjectSearchFilter_InProjectIdsFilter{ + InProjectIdsFilter: &filter.InIDsFilter{Ids: []string{projectID}}, + }, + }, { + Filter: &project.ProjectSearchFilter_OrganizationIdFilter{ + OrganizationIdFilter: &project.ProjectOrganizationIDFilter{OrganizationId: grantedOrg.GetOrganizationId()}, + }, + }}, + }) + require.NoError(collect, err) + require.Len(collect, projects.Projects, 1) + }, retryDuration, tick) + + return &project.ProjectGrant{ + OrganizationId: orgID, + CreationDate: projectGrantResp.GetCreationDate(), + ChangeDate: projectGrantResp.GetCreationDate(), + GrantedOrganizationId: grantedOrg.GetOrganizationId(), + GrantedOrganizationName: grantedOrgName, + ProjectId: projectID, + ProjectName: projectName, + State: 1, + GrantedRoleKeys: roles, + } +} + +func TestServer_ListProjectRoles(t *testing.T) { + iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instance.CreateProject(iamOwnerCtx, t, instance.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + projectResp := instance.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instance.DefaultOrg.GetId() + projectResp := instance.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instance, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instance.Client.ProjectV2.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func TestServer_ListProjectRoles_PermissionV2(t *testing.T) { + ensureFeaturePermissionV2Enabled(t, instancePermissionV2) + iamOwnerCtx := instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + + type args struct { + ctx context.Context + dep func(*project.ListProjectRolesRequest, *project.ListProjectRolesResponse) + req *project.ListProjectRolesRequest + } + tests := []struct { + name string + args args + want *project.ListProjectRolesResponse + wantErr bool + }{ + { + name: "list by id, unauthenticated", + args: args{ + ctx: CTX, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list by id, no permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeNoPermission), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, instancePermissionV2.DefaultOrg.GetId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{ + Filters: []*project.ProjectRoleSearchFilter{{}}, + }, + }, + wantErr: true, + }, + { + name: "list, not found", + args: args{ + ctx: iamOwnerCtx, + req: &project.ListProjectRolesRequest{ + ProjectId: "notfound", + }, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + }, + }, + { + name: "list single id, missing permission", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgResp := instancePermissionV2.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email()) + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgResp.GetOrganizationId(), integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 0, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{}, + }, + }, + { + name: "list single id", + args: args{ + ctx: instancePermissionV2.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner), + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 1, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, + }, + }, + }, + { + name: "list multiple id", + args: args{ + ctx: iamOwnerCtx, + dep: func(request *project.ListProjectRolesRequest, response *project.ListProjectRolesResponse) { + orgID := instancePermissionV2.DefaultOrg.GetId() + projectResp := instancePermissionV2.CreateProject(iamOwnerCtx, t, orgID, integration.ProjectName(), false, false) + + request.ProjectId = projectResp.GetId() + response.ProjectRoles[2] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[1] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + response.ProjectRoles[0] = addProjectRole(iamOwnerCtx, instancePermissionV2, t, projectResp.GetId()) + }, + req: &project.ListProjectRolesRequest{}, + }, + want: &project.ListProjectRolesResponse{ + Pagination: &filter.PaginationResponse{ + TotalResult: 3, + AppliedLimit: 100, + }, + ProjectRoles: []*project.ProjectRole{ + {}, {}, {}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.dep != nil { + tt.args.dep(tt.args.req, tt.want) + } + + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(iamOwnerCtx, time.Minute) + require.EventuallyWithT(t, func(ttt *assert.CollectT) { + got, listErr := instancePermissionV2.Client.ProjectV2.ListProjectRoles(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(ttt, listErr, "Error: "+listErr.Error()) + return + } + require.NoError(ttt, listErr) + + // always first check length, otherwise its failed anyway + if assert.Len(ttt, got.ProjectRoles, len(tt.want.ProjectRoles)) { + for i := range tt.want.ProjectRoles { + assert.EqualExportedValues(ttt, tt.want.ProjectRoles[i], got.ProjectRoles[i]) + } + } + assertPaginationResponse(ttt, tt.want.Pagination, got.Pagination) + }, retryDuration, tick, "timeout waiting for expected execution result") + }) + } +} + +func addProjectRole(ctx context.Context, instance *integration.Instance, t *testing.T, projectID string) *project.ProjectRole { + name := integration.RoleKey() + projectRoleResp := instance.AddProjectRole(ctx, t, projectID, name, name, name) + + return &project.ProjectRole{ + ProjectId: projectID, + CreationDate: projectRoleResp.GetCreationDate(), + ChangeDate: projectRoleResp.GetCreationDate(), + Key: name, + DisplayName: name, + Group: name, + } +} diff --git a/internal/api/grpc/project/v2/integration_test/server_test.go b/internal/api/grpc/project/v2/integration_test/server_test.go new file mode 100644 index 00000000000..e525fe46f80 --- /dev/null +++ b/internal/api/grpc/project/v2/integration_test/server_test.go @@ -0,0 +1,63 @@ +//go:build integration + +package project_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/feature/v2" +) + +var ( + CTX context.Context + instance *integration.Instance + instancePermissionV2 *integration.Instance +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + CTX = ctx + instance = integration.NewInstance(ctx) + instancePermissionV2 = integration.NewInstance(CTX) + return m.Run() + }()) +} + +func ensureFeaturePermissionV2Enabled(t *testing.T, instance *integration.Instance) { + ctx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner) + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + require.NoError(t, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + _, err = instance.Client.FeatureV2.SetInstanceFeatures(ctx, &feature.SetInstanceFeaturesRequest{ + PermissionCheckV2: gu.Ptr(true), + }) + require.NoError(t, err) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(ctx, 5*time.Minute) + require.EventuallyWithT(t, + func(ttt *assert.CollectT) { + f, err := instance.Client.FeatureV2.GetInstanceFeatures(ctx, &feature.GetInstanceFeaturesRequest{ + Inheritance: true, + }) + assert.NoError(ttt, err) + if f.PermissionCheckV2.GetEnabled() { + return + } + }, + retryDuration, + tick, + "timed out waiting for ensuring instance feature") +} diff --git a/internal/api/grpc/project/v2/project.go b/internal/api/grpc/project/v2/project.go new file mode 100644 index 00000000000..fcb65a38e58 --- /dev/null +++ b/internal/api/grpc/project/v2/project.go @@ -0,0 +1,162 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "github.com/muhlemmer/gu" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func (s *Server) CreateProject(ctx context.Context, req *connect.Request[project_pb.CreateProjectRequest]) (*connect.Response[project_pb.CreateProjectResponse], error) { + add := projectCreateToCommand(req.Msg) + project, err := s.command.AddProject(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.CreateProjectResponse{ + ProjectId: add.AggregateID, + CreationDate: creationDate, + }), nil +} + +func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddProject { + var aggregateID string + if req.ProjectId != nil { + aggregateID = *req.ProjectId + } + return &command.AddProject{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: req.OrganizationId, + AggregateID: aggregateID, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.AuthorizationRequired, + HasProjectCheck: req.ProjectAccessRequired, + PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting), + } +} + +func privateLabelingSettingToDomain(setting project_pb.PrivateLabelingSetting) domain.PrivateLabelingSetting { + switch setting { + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY: + return domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy + case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED: + return domain.PrivateLabelingSettingUnspecified + default: + return domain.PrivateLabelingSettingUnspecified + } +} + +func (s *Server) UpdateProject(ctx context.Context, req *connect.Request[project_pb.UpdateProjectRequest]) (*connect.Response[project_pb.UpdateProjectResponse], error) { + project, err := s.command.ChangeProject(ctx, projectUpdateToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func projectUpdateToCommand(req *project_pb.UpdateProjectRequest) *command.ChangeProject { + var labeling *domain.PrivateLabelingSetting + if req.PrivateLabelingSetting != nil { + labeling = gu.Ptr(privateLabelingSettingToDomain(*req.PrivateLabelingSetting)) + } + return &command.ChangeProject{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Name: req.Name, + ProjectRoleAssertion: req.ProjectRoleAssertion, + ProjectRoleCheck: req.AuthorizationRequired, + HasProjectCheck: req.ProjectAccessRequired, + PrivateLabelingSetting: labeling, + } +} + +func (s *Server) DeleteProject(ctx context.Context, req *connect.Request[project_pb.DeleteProjectRequest]) (*connect.Response[project_pb.DeleteProjectResponse], error) { + userGrantIDs, err := s.userGrantsFromProject(ctx, req.Msg.GetProjectId()) + if err != nil { + return nil, err + } + + deletedAt, err := s.command.DeleteProject(ctx, req.Msg.GetProjectId(), "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !deletedAt.IsZero() { + deletionDate = timestamppb.New(deletedAt) + } + return connect.NewResponse(&project_pb.DeleteProjectResponse{ + DeletionDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProject(ctx context.Context, projectID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) DeactivateProject(ctx context.Context, req *connect.Request[project_pb.DeactivateProjectRequest]) (*connect.Response[project_pb.DeactivateProjectResponse], error) { + details, err := s.command.DeactivateProject(ctx, req.Msg.GetProjectId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeactivateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) ActivateProject(ctx context.Context, req *connect.Request[project_pb.ActivateProjectRequest]) (*connect.Response[project_pb.ActivateProjectResponse], error) { + details, err := s.command.ReactivateProject(ctx, req.Msg.GetProjectId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.ActivateProjectResponse{ + ChangeDate: changeDate, + }), nil +} + +func userGrantsToIDs(userGrants []*query.UserGrant) []string { + converted := make([]string, len(userGrants)) + for i, grant := range userGrants { + converted[i] = grant.ID + } + return converted +} diff --git a/internal/api/grpc/project/v2/project_grant.go b/internal/api/grpc/project/v2/project_grant.go new file mode 100644 index 00000000000..32d9cb88077 --- /dev/null +++ b/internal/api/grpc/project/v2/project_grant.go @@ -0,0 +1,126 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func (s *Server) CreateProjectGrant(ctx context.Context, req *connect.Request[project_pb.CreateProjectGrantRequest]) (*connect.Response[project_pb.CreateProjectGrantResponse], error) { + add := projectGrantCreateToCommand(req.Msg) + project, err := s.command.AddProjectGrant(ctx, add) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + creationDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.CreateProjectGrantResponse{ + CreationDate: creationDate, + }), nil +} + +func projectGrantCreateToCommand(req *project_pb.CreateProjectGrantRequest) *command.AddProjectGrant { + return &command.AddProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) UpdateProjectGrant(ctx context.Context, req *connect.Request[project_pb.UpdateProjectGrantRequest]) (*connect.Response[project_pb.UpdateProjectGrantResponse], error) { + project, err := s.command.ChangeProjectGrant(ctx, projectGrantUpdateToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !project.EventDate.IsZero() { + changeDate = timestamppb.New(project.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func projectGrantUpdateToCommand(req *project_pb.UpdateProjectGrantRequest) *command.ChangeProjectGrant { + return &command.ChangeProjectGrant{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + GrantedOrgID: req.GrantedOrganizationId, + RoleKeys: req.RoleKeys, + } +} + +func (s *Server) DeactivateProjectGrant(ctx context.Context, req *connect.Request[project_pb.DeactivateProjectGrantRequest]) (*connect.Response[project_pb.DeactivateProjectGrantResponse], error) { + details, err := s.command.DeactivateProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeactivateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) ActivateProjectGrant(ctx context.Context, req *connect.Request[project_pb.ActivateProjectGrantRequest]) (*connect.Response[project_pb.ActivateProjectGrantResponse], error) { + details, err := s.command.ReactivateProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "") + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + changeDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.ActivateProjectGrantResponse{ + ChangeDate: changeDate, + }), nil +} + +func (s *Server) DeleteProjectGrant(ctx context.Context, req *connect.Request[project_pb.DeleteProjectGrantRequest]) (*connect.Response[project_pb.DeleteProjectGrantResponse], error) { + userGrantIDs, err := s.userGrantsFromProjectGrant(ctx, req.Msg.GetProjectId(), req.Msg.GetGrantedOrganizationId()) + if err != nil { + return nil, err + } + details, err := s.command.DeleteProjectGrant(ctx, req.Msg.GetProjectId(), "", req.Msg.GetGrantedOrganizationId(), "", userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.DeleteProjectGrantResponse{ + DeletionDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProjectGrant(ctx context.Context, projectID, grantedOrganizationID string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + grantQuery, err := query.NewUserGrantWithGrantedQuery(grantedOrganizationID) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, grantQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} diff --git a/internal/api/grpc/project/v2/project_role.go b/internal/api/grpc/project/v2/project_role.go new file mode 100644 index 00000000000..0299a4da118 --- /dev/null +++ b/internal/api/grpc/project/v2/project_role.go @@ -0,0 +1,133 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func (s *Server) AddProjectRole(ctx context.Context, req *connect.Request[project_pb.AddProjectRoleRequest]) (*connect.Response[project_pb.AddProjectRoleResponse], error) { + role, err := s.command.AddProjectRole(ctx, addProjectRoleRequestToCommand(req.Msg)) + if err != nil { + return nil, err + } + var creationDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + creationDate = timestamppb.New(role.EventDate) + } + return connect.NewResponse(&project_pb.AddProjectRoleResponse{ + CreationDate: creationDate, + }), nil +} + +func addProjectRoleRequestToCommand(req *project_pb.AddProjectRoleRequest) *command.AddProjectRole { + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.AddProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: req.DisplayName, + Group: group, + } +} + +func (s *Server) UpdateProjectRole(ctx context.Context, req *connect.Request[project_pb.UpdateProjectRoleRequest]) (*connect.Response[project_pb.UpdateProjectRoleResponse], error) { + role, err := s.command.ChangeProjectRole(ctx, updateProjectRoleRequestToCommand(req.Msg)) + if err != nil { + return nil, err + } + var changeDate *timestamppb.Timestamp + if !role.EventDate.IsZero() { + changeDate = timestamppb.New(role.EventDate) + } + return connect.NewResponse(&project_pb.UpdateProjectRoleResponse{ + ChangeDate: changeDate, + }), nil +} + +func updateProjectRoleRequestToCommand(req *project_pb.UpdateProjectRoleRequest) *command.ChangeProjectRole { + displayName := "" + if req.DisplayName != nil { + displayName = *req.DisplayName + } + group := "" + if req.Group != nil { + group = *req.Group + } + + return &command.ChangeProjectRole{ + ObjectRoot: models.ObjectRoot{ + AggregateID: req.ProjectId, + }, + Key: req.RoleKey, + DisplayName: displayName, + Group: group, + } +} + +func (s *Server) RemoveProjectRole(ctx context.Context, req *connect.Request[project_pb.RemoveProjectRoleRequest]) (*connect.Response[project_pb.RemoveProjectRoleResponse], error) { + userGrantIDs, err := s.userGrantsFromProjectAndRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey()) + if err != nil { + return nil, err + } + projectGrantIDs, err := s.projectGrantsFromProjectAndRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey()) + if err != nil { + return nil, err + } + details, err := s.command.RemoveProjectRole(ctx, req.Msg.GetProjectId(), req.Msg.GetRoleKey(), "", projectGrantIDs, userGrantIDs...) + if err != nil { + return nil, err + } + var deletionDate *timestamppb.Timestamp + if !details.EventDate.IsZero() { + deletionDate = timestamppb.New(details.EventDate) + } + return connect.NewResponse(&project_pb.RemoveProjectRoleResponse{ + RemovalDate: deletionDate, + }), nil +} + +func (s *Server) userGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectQuery, err := query.NewUserGrantProjectIDSearchQuery(projectID) + if err != nil { + return nil, err + } + rolesQuery, err := query.NewUserGrantRoleQuery(roleKey) + if err != nil { + return nil, err + } + userGrants, err := s.query.UserGrants(ctx, &query.UserGrantsQueries{ + Queries: []query.SearchQuery{projectQuery, rolesQuery}, + }, false, nil) + if err != nil { + return nil, err + } + return userGrantsToIDs(userGrants.UserGrants), nil +} + +func (s *Server) projectGrantsFromProjectAndRole(ctx context.Context, projectID, roleKey string) ([]string, error) { + projectGrants, err := s.query.SearchProjectGrantsByProjectIDAndRoleKey(ctx, projectID, roleKey) + if err != nil { + return nil, err + } + return projectGrantsToIDs(projectGrants), nil +} + +func projectGrantsToIDs(projectGrants *query.ProjectGrants) []string { + converted := make([]string, len(projectGrants.ProjectGrants)) + for i, grant := range projectGrants.ProjectGrants { + converted[i] = grant.GrantID + } + return converted +} diff --git a/internal/api/grpc/project/v2/query.go b/internal/api/grpc/project/v2/query.go new file mode 100644 index 00000000000..95f7564b179 --- /dev/null +++ b/internal/api/grpc/project/v2/query.go @@ -0,0 +1,428 @@ +package project + +import ( + "context" + + "connectrpc.com/connect" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/grpc/filter/v2" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/internal/zerrors" + filter_pb "github.com/zitadel/zitadel/pkg/grpc/filter/v2" + project_pb "github.com/zitadel/zitadel/pkg/grpc/project/v2" +) + +func (s *Server) GetProject(ctx context.Context, req *connect.Request[project_pb.GetProjectRequest]) (*connect.Response[project_pb.GetProjectResponse], error) { + project, err := s.query.GetProjectByIDWithPermission(ctx, true, req.Msg.GetProjectId(), s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.GetProjectResponse{ + Project: projectToPb(project), + }), nil +} + +func (s *Server) ListProjects(ctx context.Context, req *connect.Request[project_pb.ListProjectsRequest]) (*connect.Response[project_pb.ListProjectsResponse], error) { + queries, err := s.listProjectRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchGrantedProjects(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectsResponse{ + Projects: grantedProjectsToPb(resp.GrantedProjects), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listProjectRequestToModel(req *project_pb.ListProjectsRequest) (*query.ProjectAndGrantedProjectSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectFiltersToQuery(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectAndGrantedProjectSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: grantedProjectFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func grantedProjectFieldNameToSortingColumn(field *project_pb.ProjectFieldName) query.Column { + if field == nil { + return query.GrantedProjectColumnCreationDate + } + switch *field { + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CREATION_DATE: + return query.GrantedProjectColumnCreationDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_ID: + return query.GrantedProjectColumnID + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_NAME: + return query.GrantedProjectColumnName + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_CHANGE_DATE: + return query.GrantedProjectColumnChangeDate + case project_pb.ProjectFieldName_PROJECT_FIELD_NAME_UNSPECIFIED: + return query.GrantedProjectColumnCreationDate + default: + return query.GrantedProjectColumnCreationDate + } +} + +func projectFiltersToQuery(queries []*project_pb.ProjectSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectFilterToModel(filter *project_pb.ProjectSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectSearchFilter_InProjectIdsFilter: + return projectInIDsFilterToQuery(q.InProjectIdsFilter) + case *project_pb.ProjectSearchFilter_OrganizationIdFilter: + return projectOrganizationIDFilterToQuery(q.OrganizationIdFilter) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-vR9nC", "List.Query.Invalid") + } +} + +func projectNameFilterToQuery(q *project_pb.ProjectNameFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectNameSearchQuery(filter.TextMethodPbToQuery(q.Method), q.GetProjectName()) +} + +func projectInIDsFilterToQuery(q *filter_pb.InIDsFilter) (query.SearchQuery, error) { + return query.NewGrantedProjectIDSearchQuery(q.Ids) +} + +func projectOrganizationIDFilterToQuery(q *project_pb.ProjectOrganizationIDFilter) (query.SearchQuery, error) { + switch q.GetType() { + case project_pb.ProjectOrganizationIDFilter_OWNED: + return query.NewGrantedProjectResourceOwnerSearchQuery(q.GetOrganizationId()) + case project_pb.ProjectOrganizationIDFilter_GRANTED: + return query.NewGrantedProjectGrantedOrganizationIDSearchQuery(q.GetOrganizationId()) + case project_pb.ProjectOrganizationIDFilter_OWNED_OR_GRANTED: + return query.NewGrantedProjectOrganizationIDSearchQuery(q.GetOrganizationId()) + case project_pb.ProjectOrganizationIDFilter_TYPE_UNSPECIFIED: + return query.NewGrantedProjectOrganizationIDSearchQuery(q.GetOrganizationId()) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Sk3sd", "List.Query.Invalid") + } +} + +func grantedProjectsToPb(projects []*query.GrantedProject) []*project_pb.Project { + o := make([]*project_pb.Project, len(projects)) + for i, org := range projects { + o[i] = grantedProjectToPb(org) + } + return o +} + +func projectToPb(project *query.Project) *project_pb.Project { + return &project_pb.Project{ + ProjectId: project.ID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.State), + Name: project.Name, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + } +} + +func grantedProjectToPb(project *query.GrantedProject) *project_pb.Project { + var grantedOrganizationID, grantedOrganizationName *string + if project.GrantedOrgID != "" { + grantedOrganizationID = &project.GrantedOrgID + } + if project.OrgName != "" { + grantedOrganizationName = &project.OrgName + } + + return &project_pb.Project{ + ProjectId: project.ProjectID, + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + State: projectStateToPb(project.ProjectState), + Name: project.ProjectName, + PrivateLabelingSetting: privateLabelingSettingToPb(project.PrivateLabelingSetting), + ProjectAccessRequired: project.HasProjectCheck, + ProjectRoleAssertion: project.ProjectRoleAssertion, + AuthorizationRequired: project.ProjectRoleCheck, + GrantedOrganizationId: grantedOrganizationID, + GrantedOrganizationName: grantedOrganizationName, + GrantedState: grantedProjectStateToPb(project.ProjectGrantState), + } +} + +func projectStateToPb(state domain.ProjectState) project_pb.ProjectState { + switch state { + case domain.ProjectStateActive: + return project_pb.ProjectState_PROJECT_STATE_ACTIVE + case domain.ProjectStateInactive: + return project_pb.ProjectState_PROJECT_STATE_INACTIVE + case domain.ProjectStateUnspecified, domain.ProjectStateRemoved: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.ProjectState_PROJECT_STATE_UNSPECIFIED + } +} +func grantedProjectStateToPb(state domain.ProjectGrantState) project_pb.GrantedProjectState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + default: + return project_pb.GrantedProjectState_GRANTED_PROJECT_STATE_UNSPECIFIED + } +} + +func privateLabelingSettingToPb(setting domain.PrivateLabelingSetting) project_pb.PrivateLabelingSetting { + switch setting { + case domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingEnforceProjectResourceOwnerPolicy: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY + case domain.PrivateLabelingSettingUnspecified: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + default: + return project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_UNSPECIFIED + } +} + +func (s *Server) ListProjectGrants(ctx context.Context, req *connect.Request[project_pb.ListProjectGrantsRequest]) (*connect.Response[project_pb.ListProjectGrantsResponse], error) { + queries, err := s.listProjectGrantsRequestToModel(req.Msg) + if err != nil { + return nil, err + } + resp, err := s.query.SearchProjectGrants(ctx, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectGrantsResponse{ + ProjectGrants: projectGrantsToPb(resp.ProjectGrants), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, resp.SearchResponse), + }), nil +} + +func (s *Server) listProjectGrantsRequestToModel(req *project_pb.ListProjectGrantsRequest) (*query.ProjectGrantSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := projectGrantFiltersToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectGrantSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectGrantFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectGrantFieldNameToSortingColumn(field *project_pb.ProjectGrantFieldName) query.Column { + if field == nil { + return query.ProjectGrantColumnCreationDate + } + switch *field { + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_PROJECT_ID: + return query.ProjectGrantColumnProjectID + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CREATION_DATE: + return query.ProjectGrantColumnCreationDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_CHANGE_DATE: + return query.ProjectGrantColumnChangeDate + case project_pb.ProjectGrantFieldName_PROJECT_GRANT_FIELD_NAME_UNSPECIFIED: + return query.ProjectGrantColumnCreationDate + default: + return query.ProjectGrantColumnCreationDate + } +} + +func projectGrantFiltersToModel(queries []*project_pb.ProjectGrantSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, qry := range queries { + q[i], err = projectGrantFilterToModel(qry) + if err != nil { + return nil, err + } + } + return q, nil +} + +func projectGrantFilterToModel(filter *project_pb.ProjectGrantSearchFilter) (query.SearchQuery, error) { + switch q := filter.Filter.(type) { + case *project_pb.ProjectGrantSearchFilter_ProjectNameFilter: + return projectNameFilterToQuery(q.ProjectNameFilter) + case *project_pb.ProjectGrantSearchFilter_RoleKeyFilter: + return query.NewProjectGrantRoleKeySearchQuery(q.RoleKeyFilter.Key) + case *project_pb.ProjectGrantSearchFilter_InProjectIdsFilter: + return query.NewProjectGrantProjectIDsSearchQuery(q.InProjectIdsFilter.Ids) + case *project_pb.ProjectGrantSearchFilter_OrganizationIdFilter: + return query.NewProjectGrantResourceOwnerSearchQuery(q.OrganizationIdFilter.Id) + case *project_pb.ProjectGrantSearchFilter_GrantedOrganizationIdFilter: + return query.NewProjectGrantGrantedOrgIDSearchQuery(q.GrantedOrganizationIdFilter.Id) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-M099f", "List.Query.Invalid") + } +} + +func projectGrantsToPb(projects []*query.ProjectGrant) []*project_pb.ProjectGrant { + p := make([]*project_pb.ProjectGrant, len(projects)) + for i, project := range projects { + p[i] = projectGrantToPb(project) + } + return p +} + +func projectGrantToPb(project *query.ProjectGrant) *project_pb.ProjectGrant { + return &project_pb.ProjectGrant{ + OrganizationId: project.ResourceOwner, + CreationDate: timestamppb.New(project.CreationDate), + ChangeDate: timestamppb.New(project.ChangeDate), + GrantedOrganizationId: project.GrantedOrgID, + GrantedOrganizationName: project.OrgName, + GrantedRoleKeys: project.GrantedRoleKeys, + ProjectId: project.ProjectID, + ProjectName: project.ProjectName, + State: projectGrantStateToPb(project.State), + } +} + +func projectGrantStateToPb(state domain.ProjectGrantState) project_pb.ProjectGrantState { + switch state { + case domain.ProjectGrantStateActive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_ACTIVE + case domain.ProjectGrantStateInactive: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_INACTIVE + case domain.ProjectGrantStateUnspecified, domain.ProjectGrantStateRemoved: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + default: + return project_pb.ProjectGrantState_PROJECT_GRANT_STATE_UNSPECIFIED + } +} + +func (s *Server) ListProjectRoles(ctx context.Context, req *connect.Request[project_pb.ListProjectRolesRequest]) (*connect.Response[project_pb.ListProjectRolesResponse], error) { + queries, err := s.listProjectRolesRequestToModel(req.Msg) + if err != nil { + return nil, err + } + err = queries.AppendProjectIDQuery(req.Msg.GetProjectId()) + if err != nil { + return nil, err + } + roles, err := s.query.SearchProjectRoles(ctx, true, queries, s.checkPermission) + if err != nil { + return nil, err + } + return connect.NewResponse(&project_pb.ListProjectRolesResponse{ + ProjectRoles: roleViewsToPb(roles.ProjectRoles), + Pagination: filter.QueryToPaginationPb(queries.SearchRequest, roles.SearchResponse), + }), nil +} + +func (s *Server) listProjectRolesRequestToModel(req *project_pb.ListProjectRolesRequest) (*query.ProjectRoleSearchQueries, error) { + offset, limit, asc, err := filter.PaginationPbToQuery(s.systemDefaults, req.Pagination) + if err != nil { + return nil, err + } + queries, err := roleQueriesToModel(req.Filters) + if err != nil { + return nil, err + } + return &query.ProjectRoleSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: projectRoleFieldNameToSortingColumn(req.SortingColumn), + }, + Queries: queries, + }, nil +} + +func projectRoleFieldNameToSortingColumn(field *project_pb.ProjectRoleFieldName) query.Column { + if field == nil { + return query.ProjectRoleColumnCreationDate + } + switch *field { + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_KEY: + return query.ProjectRoleColumnKey + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CREATION_DATE: + return query.ProjectRoleColumnCreationDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_CHANGE_DATE: + return query.ProjectRoleColumnChangeDate + case project_pb.ProjectRoleFieldName_PROJECT_ROLE_FIELD_NAME_UNSPECIFIED: + return query.ProjectRoleColumnCreationDate + default: + return query.ProjectRoleColumnCreationDate + } +} + +func roleQueriesToModel(queries []*project_pb.ProjectRoleSearchFilter) (_ []query.SearchQuery, err error) { + q := make([]query.SearchQuery, len(queries)) + for i, query := range queries { + q[i], err = roleQueryToModel(query) + if err != nil { + return nil, err + } + } + return q, nil +} + +func roleQueryToModel(apiQuery *project_pb.ProjectRoleSearchFilter) (query.SearchQuery, error) { + switch q := apiQuery.Filter.(type) { + case *project_pb.ProjectRoleSearchFilter_RoleKeyFilter: + return query.NewProjectRoleKeySearchQuery(filter.TextMethodPbToQuery(q.RoleKeyFilter.Method), q.RoleKeyFilter.Key) + case *project_pb.ProjectRoleSearchFilter_DisplayNameFilter: + return query.NewProjectRoleDisplayNameSearchQuery(filter.TextMethodPbToQuery(q.DisplayNameFilter.Method), q.DisplayNameFilter.DisplayName) + default: + return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-fms0e", "List.Query.Invalid") + } +} + +func roleViewsToPb(roles []*query.ProjectRole) []*project_pb.ProjectRole { + o := make([]*project_pb.ProjectRole, len(roles)) + for i, org := range roles { + o[i] = roleViewToPb(org) + } + return o +} + +func roleViewToPb(role *query.ProjectRole) *project_pb.ProjectRole { + return &project_pb.ProjectRole{ + ProjectId: role.ProjectID, + Key: role.Key, + CreationDate: timestamppb.New(role.CreationDate), + ChangeDate: timestamppb.New(role.ChangeDate), + DisplayName: role.DisplayName, + Group: role.Group, + } +} diff --git a/internal/api/grpc/project/v2/server.go b/internal/api/grpc/project/v2/server.go new file mode 100644 index 00000000000..85a10410706 --- /dev/null +++ b/internal/api/grpc/project/v2/server.go @@ -0,0 +1,60 @@ +package project + +import ( + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/config/systemdefaults" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/query" + "github.com/zitadel/zitadel/pkg/grpc/project/v2" + "github.com/zitadel/zitadel/pkg/grpc/project/v2/projectconnect" +) + +var _ projectconnect.ProjectServiceHandler = (*Server)(nil) + +type Server struct { + systemDefaults systemdefaults.SystemDefaults + command *command.Commands + query *query.Queries + + checkPermission domain.PermissionCheck +} + +func CreateServer( + systemDefaults systemdefaults.SystemDefaults, + command *command.Commands, + query *query.Queries, + checkPermission domain.PermissionCheck, +) *Server { + return &Server{ + systemDefaults: systemDefaults, + command: command, + query: query, + checkPermission: checkPermission, + } +} + +func (s *Server) RegisterConnectServer(interceptors ...connect.Interceptor) (string, http.Handler) { + return projectconnect.NewProjectServiceHandler(s, connect.WithInterceptors(interceptors...)) +} + +func (s *Server) FileDescriptor() protoreflect.FileDescriptor { + return project.File_zitadel_project_v2_project_service_proto +} + +func (s *Server) AppName() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) MethodPrefix() string { + return project.ProjectService_ServiceDesc.ServiceName +} + +func (s *Server) AuthMethods() authz.MethodMapping { + return project.ProjectService_AuthMethods +} diff --git a/internal/integration/client.go b/internal/integration/client.go index 351eb1332bf..e5a150f111a 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -43,6 +43,7 @@ import ( oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta" "github.com/zitadel/zitadel/pkg/grpc/org/v2" org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta" + project_v2 "github.com/zitadel/zitadel/pkg/grpc/project/v2" project_v2beta "github.com/zitadel/zitadel/pkg/grpc/project/v2beta" user_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/user/v3alpha" userschema_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/userschema/v3alpha" @@ -85,6 +86,7 @@ type Client struct { SAMLv2 saml_pb.SAMLServiceClient SCIM *scim.Client Projectv2Beta project_v2beta.ProjectServiceClient + ProjectV2 project_v2.ProjectServiceClient InstanceV2Beta instance.InstanceServiceClient AppV2Beta app_v2beta.AppServiceClient ApplicationV2 application.ApplicationServiceClient @@ -132,6 +134,7 @@ func newClient(ctx context.Context, target string) (*Client, error) { SAMLv2: saml_pb.NewSAMLServiceClient(cc), SCIM: scim.NewScimClient(target), Projectv2Beta: project_v2beta.NewProjectServiceClient(cc), + ProjectV2: project_v2.NewProjectServiceClient(cc), InstanceV2Beta: instance.NewInstanceServiceClient(cc), AppV2Beta: app_v2beta.NewAppServiceClient(cc), ApplicationV2: application.NewApplicationServiceClient(cc), diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 9056410d911..d79aaa90eb8 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -2922,7 +2922,7 @@ service ManagementService { // Get Project By ID // - // Deprecated: use [project v2 service GetProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-get-project.api.mdx) instead. + // Deprecated: use [project v2 service GetProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-get-project.api.mdx) instead. // // Returns a project owned by the organization (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc GetProjectByID(GetProjectByIDRequest) returns (GetProjectByIDResponse) { @@ -2951,7 +2951,7 @@ service ManagementService { // Get Granted Project By ID // - // Deprecated: use [project v2 service ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-grants.api.mdx) instead. + // Deprecated: use [project v2 service ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-grants.api.mdx) instead. // // Returns a project owned by another organization and granted to my organization. A Project is a vessel for different applications sharing the same role context. rpc GetGrantedProjectByID(GetGrantedProjectByIDRequest) returns (GetGrantedProjectByIDResponse) { @@ -2980,7 +2980,7 @@ service ManagementService { // Search Project // - // Deprecated: use [project v2 service ListProjects](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-projects.api.mdx) instead. + // Deprecated: use [project v2 service ListProjects](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-projects.api.mdx) instead. // // Lists projects my organization is the owner of (no granted projects). A Project is a vessel for different applications sharing the same role context. rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) { @@ -3009,7 +3009,7 @@ service ManagementService { // Search Granted Project // - // Deprecated: use [project v2 service ListProjects](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-projects.api.mdx) instead. + // Deprecated: use [project v2 service ListProjects](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-projects.api.mdx) instead. // // Lists projects my organization got granted from another organization. A Project is a vessel for different applications sharing the same role context. rpc ListGrantedProjects(ListGrantedProjectsRequest) returns (ListGrantedProjectsResponse) { @@ -3038,7 +3038,7 @@ service ManagementService { // Search Granted Project Roles // - // Deprecated: use [project v2 service ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-grants.api.mdx) instead. + // Deprecated: use [project v2 service ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-grants.api.mdx) instead. // // Lists the roles a granted projects has. These are the roles, that have been granted by the owner organization to my organization. rpc ListGrantedProjectRoles(ListGrantedProjectRolesRequest) returns (ListGrantedProjectRolesResponse) { @@ -3092,7 +3092,7 @@ service ManagementService { // Create Project // - // Deprecated: use [project v2 service CreateProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-create-project.api.mdx) instead. + // Deprecated: use [project v2 service CreateProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-create-project.api.mdx) instead. // // Create a new project. A Project is a vessel for different applications sharing the same role context. rpc AddProject(AddProjectRequest) returns (AddProjectResponse) { @@ -3121,7 +3121,7 @@ service ManagementService { // Update Project // - // Deprecated: use [project v2 service UpdateProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-update-project.api.mdx) instead. + // Deprecated: use [project v2 service UpdateProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-update-project.api.mdx) instead. // // Update a project and its settings. A Project is a vessel for different applications sharing the same role context. rpc UpdateProject(UpdateProjectRequest) returns (UpdateProjectResponse) { @@ -3151,7 +3151,7 @@ service ManagementService { // Deactivate Project // - // Deprecated: use [project v2 service DeactivateProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-deactivate-project.api.mdx) instead. + // Deprecated: use [project v2 service DeactivateProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-deactivate-project.api.mdx) instead. // // Set the state of a project to deactivated. Request returns an error if the project is already deactivated. rpc DeactivateProject(DeactivateProjectRequest) returns (DeactivateProjectResponse) { @@ -3181,7 +3181,7 @@ service ManagementService { // Reactivate Project // - // Deprecated: use [project v2 service ActivateProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-activate-project.api.mdx) instead. + // Deprecated: use [project v2 service ActivateProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-activate-project.api.mdx) instead. // // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc ReactivateProject(ReactivateProjectRequest) returns (ReactivateProjectResponse) { @@ -3211,7 +3211,7 @@ service ManagementService { // Remove Project // - // Deprecated: use [project v2 service DeleteProject](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-delete-project.api.mdx) instead. + // Deprecated: use [project v2 service DeleteProject](apis/resources/project_service_v2/zitadel-project-v-2-project-service-delete-project.api.mdx) instead. // // Set the state of a project to active. Request returns an error if the project is not deactivated. rpc RemoveProject(RemoveProjectRequest) returns (RemoveProjectResponse) { @@ -3240,7 +3240,7 @@ service ManagementService { // Search Project Roles // - // Deprecated: use [project v2 service ListProjectRoles](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-roles.api.mdx) instead. + // Deprecated: use [project v2 service ListProjectRoles](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-roles.api.mdx) instead. // // Returns all roles of a project matching the search query. rpc ListProjectRoles(ListProjectRolesRequest) returns (ListProjectRolesResponse) { @@ -3270,7 +3270,7 @@ service ManagementService { // Add Project Role // - // Deprecated: use [project v2 service AddProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-add-project-role.api.mdx) instead. + // Deprecated: use [project v2 service AddProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-project-service-add-project-role.api.mdx) instead. // // Add a new project role to a project. The key must be unique within the project. rpc AddProjectRole(AddProjectRoleRequest) returns (AddProjectRoleResponse) { @@ -3300,7 +3300,7 @@ service ManagementService { // Bulk Add Project Role // - // Deprecated: use [project v2 service AddProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-add-project-role.api.mdx) instead. + // Deprecated: use [project v2 service AddProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-project-service-add-project-role.api.mdx) instead. // // Add a list of roles to a project. The keys must be unique within the project. rpc BulkAddProjectRoles(BulkAddProjectRolesRequest) returns (BulkAddProjectRolesResponse) { @@ -3330,7 +3330,7 @@ service ManagementService { // Change Project Role // - // Deprecated: use [project v2 service UpdateProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-update-project-role.api.mdx) instead. + // Deprecated: use [project v2 service UpdateProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-project-service-update-project-role.api.mdx) instead. // // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. rpc UpdateProjectRole(UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { @@ -3360,7 +3360,7 @@ service ManagementService { // Remove Project Role // - // Deprecated: use [project v2 service RemoveProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-remove-project-role.api.mdx) instead. + // Deprecated: use [project v2 service RemoveProjectRole](apis/resources/project_service_v2/zitadel-project-v-2-project-service-remove-project-role.api.mdx) instead. // // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. rpc RemoveProjectRole(RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { @@ -4129,7 +4129,7 @@ service ManagementService { // Project Grant By ID // - // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-grants.api.mdx) instead. + // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-grants.api.mdx) instead. // // Returns a project grant. A project grant is when the organization grants its project to another organization. rpc GetProjectGrantByID(GetProjectGrantByIDRequest) returns (GetProjectGrantByIDResponse) { @@ -4157,7 +4157,7 @@ service ManagementService { // Search Project Grants from Project // - // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-grants.api.mdx) instead. + // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-grants.api.mdx) instead. // // Returns a list of project grants for a specific project. A project grant is when the organization grants its project to another organization. rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { @@ -4187,7 +4187,7 @@ service ManagementService { // Search Project Grants // - // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-list-project-grants.api.mdx) instead. + // Deprecated: use [ListProjectGrants](apis/resources/project_service_v2/zitadel-project-v-2-project-service-list-project-grants.api.mdx) instead. // // Returns a list of project grants. A project grant is when the organization grants its project to another organization. rpc ListAllProjectGrants(ListAllProjectGrantsRequest) returns (ListAllProjectGrantsResponse) { @@ -4216,7 +4216,7 @@ service ManagementService { // Add Project Grant // - // Deprecated: use [CreateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-create-project-grant.api.mdx) instead. + // Deprecated: use [CreateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-project-service-create-project-grant.api.mdx) instead. // // Grant a project to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc AddProjectGrant(AddProjectGrantRequest) returns (AddProjectGrantResponse) { @@ -4245,7 +4245,7 @@ service ManagementService { // Change Project Grant // - // Deprecated: use [UpdateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-update-project-grant.api.mdx) instead. + // Deprecated: use [UpdateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-project-service-update-project-grant.api.mdx) instead. // // Change the roles of the project that is granted to another organization. The project grant will allow the granted organization to access the project and manage the authorizations for its users. Project Grant will be listed in the granted project of the granted organization. rpc UpdateProjectGrant(UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { @@ -4274,7 +4274,7 @@ service ManagementService { // Deactivate Project Grant // - // Deprecated: use [DeactivateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-deactivate-project-grant.api.mdx) instead. + // Deprecated: use [DeactivateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-project-service-deactivate-project-grant.api.mdx) instead. // // Set the state of the project grant to deactivated. The grant has to be active to be able to deactivate. rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { @@ -4303,7 +4303,7 @@ service ManagementService { // Reactivate Project Grant // - // Deprecated: use [ActivateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-activate-project-grant.api.mdx) instead. + // Deprecated: use [ActivateProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-project-service-activate-project-grant.api.mdx) instead. // // Set the state of the project grant to active. The grant has to be deactivated to be able to reactivate. rpc ReactivateProjectGrant(ReactivateProjectGrantRequest) returns (ReactivateProjectGrantResponse) { @@ -4332,7 +4332,7 @@ service ManagementService { // Remove Project Grant // - // Deprecated: use [DeleteProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-beta-project-service-delete-project-grant.api.mdx) instead. + // Deprecated: use [DeleteProjectGrant](apis/resources/project_service_v2/zitadel-project-v-2-project-service-delete-project-grant.api.mdx) instead. // // Remove a project grant. All user grants for this project grant will also be removed. A user will not have access to the project afterward (if permissions are checked). rpc RemoveProjectGrant(RemoveProjectGrantRequest) returns (RemoveProjectGrantResponse) { diff --git a/proto/zitadel/project/v2/project_service.proto b/proto/zitadel/project/v2/project_service.proto new file mode 100644 index 00000000000..c33cc7b35d1 --- /dev/null +++ b/proto/zitadel/project/v2/project_service.proto @@ -0,0 +1,901 @@ +syntax = "proto3"; + +package zitadel.project.v2; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/struct.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +import "zitadel/protoc_gen_zitadel/v2/options.proto"; + +import "zitadel/project/v2/query.proto"; +import "google/protobuf/timestamp.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2;project"; + +// Service to manage projects. +service ProjectService { + + // Create Project + // + // Create a new project. A project is a vessel to group applications, roles and + // authorizations. Every project belongs to exactly one organization, but + // can be granted to other organizations for self-management of their authorizations. + // + // Required permission: + // - `project.create` + rpc CreateProject (CreateProjectRequest) returns (CreateProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Update Project + // + // Update an existing project. + // + // Required permission: + // - `project.write` + rpc UpdateProject (UpdateProjectRequest) returns (UpdateProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Delete Project + // + // Delete an existing project. + // In case the project is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.delete` + rpc DeleteProject (DeleteProjectRequest) returns (DeleteProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Get Project + // + // Returns the project identified by the requested ID. + // + // Required permission: + // - `project.read` + rpc GetProject (GetProjectRequest) returns (GetProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + } + + // List Projects + // + // List all matching projects. By default all projects of the instance that the caller + // has permission to read are returned. + // Make sure to include a limit and sorting for pagination. + // + // Required permission: + // - `project.read` + rpc ListProjects (ListProjectsRequest) returns (ListProjectsResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.read" + } + }; + } + + // Deactivate Project + // + // Set the state of a project to deactivated. Request returns no error if the project is already deactivated. + // Applications under deactivated projects are not able to login anymore. + // + // Required permission: + // - `project.write` + rpc DeactivateProject (DeactivateProjectRequest) returns (DeactivateProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Activate Project + // + // Set the state of a project to active. Request returns no error if the project is already activated. + // + // Required permission: + // - `project.write` + rpc ActivateProject (ActivateProjectRequest) returns (ActivateProjectResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Add Project Role + // + // Add a new project role to a project. The key must be unique within the project. + // + // Required permission: + // - `project.role.write` + rpc AddProjectRole (AddProjectRoleRequest) returns (AddProjectRoleResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Update Project Role + // + // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. + // + // Required permission: + // - `project.role.write` + rpc UpdateProjectRole (UpdateProjectRoleRequest) returns (UpdateProjectRoleResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Remove Project Role + // + // Removes the role from the project and on every resource it has a dependency. + // This includes project grants and user grants. + // + // Required permission: + // - `project.role.write` + rpc RemoveProjectRole (RemoveProjectRoleRequest) returns (RemoveProjectRoleResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Project Roles + // + // Returns all roles of a project matching the search query. + // + // Required permission: + // - `project.role.read` + rpc ListProjectRoles (ListProjectRolesRequest) returns (ListProjectRolesResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.role.read" + } + }; + } + + // Create Project Grant + // + // Grant a project to another organization. + // The project grant will allow the granted organization to access the project and manage + // the authorizations for its users. + // + // Required permission: + // - `project.grant.create` + rpc CreateProjectGrant (CreateProjectGrantRequest) returns (CreateProjectGrantResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Update Project Grant + // + // Change the roles of the project that is granted to another organization. + // The project grant will allow the granted organization to access the project and manage + // the authorizations for its users. + // + // Required permission: + // - `project.grant.write` + rpc UpdateProjectGrant (UpdateProjectGrantRequest) returns (UpdateProjectGrantResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Delete Project Grant + // + // Delete a project grant. All user grants for this project grant will also be removed. + // A user will not have access to the project afterward (if permissions are checked). + // In case the project grant is not found, the request will return a successful response as + // the desired state is already achieved. + // + // Required permission: + // - `project.grant.delete` + rpc DeleteProjectGrant (DeleteProjectGrantRequest) returns (DeleteProjectGrantResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Deactivate Project Grant + // + // Set the state of the project grant to deactivated. + // Applications under deactivated projects grants are not able to login anymore. + // + // Required permission: + // - `project.grant.write` + rpc DeactivateProjectGrant(DeactivateProjectGrantRequest) returns (DeactivateProjectGrantResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // Activate Project Grant + // + // Set the state of the project grant to activated. + // + // Required permission: + // - `project.grant.write` + rpc ActivateProjectGrant(ActivateProjectGrantRequest) returns (ActivateProjectGrantResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "authenticated" + } + }; + } + + // List Project Grants + // + // Returns a list of project grants. A project grant is when the organization grants its project + // to another organization. + // + // Required permission: + // - `project.grant.read` + rpc ListProjectGrants(ListProjectGrantsRequest) returns (ListProjectGrantsResponse) { + option (zitadel.protoc_gen_zitadel.v2.options) = { + auth_option: { + permission: "project.grant.read" + } + }; + } +} + +message CreateProjectRequest { + // OrganizationID is the unique identifier of the organization the project belongs to. + string organization_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + + // ProjectID is the unique identifier of the new project. This field is optional. + // If omitted, the system will generate a unique ID for you. This is the + // recommended way. The generated ID will be returned in the response. + optional string project_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + + // Name of the project. This might be presented to users, e.g. in sign-in flows. + string name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject\""; + } + ]; + + // ProjectRoleAssertion is a setting that can be enabled to have role information + // included in the user info endpoint. + // It is also dependent on your application settings to include it in tokens and other types. + bool project_role_assertion = 4; + + // AuthorizationRequired is a boolean flag that can be enabled to check if a user has + // an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 5; + + // ProjectAccessRequired is a boolean flag that can be enabled to check if the organization + // of the user, that is trying to log in, + // has access to this project (either owns the project or is granted). + bool project_access_required = 6; + + // PrivateLabelingSetting is a setting that defines which private labeling/branding should + // trigger when getting to a login of this project. + PrivateLabelingSetting private_labeling_setting = 7 [ + (validate.rules).enum = {defined_only: true} + ]; +} + +message CreateProjectResponse { + // ProjectID is the unique identifier of the newly created project. + string project_id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629012906488334\""; + } + ]; + + // CreationDate is the timestamp of the project creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRequest { + // ProjectID is the unique identifier of the project to be updated. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; + + // Name is used to update the name of the project. This field is optional. + // If omitted, the name will remain unchanged. + optional string name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"MyProject-Updated\""; + } + ]; + + // ProjectRoleAssertion is a setting that can be enabled to have role information + // included in the user info endpoint. + // It is also dependent on your application settings to include it in tokens and other types. + // If omitted, the setting will remain unchanged. + optional bool project_role_assertion = 3; + + // AuthorizationRequired is a boolean flag that can be enabled to check if a user has + // a role of this project assigned when logging into an application of this project. + // If omitted, the setting will remain unchanged. + optional bool authorization_required = 4; + + // ProjectAccessRequired is a boolean flag that can be enabled to check if the organization + // of the user has a grant to this project. + // If omitted, the setting will remain unchanged. + optional bool project_access_required = 5; + + // PrivateLabelingSetting is a setting that defines which private labeling/branding should + // trigger when getting to a login of this project. + // If omitted, the setting will remain unchanged. + optional PrivateLabelingSetting private_labeling_setting = 6 [ + (validate.rules).enum = {defined_only: true} + ]; +} + +message UpdateProjectResponse { + // ChangeDate is the timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectRequest { + // ProjectID is the unique identifier of the project to be deleted. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeleteProjectResponse { + // DeletionDate is the timestamp of the deletion of the project. + // 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 be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message GetProjectRequest { + // ProjectID is the unique identifier of the project to be retrieved. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message GetProjectResponse { + // Project is the project that matches the project ID used in the request. + Project project = 1; +} + +message ListProjectsRequest { + // Pagination can be used to list limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + + // SortingColumn is the field the result is sorted by. The default is the creation date. + // Beware that if you change this, your result pagination might be inconsistent. + optional ProjectFieldName sorting_column = 2; + + // Filters define the criteria to query for. + repeated ProjectSearchFilter filters = 3; +} + +message ListProjectsResponse { + // Pagination contains the total number of projects matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // Projects is a list of projects matching the query. + repeated Project projects = 2; +} + +message DeactivateProjectRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message DeactivateProjectResponse { + // ChangeDate is the timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1, + max_length: 200, + example: "\"69629026806489455\""; + } + ]; +} + +message ActivateProjectResponse { + // ChangeDate is the timestamp of the change of the project. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message AddProjectRoleRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // RoleKey identifies the role. It's the only relevant attribute for ZITADEL and + // will be used for authorization checks and as claim in tokens and user info responses. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + + // DisplayName is a human readable name for the role, which might be displayed to users. + string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + + // Group allows grouping roles for display purposes. Zitadel will not handle it in any way. + // It can be used to group roles in a UI to allow easier management for administrators. + // This attribute is not to be confused with groups as a collection of users. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message AddProjectRoleResponse { + // CreationDate is the timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 2 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectRoleRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // RoleKey identifies the role. It's the only relevant attribute for ZITADEL and + // will be used for authorization checks and as claim in tokens and user info responses. + // It cannot be changed. If you need a different key, remove the role and create a new one. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; + + // DisplayName is the human readable name for the role, which might be displayed to users. + // If omitted, the name will remain unchanged. + optional string display_name = 3 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Administrator\""; + } + ]; + + // Group allows grouping roles for display purposes. Zitadel will not handle it in any way. + // It can be used to group roles in a UI to allow easier management for administrators. + // If omitted, the group will remain unchanged. + // This attribute is not to be confused with groups as a collection of users. + optional string group = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Admins\""; + } + ]; +} + +message UpdateProjectRoleResponse { + // ChangeDate is the timestamp of the change of the project role. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message RemoveProjectRoleRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // RoleKey is the key of the role to be removed. + // All dependencies of this role will be removed as well, including project grants and user grants. + // If the role is not found, the request will return a successful response as the desired state is already achieved. + string role_key = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"ADMIN\""; + } + ]; +} + +message RemoveProjectRoleResponse { + // RemovalDate is the timestamp of the removal of the project role. + // Note that the removal date is only guaranteed to be set if the removal was successful during the request. + // In case the removal occurred in a previous request, the removal date might be empty. + google.protobuf.Timestamp removal_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectRolesRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // Pagination can be used to list limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 2; + + // SortingColumn is the field the result is sorted by. The default is the creation date. + // Beware that if you change this, your result pagination might be inconsistent. + optional ProjectRoleFieldName sorting_column = 3; + + // Filters define the criteria to query for. + repeated ProjectRoleSearchFilter filters = 4; +} + +message ListProjectRolesResponse { + // Pagination contains the total number of project roles matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // ProjectRoles is a list of roles matching the query. + repeated ProjectRole project_roles = 2; +} + + +message CreateProjectGrantRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // GrantedOrganizationID is the unique identifier of the organization the project will be granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + + // RoleKeys is a list of roles to be granted to the organization for self management. + // The roles are identified by their keys. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message CreateProjectGrantResponse { + // CreationDate is the timestamp of the project grant creation. + google.protobuf.Timestamp creation_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2024-12-18T07:50:47.492Z\""; + } + ]; +} + +message UpdateProjectGrantRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // GrantedOrganizationID is the unique identifier of the organization the project was granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; + + // RoleKeys is a list of roles to be granted to the organization for self management. + // The roles are identified by their keys. + // Any roles not included in this list will be removed from the project grant. + // If you want to add a role, make sure to include all other existing roles as well. + // If any previous role is removed, all user grants for this project grant with this role will be removed as well. + repeated string role_keys = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "[\"RoleKey1\", \"RoleKey2\"]"; + } + ]; +} + +message UpdateProjectGrantResponse { + // ChangeDate is the timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeleteProjectGrantRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // GrantedOrganizationID is the unique identifier of the organization the project was granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message DeleteProjectGrantResponse { + // DeletionDate is the timestamp of the deletion of the project grant. + // 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 be empty. + google.protobuf.Timestamp deletion_date = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message DeactivateProjectGrantRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // GrantedOrganizationID is the unique identifier of the organization the project was granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message DeactivateProjectGrantResponse { + // ChangeDate is the timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ActivateProjectGrantRequest { + // ProjectID is the unique identifier of the project. + string project_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + + // GrantedOrganizationID is the unique identifier of the organization the project was granted to. + string granted_organization_id = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"28746028909593987\"" + } + ]; +} + +message ActivateProjectGrantResponse { + // ChangeDate is the timestamp of the change of the project grant. + google.protobuf.Timestamp change_date = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2025-01-23T10:34:18.051Z\""; + } + ]; +} + +message ListProjectGrantsRequest { + // Pagination can be used to list limitations and ordering. + optional zitadel.filter.v2.PaginationRequest pagination = 1; + + // SortingColumn is the field the result is sorted by. The default is the creation date. + // Beware that if you change this, your result pagination might be inconsistent. + optional ProjectGrantFieldName sorting_column = 2; + + // Filters define the criteria to query for. + repeated ProjectGrantSearchFilter filters = 3; +} + +message ListProjectGrantsResponse { + // Pagination contains the total number of project grants matching the query and the applied limit. + zitadel.filter.v2.PaginationResponse pagination = 1; + + // ProjectGrants is a list of project grants matching the query. + repeated ProjectGrant project_grants = 2; +} \ No newline at end of file diff --git a/proto/zitadel/project/v2/query.proto b/proto/zitadel/project/v2/query.proto new file mode 100644 index 00000000000..b1bf3e21c42 --- /dev/null +++ b/proto/zitadel/project/v2/query.proto @@ -0,0 +1,265 @@ +syntax = "proto3"; + +package zitadel.project.v2; + +import "google/api/field_behavior.proto"; +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; +import "zitadel/filter/v2/filter.proto"; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2;project"; + +message ProjectGrant { + // The unique identifier of the organization which granted the project to the granted_organization_id. + string organization_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629012906488334\""}]; + + // The timestamp of the granted project creation. + google.protobuf.Timestamp creation_date = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}]; + + // The timestamp of the last change to the granted project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}]; + + // The ID of the organization the project is granted to. + string granted_organization_id = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""}]; + + // The name of the organization the project is granted to. + string granted_organization_name = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"Some Organization\""}]; + + // The roles granted to the organization for self-management of the project. + repeated string granted_role_keys = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "[\"role.super.man\"]"}]; + + // The ID of the granted project. + string project_id = 7 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""}]; + + // The name of the granted project. + string project_name = 8 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"ZITADEL\""}]; + + // Describes the current state of the granted project. + ProjectGrantState state = 9; +} + +enum ProjectGrantState { + PROJECT_GRANT_STATE_UNSPECIFIED = 0; + PROJECT_GRANT_STATE_ACTIVE = 1; + PROJECT_GRANT_STATE_INACTIVE = 2; +} + +message Project { + // ProjectID is the unique identifier of the project. + string project_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629012906488334\""}]; + + // OrganizationID is the unique identifier of the organization the project belongs to. + string organization_id = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629012906488334\""}]; + + // CreationDate is the timestamp of the project creation. + google.protobuf.Timestamp creation_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}]; + + // ChangeDate is the timestamp of the last change to the project (e.g. creation, activation, deactivation). + google.protobuf.Timestamp change_date = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}]; + + // Name is the name of the project. This might be presented to users, e.g. in sign-in flows. + string name = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"ip_allow_list\""}]; + + // State describes the current state of the project. + ProjectState state = 6; + + // ProjectRoleAssertion is a boolean flag that describes if the roles of the user should be added to the token. + bool project_role_assertion = 7; + + // AuthorizationRequired is a boolean flag that can be enabled to check if a user has + // an authorization to use this project assigned when login into an application of this project. + bool authorization_required = 8; + + // ProjectAccessRequired is a boolean flag that can be enabled to check if the organization of the user, + // that is trying to log in, has access to this project (either owns the project or is granted). + bool project_access_required = 9; + + // PrivateLabelingSetting defines from where the private labeling should be triggered. + PrivateLabelingSetting private_labeling_setting = 10; + + // GrantedOrganizationID is the ID of the organization the project is granted to. + // In case the project is not granted, this field is unset. + optional string granted_organization_id = 12 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629023906488334\""}]; + + // GrantedOrganizationName is the name of the organization the project is granted to. + // In case the project is not granted, this field is unset. + optional string granted_organization_name = 13 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"Some Organization\""}]; + + // GrantedProjectState describes the current state of the granted project. + // In case the project is not granted, this field is set to GrantedProjectState.GRANTED_PROJECT_STATE_UNSPECIFIED. + GrantedProjectState granted_state = 14; +} + +enum ProjectState { + PROJECT_STATE_UNSPECIFIED = 0; + PROJECT_STATE_ACTIVE = 1; + PROJECT_STATE_INACTIVE = 2; +} + +enum GrantedProjectState { + GRANTED_PROJECT_STATE_UNSPECIFIED = 0; + GRANTED_PROJECT_STATE_ACTIVE = 1; + GRANTED_PROJECT_STATE_INACTIVE = 2; +} + +enum PrivateLabelingSetting { + PRIVATE_LABELING_SETTING_UNSPECIFIED = 0; + PRIVATE_LABELING_SETTING_ENFORCE_PROJECT_RESOURCE_OWNER_POLICY = 1; + PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY = 2; +} + +enum ProjectFieldName { + PROJECT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_FIELD_NAME_ID = 1; + PROJECT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_FIELD_NAME_CHANGE_DATE = 3; + PROJECT_FIELD_NAME_NAME = 4; +} + +enum ProjectGrantFieldName { + PROJECT_GRANT_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_GRANT_FIELD_NAME_PROJECT_ID = 1; + PROJECT_GRANT_FIELD_NAME_CREATION_DATE = 2; + PROJECT_GRANT_FIELD_NAME_CHANGE_DATE = 3; +} + +enum ProjectRoleFieldName { + PROJECT_ROLE_FIELD_NAME_UNSPECIFIED = 0; + PROJECT_ROLE_FIELD_NAME_KEY = 1; + PROJECT_ROLE_FIELD_NAME_CREATION_DATE = 2; + PROJECT_ROLE_FIELD_NAME_CHANGE_DATE = 3; +} + +message ProjectSearchFilter { + oneof filter { + option (validate.required) = true; + + // Filter for projects with a specific name. + ProjectNameFilter project_name_filter = 1; + + // Filter for projects with a specific ID. + zitadel.filter.v2.InIDsFilter in_project_ids_filter = 2; + + // Filter for projects that are owned by or granted to a specific organization. + // You can specify whether to search for owned, granted or both types of projects. + ProjectOrganizationIDFilter organization_id_filter = 3; + } +} + +message ProjectNameFilter { + // Defines the name of the project to query for. + string project_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200 + example: "\"ip_allow_list\"" + } + ]; + + // Defines which text comparison method used for the name query. + zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ProjectOrganizationIDFilter { + // OrganizationID Is the ID of the organization to query for. + string organization_id = 1 [ + (validate.rules).string = {max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629012906488334\""} + ]; + + enum Type { + TYPE_UNSPECIFIED = 0; + // Filter for projects that are owned by the organization. + OWNED = 1; + // Filter for projects that are granted to the organization. + GRANTED = 2; + // Filter for projects that are either owned by or granted to the organization. + OWNED_OR_GRANTED = 3; + } + // Defines whether to filter for owned, granted or both types of projects. + // If not specified, defaults to OWNED_OR_GRANTED and will return both owned and granted projects. + Type type = 2 [ + (validate.rules).enum = {defined_only: true} + ]; +} + + +message ProjectGrantSearchFilter { + oneof filter { + option (validate.required) = true; + + // Filter for project grants with a specific project name. + ProjectNameFilter project_name_filter = 1; + + // Filter for project grants where a specific role is granted. + ProjectRoleKeyFilter role_key_filter = 2; + + // Filter for project grants of a specific project ID. + zitadel.filter.v2.InIDsFilter in_project_ids_filter = 3; + + // Filter for project grants that were granted from a specific organization. + zitadel.filter.v2.IDFilter organization_id_filter = 4; + + // Filter for project grants that were granted to a specific organization. + zitadel.filter.v2.IDFilter granted_organization_id_filter = 5; + } +} + +message ProjectRole { + // ProjectID is the ID of the project the role belongs to. + string project_id = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"69629026806489455\""}]; + + // Key of the project role. The key identifies the role. It's the only relevant attribute for ZITADEL and + // will be used for authorization checks and as claim in tokens and user info responses. + string key = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"role.super.man\""}]; + + // CreationDate is the timestamp of the project role creation. + google.protobuf.Timestamp creation_date = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2024-12-18T07:50:47.492Z\""}]; + + // ChangeDate is the timestamp of the last change to the project role. + google.protobuf.Timestamp change_date = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"2025-01-23T10:34:18.051Z\""}]; + + // DisplayName is a human readable name for the role, which might be displayed to users. + string display_name = 5 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"Super man\""}]; + + // Group allows grouping roles for display purposes. Zitadel will not handle it in any way. + // It can be used to group roles in a UI to allow easier management for administrators. + // This attribute is not to be confused with groups as a collection of users. + string group = 6 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"people\""}]; +} + +message ProjectRoleSearchFilter { + oneof filter { + option (validate.required) = true; + + // Filter for project roles with a specific key. + ProjectRoleKeyFilter role_key_filter = 1; + + // Filter for project roles with a specific display name. + ProjectRoleDisplayNameFilter display_name_filter = 2; + } +} + +message ProjectRoleKeyFilter { + // The key of the project role to query for. + string key = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"role.super.man\""} + ]; + + // Defines which text comparison method used for the key query. + zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} + +message ProjectRoleDisplayNameFilter { + // The display name of the project role to query for. + string display_name = 1 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {example: "\"SUPER\""} + ]; + + // Defines which text comparison method used for the name query. + zitadel.filter.v2.TextFilterMethod method = 2 [(validate.rules).enum.defined_only = true]; +} diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto index 7d77ed2e403..9a4742aa452 100644 --- a/proto/zitadel/project/v2beta/project_service.proto +++ b/proto/zitadel/project/v2beta/project_service.proto @@ -108,10 +108,14 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { }; // Service to manage projects. +// +// Deprecated: use project service v2 instead. This service will be removed in the next major version of ZITADEL. service ProjectService { // Create Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Create a new Project. // // Required permission: @@ -129,6 +133,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -146,6 +151,8 @@ service ProjectService { // Update Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Update an existing project. // // Required permission: @@ -163,6 +170,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -180,6 +188,8 @@ service ProjectService { // Delete Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Delete an existing project. // In case the project is not found, the request will return a successful response as // the desired state is already achieved. @@ -198,6 +208,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -209,6 +220,8 @@ service ProjectService { // Get Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Returns the project identified by the requested ID. // // Required permission: @@ -225,6 +238,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200" value: { @@ -242,6 +256,8 @@ service ProjectService { // List Projects // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // List all matching projects. By default all projects of the instance that the caller has permission to read are returned. // Make sure to include a limit and sorting for pagination. // @@ -260,6 +276,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -277,6 +294,8 @@ service ProjectService { // Deactivate Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Set the state of a project to deactivated. Request returns no error if the project is already deactivated. // Applications under deactivated projects are not able to login anymore. // @@ -295,6 +314,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -312,6 +332,8 @@ service ProjectService { // Activate Project // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Set the state of a project to active. Request returns no error if the project is already activated. // // Required permission: @@ -329,6 +351,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -346,6 +369,8 @@ service ProjectService { // Add Project Role // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Add a new project role to a project. The key must be unique within the project. // // Required permission: @@ -363,6 +388,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -380,6 +406,8 @@ service ProjectService { // Update Project Role // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Change a project role. The key is not editable. If a key should change, remove the role and create a new one. // // Required permission: @@ -397,6 +425,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -414,6 +443,8 @@ service ProjectService { // Remove Project Role // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Removes the role from the project and on every resource it has a dependency. This includes project grants and user grants. // // Required permission: @@ -430,6 +461,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -447,6 +479,8 @@ service ProjectService { // List Project Roles // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Returns all roles of a project matching the search query. // // Required permission: @@ -463,6 +497,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -480,6 +515,8 @@ service ProjectService { // Create Project Grant // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Grant a project to another organization. // The project grant will allow the granted organization to access the project and manage the authorizations for its users. // @@ -498,6 +535,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -515,6 +553,8 @@ service ProjectService { // Update Project Grant // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Change the roles of the project that is granted to another organization. // The project grant will allow the granted organization to access the project and manage the authorizations for its users. // @@ -533,6 +573,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -550,6 +591,8 @@ service ProjectService { // Delete Project Grant // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Delete a project grant. All user grants for this project grant will also be removed. // A user will not have access to the project afterward (if permissions are checked). // In case the project grant is not found, the request will return a successful response as @@ -569,6 +612,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -580,6 +624,8 @@ service ProjectService { // Deactivate Project Grant // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Set the state of the project grant to deactivated. // Applications under deactivated projects grants are not able to login anymore. // @@ -598,6 +644,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -609,6 +656,8 @@ service ProjectService { // Activate Project Grant // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Set the state of the project grant to activated. // // Required permission: @@ -626,6 +675,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: { @@ -637,6 +687,8 @@ service ProjectService { // List Project Grants // + // Deprecated: please move to the corresponding endpoint under project service v2. This endpoint will be removed with the next major version of ZITADEL. + // // Returns a list of project grants. A project grant is when the organization grants its project to another organization. // // Required permission: @@ -654,6 +706,7 @@ service ProjectService { }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + deprecated: true; responses: { key: "200"; value: {