feat: project v2beta resource API (#9742)

# Which Problems Are Solved

Resource management of projects and sub-resources was before limited by
the context provided by the management API, which would mean you could
only manage resources belonging to a specific organization.

# How the Problems Are Solved

With the addition of a resource-based API, it is now possible to manage
projects and sub-resources on the basis of the resources themselves,
which means that as long as you have the permission for the resource,
you can create, read, update and delete it.

- CreateProject to create a project under an organization
- UpdateProject to update an existing project
- DeleteProject to delete an existing project
- DeactivateProject and ActivateProject to change the status of a
project
- GetProject to query for a specific project with an identifier
- ListProject to query for projects and granted projects
- CreateProjectGrant to create a project grant with project and granted
organization
- UpdateProjectGrant to update the roles of a project grant
- DeactivateProjectGrant and ActivateProjectGrant to change the status
of a project grant
- DeleteProjectGrant to delete an existing project grant
- ListProjectGrants to query for project grants
- AddProjectRole to add a role to an existing project
- UpdateProjectRole to change texts of an existing role
- RemoveProjectRole to remove an existing role
- ListProjectRoles to query for project roles

# Additional Changes

- Changes to ListProjects, which now contains granted projects as well
- Changes to messages as defined in the
[API_DESIGN](https://github.com/zitadel/zitadel/blob/main/API_DESIGN.md)
- Permission checks for project functionality on query and command side
- Added testing to unit tests on command side
- Change update endpoints to no error returns if nothing changes in the
resource
- Changed all integration test utility to the new service
- ListProjects now also correctly lists `granted projects`
- Permission checks for project grant and project role functionality on
query and command side
- Change existing pre checks so that they also work resource specific
without resourceowner
- Added the resourceowner to the grant and role if no resourceowner is
provided
- Corrected import tests with project grants and roles
- Added testing to unit tests on command side
- Change update endpoints to no error returns if nothing changes in the
resource
- Changed all integration test utility to the new service
- Corrected some naming in the proto files to adhere to the API_DESIGN

# Additional Context

Closes #9177

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Stefan Benz
2025-05-21 14:40:47 +02:00
committed by GitHub
parent 6889d6a1da
commit 7eb45c6cfd
64 changed files with 9821 additions and 1037 deletions

View File

@@ -34,6 +34,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_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"
saml_pb "github.com/zitadel/zitadel/pkg/grpc/saml/v2"
@@ -71,6 +72,7 @@ type Client struct {
UserV3Alpha user_v3alpha.ZITADELUsersClient
SAMLv2 saml_pb.SAMLServiceClient
SCIM *scim.Client
Projectv2Beta project_v2beta.ProjectServiceClient
InstanceV2Beta instance.InstanceServiceClient
}
@@ -109,6 +111,7 @@ func newClient(ctx context.Context, target string) (*Client, error) {
UserV3Alpha: user_v3alpha.NewZITADELUsersClient(cc),
SAMLv2: saml_pb.NewSAMLServiceClient(cc),
SCIM: scim.NewScimClient(target),
Projectv2Beta: project_v2beta.NewProjectServiceClient(cc),
InstanceV2Beta: instance.NewInstanceServiceClient(cc),
}
return client, client.pollHealth(ctx)
@@ -446,6 +449,70 @@ func (i *Instance) SetUserPassword(ctx context.Context, userID, password string,
return resp.GetDetails()
}
func (i *Instance) CreateProject(ctx context.Context, t *testing.T, orgID, name string, projectRoleCheck, hasProjectCheck bool) *project_v2beta.CreateProjectResponse {
if orgID == "" {
orgID = i.DefaultOrg.GetId()
}
resp, err := i.Client.Projectv2Beta.CreateProject(ctx, &project_v2beta.CreateProjectRequest{
OrganizationId: orgID,
Name: name,
AuthorizationRequired: projectRoleCheck,
ProjectAccessRequired: hasProjectCheck,
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeleteProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeleteProjectResponse {
resp, err := i.Client.Projectv2Beta.DeleteProject(ctx, &project_v2beta.DeleteProjectRequest{
Id: projectID,
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeactivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.DeactivateProjectResponse {
resp, err := i.Client.Projectv2Beta.DeactivateProject(ctx, &project_v2beta.DeactivateProjectRequest{
Id: projectID,
})
require.NoError(t, err)
return resp
}
func (i *Instance) ActivateProject(ctx context.Context, t *testing.T, projectID string) *project_v2beta.ActivateProjectResponse {
resp, err := i.Client.Projectv2Beta.ActivateProject(ctx, &project_v2beta.ActivateProjectRequest{
Id: projectID,
})
require.NoError(t, err)
return resp
}
func (i *Instance) AddProjectRole(ctx context.Context, t *testing.T, projectID, roleKey, displayName, group string) *project_v2beta.AddProjectRoleResponse {
var groupP *string
if group != "" {
groupP = &group
}
resp, err := i.Client.Projectv2Beta.AddProjectRole(ctx, &project_v2beta.AddProjectRoleRequest{
ProjectId: projectID,
RoleKey: roleKey,
DisplayName: displayName,
Group: groupP,
})
require.NoError(t, err)
return resp
}
func (i *Instance) RemoveProjectRole(ctx context.Context, t *testing.T, projectID, roleKey string) *project_v2beta.RemoveProjectRoleResponse {
resp, err := i.Client.Projectv2Beta.RemoveProjectRole(ctx, &project_v2beta.RemoveProjectRoleRequest{
ProjectId: projectID,
RoleKey: roleKey,
})
require.NoError(t, err)
return resp
}
func (i *Instance) AddProviderToDefaultLoginPolicy(ctx context.Context, id string) {
_, err := i.Client.Admin.AddIDPToLoginPolicy(ctx, &admin.AddIDPToLoginPolicyRequest{
IdpId: id,
@@ -705,12 +772,40 @@ func (i *Instance) CreateIntentSession(t *testing.T, ctx context.Context, userID
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
}
func (i *Instance) CreateProjectGrant(ctx context.Context, projectID, grantedOrgID string) *mgmt.AddProjectGrantResponse {
resp, err := i.Client.Mgmt.AddProjectGrant(ctx, &mgmt.AddProjectGrantRequest{
GrantedOrgId: grantedOrgID,
ProjectId: projectID,
func (i *Instance) CreateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string, roles ...string) *project_v2beta.CreateProjectGrantResponse {
resp, err := i.Client.Projectv2Beta.CreateProjectGrant(ctx, &project_v2beta.CreateProjectGrantRequest{
GrantedOrganizationId: grantedOrgID,
ProjectId: projectID,
RoleKeys: roles,
})
logging.OnError(err).Panic("create project grant")
require.NoError(t, err)
return resp
}
func (i *Instance) DeleteProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeleteProjectGrantResponse {
resp, err := i.Client.Projectv2Beta.DeleteProjectGrant(ctx, &project_v2beta.DeleteProjectGrantRequest{
GrantedOrganizationId: grantedOrgID,
ProjectId: projectID,
})
require.NoError(t, err)
return resp
}
func (i *Instance) DeactivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.DeactivateProjectGrantResponse {
resp, err := i.Client.Projectv2Beta.DeactivateProjectGrant(ctx, &project_v2beta.DeactivateProjectGrantRequest{
ProjectId: projectID,
GrantedOrganizationId: grantedOrgID,
})
require.NoError(t, err)
return resp
}
func (i *Instance) ActivateProjectGrant(ctx context.Context, t *testing.T, projectID, grantedOrgID string) *project_v2beta.ActivateProjectGrantResponse {
resp, err := i.Client.Projectv2Beta.ActivateProjectGrant(ctx, &project_v2beta.ActivateProjectGrantRequest{
ProjectId: projectID,
GrantedOrganizationId: grantedOrgID,
})
require.NoError(t, err)
return resp
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"strings"
"testing"
"time"
"github.com/brianvoe/gofakeit/v6"
@@ -133,13 +134,9 @@ func (i *Instance) CreateOIDCInactivateProjectClient(ctx context.Context, redire
return client, err
}
func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) {
project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, err
}
func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, t *testing.T, redirectURI string, loginVersion *app.LoginVersion) (*management.AddOIDCAppResponse, error) {
project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false)
resp, err := i.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
ProjectId: project.GetId(),
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
@@ -178,30 +175,11 @@ func (i *Instance) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI
})
}
func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
project, err := i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, nil, err
}
func (i *Instance) CreateOIDCTokenExchangeClient(ctx context.Context, t *testing.T) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
project := i.CreateProject(ctx, t, "", gofakeit.AppName(), false, false)
return i.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN)
}
func (i *Instance) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
}
func (i *Instance) CreateProjectWithPermissionCheck(ctx context.Context, projectRoleCheck, hasProjectCheck bool) (*management.AddProjectResponse, error) {
return i.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
HasProjectCheck: hasProjectCheck,
ProjectRoleCheck: projectRoleCheck,
})
}
func (i *Instance) CreateAPIClientJWT(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
return i.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
ProjectId: projectID,