diff --git a/console/src/app/pages/projects/project-create/project-create.component.html b/console/src/app/pages/projects/project-create/project-create.component.html
index 2dd98dec0e4..011804c7c94 100644
--- a/console/src/app/pages/projects/project-create/project-create.component.html
+++ b/console/src/app/pages/projects/project-create/project-create.component.html
@@ -2,14 +2,17 @@
title="{{ 'PROJECT.PAGES.CREATE' | translate }}"
[createSteps]="1"
[currentCreateStep]="1"
- (closed)="close()"
+ (closed)="location.back()"
>
-
{{ 'PROJECT.PAGES.CREATE_DESC' | translate }}
-
+
+
diff --git a/console/src/app/pages/projects/project-create/project-create.component.ts b/console/src/app/pages/projects/project-create/project-create.component.ts
index a20e3ae73b5..72f898b86b3 100644
--- a/console/src/app/pages/projects/project-create/project-create.component.ts
+++ b/console/src/app/pages/projects/project-create/project-create.component.ts
@@ -1,10 +1,12 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
-import { AddProjectRequest, AddProjectResponse } from 'src/app/proto/generated/zitadel/management_pb';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
-import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
+import { NewMgmtService } from 'src/app/services/new-mgmt.service';
+import { UserService } from 'src/app/services/user.service';
+import { FormBuilder, FormControl, Validators } from '@angular/forms';
+import { User } from '@zitadel/proto/zitadel/user/v2/user_pb';
@Component({
selector: 'cnsl-project-create',
@@ -12,13 +14,15 @@ import { ToastService } from 'src/app/services/toast.service';
styleUrls: ['./project-create.component.scss'],
})
export class ProjectCreateComponent {
- public project: AddProjectRequest.AsObject = new AddProjectRequest().toObject();
+ protected readonly form: ReturnType;
constructor(
- private router: Router,
- private toast: ToastService,
- private mgmtService: ManagementService,
- private _location: Location,
+ private readonly router: Router,
+ private readonly toast: ToastService,
+ private readonly newMgmtService: NewMgmtService,
+ private readonly fb: FormBuilder,
+ protected readonly location: Location,
+ protected readonly userService: UserService,
breadcrumbService: BreadcrumbService,
) {
const bread: Breadcrumb = {
@@ -26,21 +30,31 @@ export class ProjectCreateComponent {
routerLink: ['/org'],
};
breadcrumbService.setBreadcrumb([bread]);
+
+ this.form = this.buildForm();
}
- public saveProject(): void {
- this.mgmtService
- .addProject(this.project)
- .then((resp: AddProjectResponse.AsObject) => {
+ private buildForm() {
+ return this.fb.group({
+ name: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
+ selfAccount: new FormControl(true, { nonNullable: true, validators: [Validators.required] }),
+ });
+ }
+
+ protected saveProject(user: User): void {
+ const { name, selfAccount } = this.form.getRawValue();
+
+ this.newMgmtService
+ .addProject({
+ name,
+ admins: selfAccount ? [{ userId: user.userId }] : undefined,
+ })
+ .then((resp) => {
this.toast.showInfo('PROJECT.TOAST.CREATED', true);
- this.router.navigate(['projects', resp.id], { queryParams: { new: true } });
+ return this.router.navigate(['projects', resp.id], { queryParams: { new: true } });
})
.catch((error) => {
this.toast.showError(error);
});
}
-
- public close(): void {
- this._location.back();
- }
}
diff --git a/console/src/app/pages/projects/project-create/project-create.module.ts b/console/src/app/pages/projects/project-create/project-create.module.ts
index 34ddbbd0d08..df27e4055d3 100644
--- a/console/src/app/pages/projects/project-create/project-create.module.ts
+++ b/console/src/app/pages/projects/project-create/project-create.module.ts
@@ -1,7 +1,7 @@
import { A11yModule } from '@angular/cdk/a11y';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
@@ -10,6 +10,7 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { ProjectCreateRoutingModule } from './project-create-routing.module';
import { ProjectCreateComponent } from './project-create.component';
+import { MatSlideToggleModule } from '@angular/material/slide-toggle';
@NgModule({
declarations: [ProjectCreateComponent],
@@ -23,6 +24,8 @@ import { ProjectCreateComponent } from './project-create.component';
MatButtonModule,
MatIconModule,
TranslateModule,
+ MatSlideToggleModule,
+ ReactiveFormsModule,
],
})
export default class ProjectCreateModule {}
diff --git a/console/src/app/services/new-mgmt.service.ts b/console/src/app/services/new-mgmt.service.ts
index 9e0c9dc6e42..94b0b67aa75 100644
--- a/console/src/app/services/new-mgmt.service.ts
+++ b/console/src/app/services/new-mgmt.service.ts
@@ -1,6 +1,8 @@
import { Injectable } from '@angular/core';
import { GrpcService } from './grpc.service';
import {
+ AddProjectRequestSchema,
+ AddProjectResponse,
GenerateMachineSecretRequestSchema,
GenerateMachineSecretResponse,
GetDefaultPasswordComplexityPolicyResponse,
@@ -19,7 +21,6 @@ import {
ResendHumanInitializationResponse,
ResendHumanPhoneVerificationRequestSchema,
ResendHumanPhoneVerificationResponse,
- SendHumanResetPasswordNotificationRequest_Type,
SendHumanResetPasswordNotificationRequestSchema,
SendHumanResetPasswordNotificationResponse,
SetUserMetadataRequestSchema,
@@ -98,4 +99,8 @@ export class NewMgmtService {
public getDefaultPasswordComplexityPolicy(): Promise {
return this.grpcService.mgmtNew.getDefaultPasswordComplexityPolicy({});
}
+
+ public addProject(req: MessageInitShape): Promise {
+ return this.grpcService.mgmtNew.addProject(req);
+ }
}
diff --git a/console/src/assets/i18n/bg.json b/console/src/assets/i18n/bg.json
index 64a5b8cc82e..d0af170acce 100644
--- a/console/src/assets/i18n/bg.json
+++ b/console/src/assets/i18n/bg.json
@@ -1959,6 +1959,7 @@
"CREATE_DESC": "Въведете името на вашия проект.",
"ROLE": "Роля",
"NOITEMS": "Без проекти",
+ "USERSELFACCOUNT": "Използвайте личния си акаунт като собственик на проекта.",
"ZITADELPROJECT": "Това принадлежи на проекта ZITADEL. ",
"TYPE": {
"OWNED": "Притежавани проекти",
diff --git a/console/src/assets/i18n/cs.json b/console/src/assets/i18n/cs.json
index c0c25ab602d..a67623440ff 100644
--- a/console/src/assets/i18n/cs.json
+++ b/console/src/assets/i18n/cs.json
@@ -1961,6 +1961,7 @@
"CREATE_DESC": "Vložte název vašeho projektu.",
"ROLE": "Role",
"NOITEMS": "Žádné projekty",
+ "USERSELFACCOUNT": "Použijte svůj osobní účet jako vlastník projektu.",
"ZITADELPROJECT": "Tato nastavení jsou základní nastavení instance projektu ZITADEL. Pozor: Pokud provedete změny, ZITADEL se nemusí chovat podle očekávání.",
"TYPE": {
"OWNED": "Vlastní projekty",
diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json
index 83fbb8bdc91..8167952ac67 100644
--- a/console/src/assets/i18n/de.json
+++ b/console/src/assets/i18n/de.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "Gebe den Namen ein.",
"ROLE": "Rolle",
"NOITEMS": "Keine Projekte",
+ "USERSELFACCOUNT": "Verwenden Sie Ihr persönliches Konto als Projektinhaber.",
"ZITADELPROJECT": "Diese Einstellungen gehören zum ZITADEL-Projekt. Wenn Du diese änderst, verhält sich ZITADEL möglicherweise nicht wie beabsichtigt.",
"TYPE": {
"OWNED": "Eigene Projekte",
diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json
index 4e650c280ef..00cf13eaa16 100644
--- a/console/src/assets/i18n/en.json
+++ b/console/src/assets/i18n/en.json
@@ -1963,6 +1963,7 @@
"CREATE_DESC": "Insert your project's name.",
"ROLE": "Role",
"NOITEMS": "No projects",
+ "USERSELFACCOUNT": "Use your personal account as project owner.",
"ZITADELPROJECT": "This belongs to the ZITADEL project. Beware: If you make changes ZITADEL may not behave as intended.",
"TYPE": {
"OWNED": "Owned Projects",
diff --git a/console/src/assets/i18n/es.json b/console/src/assets/i18n/es.json
index 1a7974806a1..e41ccf78d25 100644
--- a/console/src/assets/i18n/es.json
+++ b/console/src/assets/i18n/es.json
@@ -1961,6 +1961,7 @@
"CREATE_DESC": "Inserta el nombre de tu proyecto.",
"ROLE": "Rol",
"NOITEMS": "Sin proyectos",
+ "USERSELFACCOUNT": "Utiliza tu cuenta personal como propietario del proyecto.",
"ZITADELPROJECT": "Esto pertenece al proyecto ZITADEL. Cuidado: Si haces cambio puede que ZITADEL no se comporte como se espera.",
"TYPE": {
"OWNED": "Proyectos propios",
diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json
index 585dc01a3c7..f55d8f6c50c 100644
--- a/console/src/assets/i18n/fr.json
+++ b/console/src/assets/i18n/fr.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "Insérez le nom de votre projet.",
"ROLE": "Rôle",
"NOITEMS": "Aucun projet",
+ "USERSELFACCOUNT": "Utilisez votre compte personnel en tant que propriétaire du projet.",
"ZITADELPROJECT": "Ceci appartient au projet ZITADEL. Attention : si vous faites de changements, ZITADEL pourrait ne pas fonctionner correctement.",
"TYPE": {
"OWNED": "Projets possédés",
diff --git a/console/src/assets/i18n/hu.json b/console/src/assets/i18n/hu.json
index c505d82b63d..ab482f326d1 100644
--- a/console/src/assets/i18n/hu.json
+++ b/console/src/assets/i18n/hu.json
@@ -1958,6 +1958,7 @@
"CREATE_DESC": "Add meg a projekted nevét.",
"ROLE": "Szerepkör",
"NOITEMS": "Nincsenek projektek",
+ "USERSELFACCOUNT": "Használja személyes fiókját a projekt tulajdonosaként.",
"ZITADELPROJECT": "Ez a ZITADEL projekthez tartozik. Vigyázz: Ha változtatásokat hajtasz végre, a ZITADEL lehet, hogy nem fog a szándékod szerint működni.",
"TYPE": {
"OWNED": "Saját Projektek",
diff --git a/console/src/assets/i18n/id.json b/console/src/assets/i18n/id.json
index 9be2153bb5e..6d651e304e3 100644
--- a/console/src/assets/i18n/id.json
+++ b/console/src/assets/i18n/id.json
@@ -1819,6 +1819,7 @@
"CREATE_DESC": "Masukkan nama proyek Anda.",
"ROLE": "Peran",
"NOITEMS": "Tidak ada proyek",
+ "USERSELFACCOUNT": "Gunakan akun pribadi Anda sebagai pemilik proyek.",
"ZITADELPROJECT": "Ini milik proyek ZITADEL. Hati-hati: Jika Anda membuat perubahan, ZITADEL mungkin tidak berfungsi sebagaimana mestinya.",
"TYPE": {
"OWNED": "Proyek Milik",
diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json
index 6c18318b1c7..08dbf679f75 100644
--- a/console/src/assets/i18n/it.json
+++ b/console/src/assets/i18n/it.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "Inserisci il nome del tuo progetto.",
"ROLE": "Ruolo",
"NOITEMS": "Nessun progetto",
+ "USERSELFACCOUNT": "Utilizza il tuo account personale come proprietario del progetto.",
"ZITADELPROJECT": "Questo appartiene al progetto ZITADEL. Attenzione: se fai delle modifiche ZITADEL potrebbe non comportarsi come previsto.",
"TYPE": {
"OWNED": "Progetti proprietari",
diff --git a/console/src/assets/i18n/ja.json b/console/src/assets/i18n/ja.json
index 2a4dea9ff19..e3d75102480 100644
--- a/console/src/assets/i18n/ja.json
+++ b/console/src/assets/i18n/ja.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "プロジェクトの名前を入力します。",
"ROLE": "ロール",
"NOITEMS": "プロジェクトはありません",
+ "USERSELFACCOUNT": "プロジェクトの所有者としては、ご自身の個人アカウントをご利用ください。",
"ZITADELPROJECT": "これはZITADELプロジェクトに属します。注意:変更した場合、ZITADELは意図したとおりに動作しないことがあります。",
"TYPE": {
"OWNED": "所有プロジェクト",
diff --git a/console/src/assets/i18n/ko.json b/console/src/assets/i18n/ko.json
index 8ebbfef3b2d..9caca028379 100644
--- a/console/src/assets/i18n/ko.json
+++ b/console/src/assets/i18n/ko.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "프로젝트의 이름을 입력하세요.",
"ROLE": "역할",
"NOITEMS": "프로젝트가 없습니다",
+ "USERSELFACCOUNT": "프로젝트 소유자로 개인 계정을 사용해 주십시오",
"ZITADELPROJECT": "이 프로젝트는 ZITADEL 프로젝트에 속해 있습니다. 변경 시 ZITADEL이 의도한 대로 작동하지 않을 수 있습니다.",
"TYPE": {
"OWNED": "소유한 프로젝트",
diff --git a/console/src/assets/i18n/mk.json b/console/src/assets/i18n/mk.json
index ba482652770..c0fee920bd4 100644
--- a/console/src/assets/i18n/mk.json
+++ b/console/src/assets/i18n/mk.json
@@ -1961,6 +1961,7 @@
"CREATE_DESC": "Внесете го името на вашиот проект.",
"ROLE": "Улога",
"NOITEMS": "Нема проекти",
+ "USERSELFACCOUNT": "Користете ја вашата лична корисничка сметка како сопственик на проектот.",
"ZITADELPROJECT": "Ова припаѓа на ZITADEL проектот. Внимание: Ако направите промени, ZITADEL може да не работи како што е предвидено.",
"TYPE": {
"OWNED": "Сопствени проекти",
diff --git a/console/src/assets/i18n/nl.json b/console/src/assets/i18n/nl.json
index 1a176feeb01..6898331e83e 100644
--- a/console/src/assets/i18n/nl.json
+++ b/console/src/assets/i18n/nl.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "Voer de naam van uw project in.",
"ROLE": "Rol",
"NOITEMS": "Geen projecten",
+ "USERSELFACCOUNT": "Gebruik uw persoonlijke account als projecteigenaar.",
"ZITADELPROJECT": "Dit behoort tot het ZITADEL-project. Pas op: Als u wijzigingen aanbrengt, werkt ZITADEL mogelijk niet zoals bedoeld.",
"TYPE": {
"OWNED": "Eigen Projecten",
diff --git a/console/src/assets/i18n/pl.json b/console/src/assets/i18n/pl.json
index a860ca0634a..8c541034a03 100644
--- a/console/src/assets/i18n/pl.json
+++ b/console/src/assets/i18n/pl.json
@@ -1959,6 +1959,7 @@
"CREATE_DESC": "Wprowadź nazwę projektu.",
"ROLE": "Rola",
"NOITEMS": "Brak projektów",
+ "USERSELFACCOUNT": "Użyj swojego konta osobistego jako właściciel projektu.",
"ZITADELPROJECT": "To należy do projektu ZITADEL. Uwaga: Jeśli wprowadzisz zmiany, ZITADEL może nie działać zgodnie z oczekiwaniami.",
"TYPE": {
"OWNED": "Własne Projekty",
diff --git a/console/src/assets/i18n/pt.json b/console/src/assets/i18n/pt.json
index 88b31bddcc6..d334b1cfd25 100644
--- a/console/src/assets/i18n/pt.json
+++ b/console/src/assets/i18n/pt.json
@@ -1960,6 +1960,7 @@
"CREATE_DESC": "Insira o nome do seu projeto.",
"ROLE": "Função",
"NOITEMS": "Nenhum projeto",
+ "USERSELFACCOUNT": "Use a sua conta pessoal como proprietário do projeto.",
"ZITADELPROJECT": "Isto pertence ao projeto ZITADEL. Atenção: Se você fizer alterações, o ZITADEL pode não funcionar como o esperado.",
"TYPE": {
"OWNED": "Projetos Próprios",
diff --git a/console/src/assets/i18n/ro.json b/console/src/assets/i18n/ro.json
index f8926f0e2b8..76e18fda9f7 100644
--- a/console/src/assets/i18n/ro.json
+++ b/console/src/assets/i18n/ro.json
@@ -1958,6 +1958,7 @@
"CREATE_DESC": "Introduceți numele proiectului dvs.",
"ROLE": "Rol",
"NOITEMS": "Niciun proiect",
+ "USERSELFACCOUNT": "Utilizați contul personal ca proprietar al proiectului.",
"ZITADELPROJECT": "Acesta aparține proiectului ZITADEL. Atenție: Dacă faceți modificări, ZITADEL ar putea să nu se comporte conform intențiilor.",
"TYPE": {
"OWNED": "Proiecte deținute",
diff --git a/console/src/assets/i18n/ru.json b/console/src/assets/i18n/ru.json
index 79bd4abe6aa..c06a7d25fc8 100644
--- a/console/src/assets/i18n/ru.json
+++ b/console/src/assets/i18n/ru.json
@@ -2045,6 +2045,7 @@
"CREATE_DESC": "Введите название вашего проекта.",
"ROLE": "Роль",
"NOITEMS": "Нет проектов",
+ "USERSELFACCOUNT": "Используйте свою личную учетную запись в качестве владельца проекта.",
"ZITADELPROJECT": "Это принадлежит проекту ZITADEL. Осторожно: Если вы внесёте изменения, ZITADEL может вести себя не так, как предполагалось.",
"TYPE": {
"OWNED": "Собственные проекты",
diff --git a/console/src/assets/i18n/sv.json b/console/src/assets/i18n/sv.json
index 17c2c1c362c..18fd8089064 100644
--- a/console/src/assets/i18n/sv.json
+++ b/console/src/assets/i18n/sv.json
@@ -1964,6 +1964,7 @@
"CREATE_DESC": "Ange ditt projekts namn.",
"ROLE": "Roll",
"NOITEMS": "Inga projekt",
+ "USERSELFACCOUNT": "Använd ditt personliga konto som projektägare.",
"ZITADELPROJECT": "Detta tillhör ZITADEL-projektet. Observera: Om du gör ändringar kan ZITADEL inte fungera som avsett.",
"TYPE": {
"OWNED": "Ägda Projekt",
diff --git a/console/src/assets/i18n/tr.json b/console/src/assets/i18n/tr.json
index 73b360f6312..c9b31ea25fb 100644
--- a/console/src/assets/i18n/tr.json
+++ b/console/src/assets/i18n/tr.json
@@ -1963,6 +1963,7 @@
"CREATE_DESC": "Projenizin adını girin.",
"ROLE": "Rol",
"NOITEMS": "Proje yok",
+ "USERSELFACCOUNT": "Proje sahibi olarak kişisel hesabınızı kullanın.",
"ZITADELPROJECT": "Bu ZITADEL projesine aittir. Dikkat: Değişiklik yaparsanız ZITADEL amaçlandığı gibi davranmayabilir.",
"TYPE": {
"OWNED": "Sahip Olunan Projeler",
diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json
index fa12176e749..047bd79c99e 100644
--- a/console/src/assets/i18n/zh.json
+++ b/console/src/assets/i18n/zh.json
@@ -1963,6 +1963,7 @@
"CREATE_DESC": "插入您的项目名称。",
"ROLE": "角色",
"NOITEMS": "没有项目",
+ "USERSELFACCOUNT": "使用您的个人账户作为项目所有者",
"ZITADELPROJECT": "这属于 ZITADEL 项目。注意:如果您修改了设置,ZITADEL 可能无法正常运行。",
"TYPE": {
"OWNED": "拥有的项目",
diff --git a/internal/api/grpc/management/project_converter.go b/internal/api/grpc/management/project_converter.go
index 8ea0014800b..994b6769c66 100644
--- a/internal/api/grpc/management/project_converter.go
+++ b/internal/api/grpc/management/project_converter.go
@@ -18,6 +18,7 @@ import (
)
func ProjectCreateToCommand(req *mgmt_pb.AddProjectRequest, projectID string, resourceOwner string) *command.AddProject {
+ admins := projectCreateAdminsToCommand(req.GetAdmins())
return &command.AddProject{
ObjectRoot: models.ObjectRoot{
AggregateID: projectID,
@@ -28,6 +29,7 @@ func ProjectCreateToCommand(req *mgmt_pb.AddProjectRequest, projectID string, re
ProjectRoleCheck: req.ProjectRoleCheck,
HasProjectCheck: req.HasProjectCheck,
PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting),
+ Admins: admins,
}
}
@@ -56,6 +58,20 @@ func privateLabelingSettingToDomain(setting proj_pb.PrivateLabelingSetting) doma
}
}
+func projectCreateAdminsToCommand(requestAdmins []*mgmt_pb.AddProjectRequest_Admin) []*command.AddProjectAdmin {
+ if len(requestAdmins) == 0 {
+ return nil
+ }
+ admins := make([]*command.AddProjectAdmin, len(requestAdmins))
+ for i, admin := range requestAdmins {
+ admins[i] = &command.AddProjectAdmin{
+ ID: admin.GetUserId(),
+ Roles: admin.GetRoles(),
+ }
+ }
+ return admins
+}
+
func AddProjectRoleRequestToCommand(req *mgmt_pb.AddProjectRoleRequest, resourceOwner string) *command.AddProjectRole {
return &command.AddProjectRole{
ObjectRoot: models.ObjectRoot{
diff --git a/internal/api/grpc/project/v2beta/integration_test/project_test.go b/internal/api/grpc/project/v2beta/integration_test/project_test.go
index 0b591f5bd12..869e64ce79d 100644
--- a/internal/api/grpc/project/v2beta/integration_test/project_test.go
+++ b/internal/api/grpc/project/v2beta/integration_test/project_test.go
@@ -96,6 +96,12 @@ func TestServer_CreateProject_Permission(t *testing.T) {
iamOwnerCtx := instance.WithAuthorizationToken(CTX, integration.UserTypeIAMOwner)
orgResp := instance.CreateOrganization(iamOwnerCtx, integration.OrganizationName(), integration.Email())
+ // user with ORG_PROJECT_CREATOR role in same org
+ user1Id, token1 := getOrgProjectCreator(t, iamOwnerCtx, orgResp.GetOrganizationId(), orgResp.GetOrganizationId())
+
+ // user with ORG_PROJECT_CREATOR role in a different org
+ _, token2 := getOrgProjectCreator(t, iamOwnerCtx, orgResp.GetOrganizationId(), instance.DefaultOrg.GetId())
+
type want struct {
id bool
creationDate bool
@@ -136,7 +142,7 @@ func TestServer_CreateProject_Permission(t *testing.T) {
},
{
name: "with ORG_PROJECT_CREATOR permission, same organization, ok",
- ctx: integration.WithAuthorizationToken(CTX, getOrgProjectCreatorToken(t, iamOwnerCtx, orgResp.GetOrganizationId(), orgResp.GetOrganizationId())),
+ ctx: integration.WithAuthorizationToken(CTX, token1),
req: &project.CreateProjectRequest{
Name: integration.ProjectName(),
OrganizationId: orgResp.GetOrganizationId(),
@@ -148,7 +154,7 @@ func TestServer_CreateProject_Permission(t *testing.T) {
},
{
name: "with ORG_PROJECT_CREATOR permission, other organization, ok",
- ctx: integration.WithAuthorizationToken(CTX, getOrgProjectCreatorToken(t, iamOwnerCtx, orgResp.GetOrganizationId(), instance.DefaultOrg.GetId())),
+ ctx: integration.WithAuthorizationToken(CTX, token2),
req: &project.CreateProjectRequest{
Name: integration.ProjectName(),
OrganizationId: instance.DefaultOrg.GetId(),
@@ -158,6 +164,43 @@ func TestServer_CreateProject_Permission(t *testing.T) {
creationDate: true,
},
},
+ {
+ name: "with ORG_PROJECT_CREATOR permission, with admins and roles, ok",
+ ctx: integration.WithAuthorizationToken(CTX, token1),
+ req: &project.CreateProjectRequest{
+ Name: integration.ProjectName(),
+ OrganizationId: orgResp.GetOrganizationId(),
+ Admins: []*project.CreateProjectRequest_Admin{
+ {
+ UserId: user1Id,
+ Roles: []string{"role1", "role2"},
+ },
+ },
+ },
+ want: want{
+ id: true,
+ creationDate: true,
+ },
+ },
+ {
+ name: "with ORG_PROJECT_CREATOR permission, missing user from the admins list, ok",
+ ctx: integration.WithAuthorizationToken(CTX, token1),
+ req: &project.CreateProjectRequest{
+ Name: integration.ProjectName(),
+ OrganizationId: orgResp.GetOrganizationId(),
+ Admins: []*project.CreateProjectRequest_Admin{
+ {
+ UserId: user1Id,
+ Roles: []string{"role1", "role2"},
+ },
+ {
+ UserId: "random_user",
+ Roles: []string{"role1", "role2"},
+ },
+ },
+ },
+ wantErr: true,
+ },
{
name: "organization owner, ok",
ctx: instance.WithAuthorizationToken(CTX, integration.UserTypeOrgOwner),
@@ -199,7 +242,7 @@ func TestServer_CreateProject_Permission(t *testing.T) {
}
}
-func getOrgProjectCreatorToken(t *testing.T, ctx context.Context, orgId1, orgId2 string) string {
+func getOrgProjectCreator(t *testing.T, ctx context.Context, orgId1, orgId2 string) (string, string) {
// create a machine user in Org 1
userResp := instance.CreateUserTypeMachine(ctx, orgId1)
@@ -213,7 +256,7 @@ func getOrgProjectCreatorToken(t *testing.T, ctx context.Context, orgId1, orgId2
})
require.NoError(t, err)
- return instance.CreatePersonalAccessToken(ctx, userResp.GetId()).Token
+ return userResp.GetId(), instance.CreatePersonalAccessToken(ctx, userResp.GetId()).Token
}
func assertCreateProjectResponse(t *testing.T, creationDate, changeDate time.Time, expectedCreationDate, expectedID bool, actualResp *project.CreateProjectResponse) {
diff --git a/internal/api/grpc/project/v2beta/project.go b/internal/api/grpc/project/v2beta/project.go
index 95f08ea61e5..55f6a87b401 100644
--- a/internal/api/grpc/project/v2beta/project.go
+++ b/internal/api/grpc/project/v2beta/project.go
@@ -31,6 +31,7 @@ func (s *Server) CreateProject(ctx context.Context, req *connect.Request[project
}
func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddProject {
+ admins := projectCreateAdminsToCommand(req.GetAdmins())
var aggregateID string
if req.Id != nil {
aggregateID = *req.Id
@@ -45,9 +46,24 @@ func projectCreateToCommand(req *project_pb.CreateProjectRequest) *command.AddPr
ProjectRoleCheck: req.AuthorizationRequired,
HasProjectCheck: req.ProjectAccessRequired,
PrivateLabelingSetting: privateLabelingSettingToDomain(req.PrivateLabelingSetting),
+ Admins: admins,
}
}
+func projectCreateAdminsToCommand(requestAdmins []*project_pb.CreateProjectRequest_Admin) []*command.AddProjectAdmin {
+ if len(requestAdmins) == 0 {
+ return nil
+ }
+ admins := make([]*command.AddProjectAdmin, len(requestAdmins))
+ for i, admin := range requestAdmins {
+ admins[i] = &command.AddProjectAdmin{
+ ID: admin.GetUserId(),
+ Roles: admin.GetRoles(),
+ }
+ }
+ return admins
+}
+
func privateLabelingSettingToDomain(setting project_pb.PrivateLabelingSetting) domain.PrivateLabelingSetting {
switch setting {
case project_pb.PrivateLabelingSetting_PRIVATE_LABELING_SETTING_ALLOW_LOGIN_USER_RESOURCE_OWNER_POLICY:
diff --git a/internal/command/project.go b/internal/command/project.go
index 4d060b6233b..aceb20912cd 100644
--- a/internal/command/project.go
+++ b/internal/command/project.go
@@ -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
diff --git a/internal/command/project_test.go b/internal/command/project_test.go
index 6c03420f6bc..46ea878b1dd 100644
--- a/internal/command/project_test.go
+++ b/internal/command/project_test.go
@@ -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) {
diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto
index 19dfa9153a7..479acdc1f36 100644
--- a/proto/zitadel/management.proto
+++ b/proto/zitadel/management.proto
@@ -9850,6 +9850,12 @@ message ListProjectChangesResponse {
}
message AddProjectRequest {
+ message Admin {
+ string user_id = 1;
+ // specify the Project Member Roles for the provided user (default is PROJECT_OWNER if roles are empty
+ repeated string roles = 3;
+ }
+
string name = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
@@ -9880,6 +9886,8 @@ message AddProjectRequest {
description: "Define which private labeling/branding should trigger when getting to a login of this project.";
}
];
+ // List of users and Project Member roles (PROJECT_OWNER, by default) to be assigned to those users.
+ repeated Admin admins = 6;
}
message AddProjectResponse {
diff --git a/proto/zitadel/project/v2beta/project_service.proto b/proto/zitadel/project/v2beta/project_service.proto
index 66f221b9116..bfeae8573eb 100644
--- a/proto/zitadel/project/v2beta/project_service.proto
+++ b/proto/zitadel/project/v2beta/project_service.proto
@@ -6,14 +6,16 @@ import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/struct.proto";
+import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
-import "zitadel/project/v2beta/query.proto";
-import "google/protobuf/timestamp.proto";
import "zitadel/filter/v2beta/filter.proto";
+import "zitadel/project/v2beta/query.proto";
+import "zitadel/user/v2/user.proto";
+import "zitadel/user/v2/user_service.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/project/v2beta;project";
@@ -669,6 +671,12 @@ service ProjectService {
}
message CreateProjectRequest {
+ message Admin {
+ string user_id = 1;
+ // specify the Project Member Roles for the provided user (default is PROJECT_OWNER if roles are empty
+ repeated string roles = 3;
+ }
+
// The unique identifier of the organization the project belongs to.
string organization_id = 1 [
(validate.rules).string = {min_len: 1, max_len: 200},
@@ -707,6 +715,8 @@ message CreateProjectRequest {
PrivateLabelingSetting private_labeling_setting = 7 [
(validate.rules).enum = {defined_only: true}
];
+ // List of users and Project Member roles (PROJECT_OWNER, by default) to be assigned to those users.
+ repeated Admin admins = 8;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"organizationId\":\"69629026806489455\",\"name\":\"MyProject\",\"projectRoleAssertion\":true,\"projectRoleCheck\":true,\"hasProjectCheck\":true,\"privateLabelingSetting\":\"PRIVATE_LABELING_SETTING_UNSPECIFIED\"}";
};