mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-06 06:02:15 +00:00
fix(project): include an option to add project members during project creation (#10654)
# Which Problems Are Solved When a project is created by a user with only the `PROJECT_CREATOR` role, they can no longer view/manage the created project. Although the project is created, the user sees the following error: `No matching permissions found (AUTH-3jknH)`. This is due to the [removal](https://github.com/zitadel/zitadel/pull/9317) of auto-assignment of the `PROJECT_OWNER` role when a project is newly created. # How the Problems Are Solved By introducing optional fields in the CreateProject API to include a list of users and a list of project member roles to be assigned to the users. When there are no roles mentioned, the `PROJECT_OWNER` role is assigned by default to all the users mentioned in the list. # Additional Changes N/A # Additional Context - Closes #10561 - Closes #10592 - Should be backported as this issue is not specific to v4 --------- Co-authored-by: conblem <mail@conblem.me> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com> Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
@@ -27,6 +27,12 @@ type AddProject struct {
|
||||
ProjectRoleCheck bool
|
||||
HasProjectCheck bool
|
||||
PrivateLabelingSetting domain.PrivateLabelingSetting
|
||||
Admins []*AddProjectAdmin
|
||||
}
|
||||
|
||||
type AddProjectAdmin struct {
|
||||
ID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func (p *AddProject) IsValid() error {
|
||||
@@ -74,6 +80,12 @@ func (c *Commands) AddProject(ctx context.Context, add *AddProject) (_ *domain.O
|
||||
add.HasProjectCheck,
|
||||
add.PrivateLabelingSetting),
|
||||
}
|
||||
projectMemberEvents, err := c.addProjectMemberEvents(ctx, add.ResourceOwner, add.AggregateID, add.Admins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, projectMemberEvents...)
|
||||
|
||||
postCommit, err := c.projectCreatedMilestone(ctx, &events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -187,6 +199,32 @@ func (c *Commands) checkProjectExists(ctx context.Context, projectID, resourceOw
|
||||
return agg.ResourceOwner, nil
|
||||
}
|
||||
|
||||
func (c *Commands) addProjectMemberEvents(ctx context.Context, resourceOwner, projectID string, admins []*AddProjectAdmin) ([]eventstore.Command, error) {
|
||||
if len(admins) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
events := make([]eventstore.Command, 0)
|
||||
for _, admin := range admins {
|
||||
_, err := c.checkUserExists(ctx, admin.ID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := admin.Roles
|
||||
if len(roles) == 0 {
|
||||
roles = append(roles, domain.RoleProjectOwner)
|
||||
}
|
||||
|
||||
projectMemberWriteModel := NewProjectMemberWriteModel(projectID, admin.ID, resourceOwner)
|
||||
events = append(events, project.NewProjectMemberAddedEvent(ctx,
|
||||
ProjectAggregateFromWriteModelWithCTX(ctx, &projectMemberWriteModel.WriteModel),
|
||||
admin.ID,
|
||||
roles...,
|
||||
))
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
type ChangeProject struct {
|
||||
models.ObjectRoot
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/project"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@@ -195,6 +197,155 @@ func TestCommandSide_AddProject(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project, with admins and no roles, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username1",
|
||||
"firstname1",
|
||||
"lastname1",
|
||||
"nickname1",
|
||||
"displayname1",
|
||||
language.German,
|
||||
domain.GenderMale,
|
||||
"email1",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
project.NewProjectAddedEvent(
|
||||
context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
|
||||
),
|
||||
project.NewProjectMemberAddedEvent(
|
||||
context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"user1", []string{"PROJECT_OWNER"}...,
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
project: &AddProject{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"},
|
||||
Name: "project",
|
||||
ProjectRoleAssertion: true,
|
||||
ProjectRoleCheck: true,
|
||||
HasProjectCheck: true,
|
||||
PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
|
||||
Admins: []*AddProjectAdmin{
|
||||
{
|
||||
ID: "user1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project, with admins and specific roles, ok",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"username1",
|
||||
"firstname1",
|
||||
"lastname1",
|
||||
"nickname1",
|
||||
"displayname1",
|
||||
language.German,
|
||||
domain.GenderMale,
|
||||
"email1",
|
||||
true,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectPush(
|
||||
project.NewProjectAddedEvent(
|
||||
context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"project", true, true, true,
|
||||
domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
|
||||
),
|
||||
project.NewProjectMemberAddedEvent(
|
||||
context.Background(),
|
||||
&project.NewAggregate("project1", "org1").Aggregate,
|
||||
"user1", []string{"role1", "role2"}...,
|
||||
),
|
||||
),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
project: &AddProject{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"},
|
||||
Name: "project",
|
||||
ProjectRoleAssertion: true,
|
||||
ProjectRoleCheck: true,
|
||||
HasProjectCheck: true,
|
||||
PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
|
||||
Admins: []*AddProjectAdmin{
|
||||
{
|
||||
ID: "user1",
|
||||
Roles: []string{"role1", "role2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "project, admin user not found",
|
||||
fields: fields{
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
checkPermission: newMockPermissionCheckAllowed(),
|
||||
},
|
||||
args: args{
|
||||
ctx: authz.WithInstanceID(context.Background(), "instanceID"),
|
||||
project: &AddProject{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "project1", ResourceOwner: "org1"},
|
||||
Name: "project",
|
||||
ProjectRoleAssertion: true,
|
||||
ProjectRoleCheck: true,
|
||||
HasProjectCheck: true,
|
||||
PrivateLabelingSetting: domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy,
|
||||
Admins: []*AddProjectAdmin{
|
||||
{
|
||||
ID: "user2",
|
||||
Roles: []string{"role1", "role2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: res{
|
||||
err: zerrors.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user