mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 19:07:30 +00:00
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:
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/feature"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
@@ -17,67 +19,60 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func (c *Commands) AddProjectWithID(ctx context.Context, project *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
if resourceOwner == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-w8tnSoJxtn", "Errors.ResourceOwnerMissing")
|
||||
}
|
||||
if projectID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nDXf5vXoUj", "Errors.IDMissing")
|
||||
}
|
||||
if !project.IsValid() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid")
|
||||
}
|
||||
project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return project, nil
|
||||
type AddProject struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Name string
|
||||
ProjectRoleAssertion bool
|
||||
ProjectRoleCheck bool
|
||||
HasProjectCheck bool
|
||||
PrivateLabelingSetting domain.PrivateLabelingSetting
|
||||
}
|
||||
|
||||
func (c *Commands) AddProject(ctx context.Context, project *domain.Project, resourceOwner string) (_ *domain.Project, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
if !project.IsValid() {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid")
|
||||
func (p *AddProject) IsValid() error {
|
||||
if p.ResourceOwner == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing")
|
||||
}
|
||||
if resourceOwner == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-fmq7bqQX1s", "Errors.ResourceOwnerMissing")
|
||||
if p.Name == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "PROJECT-IOVCC", "Errors.Project.Invalid")
|
||||
}
|
||||
|
||||
projectID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project, err = c.addProjectWithID(ctx, project, resourceOwner, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return project, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Project, resourceOwner, projectID string) (_ *domain.Project, err error) {
|
||||
projectAdd.AggregateID = projectID
|
||||
projectWriteModel, err := c.getProjectWriteModelByID(ctx, projectAdd.AggregateID, resourceOwner)
|
||||
func (c *Commands) AddProject(ctx context.Context, add *AddProject) (_ *domain.ObjectDetails, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if err := add.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if add.AggregateID == "" {
|
||||
add.AggregateID, err = c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
wm, err := c.getProjectWriteModelByID(ctx, add.AggregateID, add.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isProjectStateExists(projectWriteModel.State) {
|
||||
if isProjectStateExists(wm.State) {
|
||||
return nil, zerrors.ThrowAlreadyExists(nil, "COMMAND-opamwu", "Errors.Project.AlreadyExisting")
|
||||
}
|
||||
if err := c.checkPermissionUpdateProject(ctx, wm.ResourceOwner, wm.AggregateID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []eventstore.Command{
|
||||
project.NewProjectAddedEvent(
|
||||
ctx,
|
||||
//nolint: contextcheck
|
||||
ProjectAggregateFromWriteModel(&projectWriteModel.WriteModel),
|
||||
projectAdd.Name,
|
||||
projectAdd.ProjectRoleAssertion,
|
||||
projectAdd.ProjectRoleCheck,
|
||||
projectAdd.HasProjectCheck,
|
||||
projectAdd.PrivateLabelingSetting),
|
||||
ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel),
|
||||
add.Name,
|
||||
add.ProjectRoleAssertion,
|
||||
add.ProjectRoleCheck,
|
||||
add.HasProjectCheck,
|
||||
add.PrivateLabelingSetting),
|
||||
}
|
||||
postCommit, err := c.projectCreatedMilestone(ctx, &events)
|
||||
if err != nil {
|
||||
@@ -88,11 +83,11 @@ func (c *Commands) addProjectWithID(ctx context.Context, projectAdd *domain.Proj
|
||||
return nil, err
|
||||
}
|
||||
postCommit(ctx)
|
||||
err = AppendAndReduce(projectWriteModel, pushedEvents...)
|
||||
err = AppendAndReduce(wm, pushedEvents...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return projectWriteModelToProject(projectWriteModel), nil
|
||||
return writeModelToObjectDetails(&wm.WriteModel), nil
|
||||
}
|
||||
|
||||
func AddProjectCommand(
|
||||
@@ -143,14 +138,14 @@ func projectWriteModel(ctx context.Context, filter preparation.FilterToQueryRedu
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resourceOwner string) (*eventstore.Aggregate, domain.ProjectState, error) {
|
||||
result, err := c.projectState(ctx, projectID, resourceOwner)
|
||||
func (c *Commands) projectAggregateByID(ctx context.Context, projectID string) (*eventstore.Aggregate, domain.ProjectState, error) {
|
||||
result, err := c.projectState(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-NDQoF", "Errors.Project.NotFound")
|
||||
}
|
||||
if len(result) == 0 {
|
||||
_ = projection.ProjectGrantFields.Trigger(ctx)
|
||||
result, err = c.projectState(ctx, projectID, resourceOwner)
|
||||
result, err = c.projectState(ctx, projectID)
|
||||
if err != nil || len(result) == 0 {
|
||||
return nil, domain.ProjectStateUnspecified, zerrors.ThrowNotFound(err, "COMMA-U1nza", "Errors.Project.NotFound")
|
||||
}
|
||||
@@ -164,7 +159,7 @@ func (c *Commands) projectAggregateByID(ctx context.Context, projectID, resource
|
||||
return &result[0].Aggregate, state, nil
|
||||
}
|
||||
|
||||
func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner string) ([]*eventstore.SearchResult, error) {
|
||||
func (c *Commands) projectState(ctx context.Context, projectID string) ([]*eventstore.SearchResult, error) {
|
||||
return c.eventstore.Search(
|
||||
ctx,
|
||||
map[eventstore.FieldType]any{
|
||||
@@ -172,12 +167,11 @@ func (c *Commands) projectState(ctx context.Context, projectID, resourceOwner st
|
||||
eventstore.FieldTypeObjectID: projectID,
|
||||
eventstore.FieldTypeObjectRevision: project.ProjectObjectRevision,
|
||||
eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
|
||||
eventstore.FieldTypeResourceOwner: resourceOwner,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (err error) {
|
||||
func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOwner string) (_ string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@@ -185,50 +179,69 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw
|
||||
return c.checkProjectExistsOld(ctx, projectID, resourceOwner)
|
||||
}
|
||||
|
||||
_, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
|
||||
agg, state, err := c.projectAggregateByID(ctx, projectID)
|
||||
if err != nil || !state.Valid() {
|
||||
return zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound")
|
||||
return "", zerrors.ThrowPreconditionFailed(err, "COMMA-VCnwD", "Errors.Project.NotFound")
|
||||
}
|
||||
return agg.ResourceOwner, nil
|
||||
}
|
||||
|
||||
type ChangeProject struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Name *string
|
||||
ProjectRoleAssertion *bool
|
||||
ProjectRoleCheck *bool
|
||||
HasProjectCheck *bool
|
||||
PrivateLabelingSetting *domain.PrivateLabelingSetting
|
||||
}
|
||||
|
||||
func (p *ChangeProject) IsValid() error {
|
||||
if p.AggregateID == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid")
|
||||
}
|
||||
if p.Name != nil && *p.Name == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Commands) ChangeProject(ctx context.Context, projectChange *domain.Project, resourceOwner string) (*domain.Project, error) {
|
||||
if !projectChange.IsValid() || projectChange.AggregateID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-4m9vS", "Errors.Project.Invalid")
|
||||
func (c *Commands) ChangeProject(ctx context.Context, change *ChangeProject) (_ *domain.ObjectDetails, err error) {
|
||||
if err := change.IsValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingProject, err := c.getProjectWriteModelByID(ctx, projectChange.AggregateID, resourceOwner)
|
||||
existing, err := c.getProjectWriteModelByID(ctx, change.AggregateID, change.ResourceOwner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isProjectStateExists(existingProject.State) {
|
||||
if !isProjectStateExists(existing.State) {
|
||||
return nil, zerrors.ThrowNotFound(nil, "COMMAND-3M9sd", "Errors.Project.NotFound")
|
||||
}
|
||||
if err := c.checkPermissionUpdateProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
|
||||
changedEvent, hasChanged, err := existingProject.NewChangedEvent(
|
||||
changedEvent := existing.NewChangedEvent(
|
||||
ctx,
|
||||
projectAgg,
|
||||
projectChange.Name,
|
||||
projectChange.ProjectRoleAssertion,
|
||||
projectChange.ProjectRoleCheck,
|
||||
projectChange.HasProjectCheck,
|
||||
projectChange.PrivateLabelingSetting)
|
||||
ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel),
|
||||
change.Name,
|
||||
change.ProjectRoleAssertion,
|
||||
change.ProjectRoleCheck,
|
||||
change.HasProjectCheck,
|
||||
change.PrivateLabelingSetting)
|
||||
if changedEvent == nil {
|
||||
return writeModelToObjectDetails(&existing.WriteModel), nil
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, existing, changedEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasChanged {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-2M0fs", "Errors.NoChangesFound")
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, existingProject, changedEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return projectWriteModelToProject(existingProject), nil
|
||||
return writeModelToObjectDetails(&existing.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) DeactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if projectID == "" || resourceOwner == "" {
|
||||
if projectID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-88iF0", "Errors.Project.ProjectIDMissing")
|
||||
}
|
||||
|
||||
@@ -236,7 +249,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso
|
||||
return c.deactivateProjectOld(ctx, projectID, resourceOwner)
|
||||
}
|
||||
|
||||
projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
|
||||
projectAgg, state, err := c.projectAggregateByID(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -247,6 +260,9 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso
|
||||
if state != domain.ProjectStateActive {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-mki55", "Errors.Project.NotActive")
|
||||
}
|
||||
if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectDeactivatedEvent(ctx, projectAgg))
|
||||
if err != nil {
|
||||
@@ -261,7 +277,7 @@ func (c *Commands) DeactivateProject(ctx context.Context, projectID string, reso
|
||||
}
|
||||
|
||||
func (c *Commands) ReactivateProject(ctx context.Context, projectID string, resourceOwner string) (*domain.ObjectDetails, error) {
|
||||
if projectID == "" || resourceOwner == "" {
|
||||
if projectID == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-3ihsF", "Errors.Project.ProjectIDMissing")
|
||||
}
|
||||
|
||||
@@ -269,7 +285,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso
|
||||
return c.reactivateProjectOld(ctx, projectID, resourceOwner)
|
||||
}
|
||||
|
||||
projectAgg, state, err := c.projectAggregateByID(ctx, projectID, resourceOwner)
|
||||
projectAgg, state, err := c.projectAggregateByID(ctx, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -280,6 +296,9 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso
|
||||
if state != domain.ProjectStateInactive {
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5M9bs", "Errors.Project.NotInactive")
|
||||
}
|
||||
if err := c.checkPermissionUpdateProject(ctx, projectAgg.ResourceOwner, projectAgg.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pushedEvents, err := c.eventstore.Push(ctx, project.NewProjectReactivatedEvent(ctx, projectAgg))
|
||||
if err != nil {
|
||||
@@ -293,6 +312,7 @@ func (c *Commands) ReactivateProject(ctx context.Context, projectID string, reso
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deprecated: use commands.DeleteProject
|
||||
func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner string, cascadingUserGrantIDs ...string) (*domain.ObjectDetails, error) {
|
||||
if projectID == "" || resourceOwner == "" {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-66hM9", "Errors.Project.ProjectIDMissing")
|
||||
@@ -316,15 +336,18 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
|
||||
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
|
||||
}
|
||||
|
||||
projectAgg := ProjectAggregateFromWriteModel(&existingProject.WriteModel)
|
||||
events := []eventstore.Command{
|
||||
project.NewProjectRemovedEvent(ctx, projectAgg, existingProject.Name, uniqueConstraints),
|
||||
project.NewProjectRemovedEvent(ctx,
|
||||
ProjectAggregateFromWriteModelWithCTX(ctx, &existingProject.WriteModel),
|
||||
existingProject.Name,
|
||||
uniqueConstraints,
|
||||
),
|
||||
}
|
||||
|
||||
for _, grantID := range cascadingUserGrantIDs {
|
||||
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
|
||||
if err != nil {
|
||||
logging.LogWithFields("COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
|
||||
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
@@ -341,6 +364,53 @@ func (c *Commands) RemoveProject(ctx context.Context, projectID, resourceOwner s
|
||||
return writeModelToObjectDetails(&existingProject.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) DeleteProject(ctx context.Context, id, resourceOwner string, cascadingUserGrantIDs ...string) (time.Time, error) {
|
||||
if id == "" {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "COMMAND-obqos2l3no", "Errors.IDMissing")
|
||||
}
|
||||
|
||||
existing, err := c.getProjectWriteModelByID(ctx, id, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if !isProjectStateExists(existing.State) {
|
||||
return existing.WriteModel.ChangeDate, nil
|
||||
}
|
||||
if err := c.checkPermissionDeleteProject(ctx, existing.ResourceOwner, existing.AggregateID); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
samlEntityIDsAgg, err := c.getSAMLEntityIdsWriteModelByProjectID(ctx, id, resourceOwner)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
uniqueConstraints := make([]*eventstore.UniqueConstraint, len(samlEntityIDsAgg.EntityIDs))
|
||||
for i, entityID := range samlEntityIDsAgg.EntityIDs {
|
||||
uniqueConstraints[i] = project.NewRemoveSAMLConfigEntityIDUniqueConstraint(entityID.EntityID)
|
||||
}
|
||||
events := []eventstore.Command{
|
||||
project.NewProjectRemovedEvent(ctx,
|
||||
ProjectAggregateFromWriteModelWithCTX(ctx, &existing.WriteModel),
|
||||
existing.Name,
|
||||
uniqueConstraints,
|
||||
),
|
||||
}
|
||||
for _, grantID := range cascadingUserGrantIDs {
|
||||
event, _, err := c.removeUserGrant(ctx, grantID, "", true)
|
||||
if err != nil {
|
||||
logging.WithFields("id", "COMMAND-b8Djf", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
|
||||
continue
|
||||
}
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
if err := c.pushAppendAndReduce(ctx, existing, events...); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return existing.WriteModel.ChangeDate, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getProjectWriteModelByID(ctx context.Context, projectID, resourceOwner string) (_ *ProjectWriteModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
Reference in New Issue
Block a user