mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-16 20:28:41 +00:00

# Which Problems Are Solved Permission checks in project v2beta API did not cover projects and granted projects correctly. # How the Problems Are Solved Add permission checks v1 correctly to the list queries, add correct permission checks v2 for projects. # Additional Changes Correct Pre-Checks for project grants that the right resource owner is used. # Additional Context Permission checks v2 for project grants is still outstanding under #9972.
473 lines
17 KiB
Go
473 lines
17 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
|
"github.com/zitadel/zitadel/internal/feature"
|
|
"github.com/zitadel/zitadel/internal/repository/org"
|
|
"github.com/zitadel/zitadel/internal/repository/project"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type AddProjectGrant struct {
|
|
es_models.ObjectRoot
|
|
|
|
GrantID string
|
|
GrantedOrgID string
|
|
RoleKeys []string
|
|
}
|
|
|
|
func (p *AddProjectGrant) IsValid() error {
|
|
if p.AggregateID == "" {
|
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-FYRnWEzBzV", "Errors.Project.Grant.Invalid")
|
|
}
|
|
if p.GrantedOrgID == "" {
|
|
return zerrors.ThrowInvalidArgument(nil, "COMMAND-PPhHpWGRAE", "Errors.Project.Grant.Invalid")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Commands) AddProjectGrant(ctx context.Context, grant *AddProjectGrant) (_ *domain.ObjectDetails, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
if err := grant.IsValid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if grant.GrantID == "" {
|
|
grant.GrantID, err = c.idGenerator.Next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, grant.AggregateID, grant.GrantedOrgID, grant.ResourceOwner, grant.RoleKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if there is no resourceowner provided then use the resourceowner of the project
|
|
if grant.ResourceOwner == "" {
|
|
grant.ResourceOwner = projectResourceOwner
|
|
}
|
|
if err := c.checkPermissionUpdateProjectGrant(ctx, grant.ResourceOwner, grant.AggregateID, grant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
wm := NewProjectGrantWriteModel(grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner)
|
|
// error if provided resourceowner is not equal to the resourceowner of the project or the project grant is for the same organization
|
|
if projectResourceOwner != wm.ResourceOwner || wm.ResourceOwner == grant.GrantedOrgID {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-ckUpbvboAH", "Errors.Project.Grant.Invalid")
|
|
}
|
|
if err := c.pushAppendAndReduce(ctx,
|
|
wm,
|
|
project.NewGrantAddedEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &wm.WriteModel),
|
|
grant.GrantID,
|
|
grant.GrantedOrgID,
|
|
grant.RoleKeys),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&wm.WriteModel), nil
|
|
}
|
|
|
|
type ChangeProjectGrant struct {
|
|
es_models.ObjectRoot
|
|
|
|
GrantID string
|
|
GrantedOrgID string
|
|
RoleKeys []string
|
|
}
|
|
|
|
func (c *Commands) ChangeProjectGrant(ctx context.Context, grant *ChangeProjectGrant, cascadeUserGrantIDs ...string) (_ *domain.ObjectDetails, err error) {
|
|
if grant.GrantID == "" && grant.GrantedOrgID == "" {
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "PROJECT-1j83s", "Errors.IDMissing")
|
|
}
|
|
existingGrant, err := c.projectGrantWriteModelByID(ctx, grant.GrantID, grant.GrantedOrgID, grant.AggregateID, grant.ResourceOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !existingGrant.State.Exists() {
|
|
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
|
|
}
|
|
|
|
if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
projectResourceOwner, err := c.checkProjectGrantPreCondition(ctx, existingGrant.AggregateID, existingGrant.GrantedOrgID, existingGrant.ResourceOwner, grant.RoleKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// error if provided resourceowner is not equal to the resourceowner of the project
|
|
if existingGrant.ResourceOwner != projectResourceOwner {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-q1BhA68RBC", "Errors.Project.Grant.Invalid")
|
|
}
|
|
|
|
// return if there are no changes to the project grant roles
|
|
if reflect.DeepEqual(existingGrant.RoleKeys, grant.RoleKeys) {
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
events := []eventstore.Command{
|
|
project.NewGrantChangedEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
|
|
existingGrant.GrantID,
|
|
grant.RoleKeys,
|
|
),
|
|
}
|
|
|
|
removedRoles := domain.GetRemovedRoles(existingGrant.RoleKeys, grant.RoleKeys)
|
|
if len(removedRoles) == 0 {
|
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
for _, userGrantID := range cascadeUserGrantIDs {
|
|
event, err := c.removeRoleFromUserGrant(ctx, userGrantID, removedRoles, true)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) removeRoleFromProjectGrant(ctx context.Context, projectAgg *eventstore.Aggregate, projectID, projectGrantID, roleKey string, cascade bool) (_ eventstore.Command, _ *ProjectGrantWriteModel, err error) {
|
|
existingProjectGrant, err := c.projectGrantWriteModelByID(ctx, projectGrantID, "", projectID, "")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !existingProjectGrant.State.Exists() {
|
|
return nil, nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
|
|
}
|
|
keyExists := false
|
|
for i, key := range existingProjectGrant.RoleKeys {
|
|
if key == roleKey {
|
|
keyExists = true
|
|
copy(existingProjectGrant.RoleKeys[i:], existingProjectGrant.RoleKeys[i+1:])
|
|
existingProjectGrant.RoleKeys[len(existingProjectGrant.RoleKeys)-1] = ""
|
|
existingProjectGrant.RoleKeys = existingProjectGrant.RoleKeys[:len(existingProjectGrant.RoleKeys)-1]
|
|
continue
|
|
}
|
|
}
|
|
if !keyExists {
|
|
return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-5m8g9", "Errors.Project.Grant.RoleKeyNotFound")
|
|
}
|
|
changedProjectGrant := NewProjectGrantWriteModel(projectGrantID, projectID, "", existingProjectGrant.ResourceOwner)
|
|
|
|
if cascade {
|
|
return project.NewGrantCascadeChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil
|
|
}
|
|
|
|
return project.NewGrantChangedEvent(ctx, projectAgg, projectGrantID, existingProjectGrant.RoleKeys), changedProjectGrant, nil
|
|
}
|
|
|
|
func (c *Commands) DeactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) {
|
|
if (grantID == "" && grantedOrgID == "") || projectID == "" {
|
|
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
|
|
}
|
|
|
|
projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner)
|
|
if err != nil {
|
|
return details, err
|
|
}
|
|
if !existingGrant.State.Exists() {
|
|
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
|
|
}
|
|
// error if provided resourceowner is not equal to the resourceowner of the project
|
|
if projectResourceOwner != existingGrant.ResourceOwner {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-0l10S9OmZV", "Errors.Project.Grant.Invalid")
|
|
}
|
|
// return if project grant is already inactive
|
|
if existingGrant.State == domain.ProjectGrantStateInactive {
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
// error if project grant is neither active nor inactive
|
|
if existingGrant.State != domain.ProjectGrantStateActive {
|
|
return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotActive")
|
|
}
|
|
if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx,
|
|
project.NewGrantDeactivateEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
|
|
existingGrant.GrantID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) ReactivateProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string) (details *domain.ObjectDetails, err error) {
|
|
if (grantID == "" && grantedOrgID == "") || projectID == "" {
|
|
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-p0s4V", "Errors.IDMissing")
|
|
}
|
|
|
|
projectResourceOwner, err := c.checkProjectExists(ctx, projectID, resourceOwner)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner)
|
|
if err != nil {
|
|
return details, err
|
|
}
|
|
if !existingGrant.State.Exists() {
|
|
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
|
|
}
|
|
// error if provided resourceowner is not equal to the resourceowner of the project
|
|
if projectResourceOwner != existingGrant.ResourceOwner {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "PROJECT-byscAarAST", "Errors.Project.Grant.Invalid")
|
|
}
|
|
// return if project grant is already active
|
|
if existingGrant.State == domain.ProjectGrantStateActive {
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
// error if project grant is neither active nor inactive
|
|
if existingGrant.State != domain.ProjectGrantStateInactive {
|
|
return details, zerrors.ThrowPreconditionFailed(nil, "PROJECT-47fu8", "Errors.Project.Grant.NotInactive")
|
|
}
|
|
if err := c.checkPermissionUpdateProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx,
|
|
project.NewGrantReactivatedEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
|
|
existingGrant.GrantID,
|
|
),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
// Deprecated: use commands.DeleteProjectGrant
|
|
func (c *Commands) RemoveProjectGrant(ctx context.Context, projectID, grantID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) {
|
|
if grantID == "" || projectID == "" {
|
|
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing")
|
|
}
|
|
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, "", projectID, resourceOwner)
|
|
if err != nil {
|
|
return details, err
|
|
}
|
|
if !existingGrant.State.Exists() {
|
|
return nil, zerrors.ThrowNotFound(nil, "PROJECT-D8JxR", "Errors.Project.Grant.NotFound")
|
|
}
|
|
if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
events := make([]eventstore.Command, 0)
|
|
events = append(events, project.NewGrantRemovedEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
|
|
existingGrant.GrantID,
|
|
existingGrant.GrantedOrgID,
|
|
),
|
|
)
|
|
|
|
for _, userGrantID := range cascadeUserGrantIDs {
|
|
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
|
|
if err != nil {
|
|
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) DeleteProjectGrant(ctx context.Context, projectID, grantID, grantedOrgID, resourceOwner string, cascadeUserGrantIDs ...string) (details *domain.ObjectDetails, err error) {
|
|
if (grantID == "" && grantedOrgID == "") || projectID == "" {
|
|
return details, zerrors.ThrowInvalidArgument(nil, "PROJECT-1m9fJ", "Errors.IDMissing")
|
|
}
|
|
existingGrant, err := c.projectGrantWriteModelByID(ctx, grantID, grantedOrgID, projectID, resourceOwner)
|
|
if err != nil {
|
|
return details, err
|
|
}
|
|
// return if project grant does not exist, or was removed already
|
|
if !existingGrant.State.Exists() {
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
if err := c.checkPermissionDeleteProjectGrant(ctx, existingGrant.ResourceOwner, existingGrant.AggregateID, existingGrant.GrantID); err != nil {
|
|
return nil, err
|
|
}
|
|
events := make([]eventstore.Command, 0)
|
|
events = append(events, project.NewGrantRemovedEvent(ctx,
|
|
ProjectAggregateFromWriteModelWithCTX(ctx, &existingGrant.WriteModel),
|
|
existingGrant.GrantID,
|
|
existingGrant.GrantedOrgID,
|
|
),
|
|
)
|
|
|
|
for _, userGrantID := range cascadeUserGrantIDs {
|
|
event, _, err := c.removeUserGrant(ctx, userGrantID, "", true)
|
|
if err != nil {
|
|
logging.WithFields("id", "COMMAND-3m8sG", "usergrantid", grantID).WithError(err).Warn("could not cascade remove user grant")
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
pushedEvents, err := c.eventstore.Push(ctx, events...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = AppendAndReduce(existingGrant, pushedEvents...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModelToObjectDetails(&existingGrant.WriteModel), nil
|
|
}
|
|
|
|
func (c *Commands) projectGrantWriteModelByID(ctx context.Context, grantID, grantedOrgID, projectID, resourceOwner string) (member *ProjectGrantWriteModel, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
writeModel := NewProjectGrantWriteModel(grantID, grantedOrgID, projectID, resourceOwner)
|
|
err = c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return writeModel, nil
|
|
}
|
|
|
|
func (c *Commands) checkProjectGrantPreCondition(ctx context.Context, projectID, grantedOrgID, resourceOwner string, roles []string) (string, error) {
|
|
if !authz.GetFeatures(ctx).ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeProjectGrant) {
|
|
return c.checkProjectGrantPreConditionOld(ctx, projectID, grantedOrgID, resourceOwner, roles)
|
|
}
|
|
projectResourceOwner, existingRoleKeys, err := c.searchProjectGrantState(ctx, projectID, grantedOrgID, resourceOwner)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if domain.HasInvalidRoles(existingRoleKeys, roles) {
|
|
return "", zerrors.ThrowPreconditionFailed(err, "COMMAND-6m9gd", "Errors.Project.Role.NotFound")
|
|
}
|
|
return projectResourceOwner, nil
|
|
}
|
|
|
|
func (c *Commands) searchProjectGrantState(ctx context.Context, projectID, grantedOrgID, resourceOwner string) (_ string, existingRoleKeys []string, err error) {
|
|
projectStateQuery := map[eventstore.FieldType]any{
|
|
eventstore.FieldTypeAggregateType: project.AggregateType,
|
|
eventstore.FieldTypeAggregateID: projectID,
|
|
eventstore.FieldTypeFieldName: project.ProjectStateSearchField,
|
|
eventstore.FieldTypeObjectType: project.ProjectSearchType,
|
|
}
|
|
grantedOrgQuery := map[eventstore.FieldType]any{
|
|
eventstore.FieldTypeAggregateType: org.AggregateType,
|
|
eventstore.FieldTypeAggregateID: grantedOrgID,
|
|
eventstore.FieldTypeFieldName: org.OrgStateSearchField,
|
|
eventstore.FieldTypeObjectType: org.OrgSearchType,
|
|
}
|
|
roleQuery := map[eventstore.FieldType]any{
|
|
eventstore.FieldTypeAggregateType: project.AggregateType,
|
|
eventstore.FieldTypeAggregateID: projectID,
|
|
eventstore.FieldTypeFieldName: project.ProjectRoleKeySearchField,
|
|
eventstore.FieldTypeObjectType: project.ProjectRoleSearchType,
|
|
}
|
|
|
|
// as resourceowner is not always provided, it has to be separately
|
|
if resourceOwner != "" {
|
|
projectStateQuery[eventstore.FieldTypeResourceOwner] = resourceOwner
|
|
roleQuery[eventstore.FieldTypeResourceOwner] = resourceOwner
|
|
}
|
|
|
|
results, err := c.eventstore.Search(
|
|
ctx,
|
|
projectStateQuery,
|
|
grantedOrgQuery,
|
|
roleQuery,
|
|
)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
var (
|
|
existsProject bool
|
|
existingProjectResourceOwner string
|
|
existsGrantedOrg bool
|
|
)
|
|
|
|
for _, result := range results {
|
|
switch result.Object.Type {
|
|
case project.ProjectRoleSearchType:
|
|
var role string
|
|
err := result.Value.Unmarshal(&role)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
existingRoleKeys = append(existingRoleKeys, role)
|
|
case org.OrgSearchType:
|
|
var state domain.OrgState
|
|
err := result.Value.Unmarshal(&state)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
existsGrantedOrg = state.Valid() && state != domain.OrgStateRemoved
|
|
case project.ProjectSearchType:
|
|
var state domain.ProjectState
|
|
err := result.Value.Unmarshal(&state)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
existsProject = state.Valid() && state != domain.ProjectStateRemoved
|
|
existingProjectResourceOwner = result.Aggregate.ResourceOwner
|
|
}
|
|
}
|
|
|
|
if !existsProject {
|
|
return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-m9gsd", "Errors.Project.NotFound")
|
|
}
|
|
if !existsGrantedOrg {
|
|
return "", nil, zerrors.ThrowPreconditionFailed(err, "COMMAND-3m9gg", "Errors.Org.NotFound")
|
|
}
|
|
return existingProjectResourceOwner, existingRoleKeys, nil
|
|
}
|