feat: allow global org users to create org and self delete (#2759)

* fix: grant PROJECT_OWNER_VIEWER_GLOBAL org.create permission

* Update authz.yaml

* feat: delete my user

* console things

* lint

* signout after deletion

* stylelint rule

* Update authz.yaml

* Update authz.yaml

* setup step

* role SELF_MANAGEMENT_GLOBAL setup

* fix: change default role on global org

* Apply suggestions from code review

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

* Update console/src/assets/i18n/it.json

Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>

Co-authored-by: Max Peintner <max@caos.ch>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
This commit is contained in:
Livio Amstutz 2021-12-09 09:41:21 +01:00 committed by GitHub
parent 7ea93bd5b6
commit 43f15953c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 349 additions and 62 deletions

View File

@ -330,6 +330,11 @@ InternalAuthZ:
- "user.global.read" - "user.global.read"
- "user.grant.read" - "user.grant.read"
- "user.membership.read" - "user.membership.read"
- Role: 'SELF_MANAGEMENT_GLOBAL'
Permissions:
- "org.create"
- "policy.read"
- "user.self.delete"
- Role: 'PROJECT_OWNER_GLOBAL' - Role: 'PROJECT_OWNER_GLOBAL'
Permissions: Permissions:
- "org.global.read" - "org.global.read"

View File

@ -1,7 +1,5 @@
{ {
"plugins": [ "plugins": ["stylelint-scss"],
"stylelint-scss"
],
"extends": "stylelint-config-standard", "extends": "stylelint-config-standard",
"rules": { "rules": {
"value-keyword-case": null, "value-keyword-case": null,
@ -11,30 +9,20 @@
"at-rule-empty-line-before": [ "at-rule-empty-line-before": [
"always", "always",
{ {
"except": [ "except": ["blockless-after-same-name-blockless", "first-nested"],
"blockless-after-same-name-blockless", "ignore": ["after-comment"],
"first-nested" "ignoreAtRules": ["else"]
],
"ignore": [
"after-comment"
],
"ignoreAtRules": [
"else"
]
} }
], ],
"block-closing-brace-newline-after": [ "block-closing-brace-newline-after": [
"always", "always",
{ {
"ignoreAtRules": [ "ignoreAtRules": ["if", "else"]
"if",
"else"
]
} }
], ],
"max-line-length": 125, "max-line-length": 125,
"no-empty-source": null, "no-empty-source": null,
"number-leading-zero": "never", "number-leading-zero": null,
"scss/at-rule-no-unknown": null, "scss/at-rule-no-unknown": null,
"scss/at-else-if-parentheses-space-before": "always", "scss/at-else-if-parentheses-space-before": "always",
"scss/at-function-parentheses-space-before": "never", "scss/at-function-parentheses-space-before": "never",

View File

@ -1,4 +1,4 @@
<div class="card" [ngClass]="{'nomargin': nomargin}"> <div class="card" [ngClass]="{'nomargin': nomargin, 'warn': warn}">
<div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}"> <div *ngIf="title || description" class="header" [ngClass]="{'bottom-margin': expanded}">
<div *ngIf="title" class="row"> <div *ngIf="title" class="row">
<h2 class="title">{{title}}</h2> <h2 class="title">{{title}}</h2>

View File

@ -11,14 +11,13 @@ import { Component, Input } from '@angular/core';
style({ height: '0', opacity: 0 }), style({ height: '0', opacity: 0 }),
animate('150ms ease-in-out', style({ height: '*', opacity: 1 })), animate('150ms ease-in-out', style({ height: '*', opacity: 1 })),
]), ]),
transition(':leave', [ transition(':leave', [animate('150ms ease-in-out', style({ height: '0', opacity: 0 }))]),
animate('150ms ease-in-out', style({ height: '0', opacity: 0 })),
]),
]), ]),
], ],
}) })
export class CardComponent { export class CardComponent {
@Input() public expanded: boolean = true; @Input() public expanded: boolean = true;
@Input() public warn: boolean = false;
@Input() public title: string = ''; @Input() public title: string = '';
@Input() public description: string = ''; @Input() public description: string = '';
@Input() public animate: boolean = false; @Input() public animate: boolean = false;

View File

@ -7,19 +7,23 @@
$background: map-get($theme, background); $background: map-get($theme, background);
$card-background-color: mat.get-color-from-palette($background, card); $card-background-color: mat.get-color-from-palette($background, card);
$is-dark-theme: map-get($theme, is-dark); $is-dark-theme: map-get($theme, is-dark);
$border-color: if($is-dark-theme, rgba(#8795a1, .2), rgba(#8795a1, .2)); $border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #ffffff, #000000); $border-selected-color: if($is-dark-theme, #ffffff, #000000);
/* stylelint-enable */ /* stylelint-enable */
.card { .card {
background-color: $card-background-color; background-color: $card-background-color;
transition: background-color .3s cubic-bezier(.645, .045, .355, 1); transition: background-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
border: 1px solid $border-color; border: 1px solid $border-color;
box-sizing: border-box; box-sizing: border-box;
border-radius: .5rem; border-radius: 0.5rem;
outline: none; outline: none;
height: 100%; height: 100%;
&.warn {
border-color: var(--warn);
}
.selection-icon { .selection-icon {
opacity: 0; opacity: 0;
position: absolute; position: absolute;

View File

@ -55,6 +55,17 @@
<ng-template cnslHasFeature [hasFeature]="['metadata.user']"> <ng-template cnslHasFeature [hasFeature]="['metadata.user']">
<cnsl-metadata *ngIf="user?.id" [userId]="user.id"></cnsl-metadata> <cnsl-metadata *ngIf="user?.id" [userId]="user.id"></cnsl-metadata>
</ng-template> </ng-template>
<ng-template cnslHasRole [hasRole]="['user.self.delete']">
<cnsl-card title="{{'USER.PAGES.DELETEACCOUNT'| translate}}" [warn]="true">
<p>{{'USER.PAGES.DELETEACCOUNT_DESC'| translate}}</p>
<div class="delete-account-wrapper">
<button color="warn" mat-raised-button (click)="deleteAccount()">{{'USER.PAGES.DELETEACCOUNT_BTN' |
translate}}</button>
</div>
</cnsl-card>
</ng-template>
</div> </div>
<div *ngIf="user" class="side" metainfo> <div *ngIf="user" class="side" metainfo>

View File

@ -60,3 +60,8 @@
.side-padding { .side-padding {
padding-top: 1rem; padding-top: 1rem;
} }
.delete-account-wrapper {
display: flex;
justify-content: flex-end;
}

View File

@ -4,7 +4,9 @@ import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { ChangeType } from 'src/app/modules/changes/changes.component'; import { ChangeType } from 'src/app/modules/changes/changes.component';
import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource'; import { UserGrantContext } from 'src/app/modules/user-grants/user-grants-datasource';
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb'; import { Email, Gender, Phone, Profile, User, UserState } from 'src/app/proto/generated/zitadel/user_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service'; import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
@ -36,6 +38,7 @@ export class AuthUserDetailComponent implements OnDestroy {
private toast: ToastService, private toast: ToastService,
public userService: GrpcAuthService, public userService: GrpcAuthService,
private dialog: MatDialog, private dialog: MatDialog,
private auth: AuthenticationService,
) { ) {
this.loading = true; this.loading = true;
this.refreshUser(); this.refreshUser();
@ -252,4 +255,30 @@ export class AuthUserDetailComponent implements OnDestroy {
break; break;
} }
} }
public deleteAccount(): void {
const dialogRef = this.dialog.open(WarnDialogComponent, {
data: {
confirmKey: 'USER.DIALOG.DELETE_BTN',
cancelKey: 'ACTIONS.CANCEL',
titleKey: 'USER.DIALOG.DELETE_TITLE',
descriptionKey: 'USER.DIALOG.DELETE_AUTH_DESCRIPTION',
},
width: '400px',
});
dialogRef.afterClosed().subscribe((resp) => {
if (resp) {
this.userService
.RemoveMyUser()
.then(() => {
this.toast.showInfo('USER.PAGES.DELETEACCOUNT_SUCCESS', true);
this.auth.signout();
})
.catch((error) => {
this.toast.showError(error);
});
}
});
}
} }

View File

@ -56,6 +56,8 @@ import {
RemoveMyPasswordlessResponse, RemoveMyPasswordlessResponse,
RemoveMyPhoneRequest, RemoveMyPhoneRequest,
RemoveMyPhoneResponse, RemoveMyPhoneResponse,
RemoveMyUserRequest,
RemoveMyUserResponse,
ResendMyEmailVerificationRequest, ResendMyEmailVerificationRequest,
ResendMyEmailVerificationResponse, ResendMyEmailVerificationResponse,
ResendMyPhoneVerificationRequest, ResendMyPhoneVerificationRequest,
@ -400,6 +402,11 @@ export class GrpcAuthService {
return this.grpcService.auth.listMyMemberships(req, null).then((resp) => resp.toObject()); return this.grpcService.auth.listMyMemberships(req, null).then((resp) => resp.toObject());
} }
public RemoveMyUser(): Promise<RemoveMyUserResponse.AsObject> {
const req = new RemoveMyUserRequest();
return this.grpcService.auth.removeMyUser(req, null).then((resp) => resp.toObject());
}
public getMyEmail(): Promise<GetMyEmailResponse.AsObject> { public getMyEmail(): Promise<GetMyEmailResponse.AsObject> {
const req = new GetMyEmailRequest(); const req = new GetMyEmailRequest();
return this.grpcService.auth.getMyEmail(req, null).then((resp) => resp.toObject()); return this.grpcService.auth.getMyEmail(req, null).then((resp) => resp.toObject());

View File

@ -182,7 +182,11 @@
"STATE": "Status", "STATE": "Status",
"DELETE": "Benutzer löschen", "DELETE": "Benutzer löschen",
"UNLOCK": "Benutzer entsperren", "UNLOCK": "Benutzer entsperren",
"LOCKEDDESCRIPTION": "Dieser Benutzer wurde aufgrund der Überschreitung der maximalen Anmeldeversuche gesperrt und muss zur erneuten Verwendung entsperrt werden." "LOCKEDDESCRIPTION": "Dieser Benutzer wurde aufgrund der Überschreitung der maximalen Anmeldeversuche gesperrt und muss zur erneuten Verwendung entsperrt werden.",
"DELETEACCOUNT": "Account löschen",
"DELETEACCOUNT_DESC": "Wenn du diese Aktion ausführst, wirst du abgemeldet und danach keinen Zugriff mehr auf dein Konto haben. Diese Aktion kann nicht rückgängig gemacht werden.",
"DELETEACCOUNT_BTN": "Account löschen",
"DELETEACCOUNT_SUCCESS": "Account erfolgreich gelöscht!"
}, },
"DETAILS": { "DETAILS": {
"DATECREATED": "Erstellt", "DATECREATED": "Erstellt",
@ -190,7 +194,9 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "User löschen", "DELETE_TITLE": "User löschen",
"DELETE_DESCRIPTION": "Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?" "DELETE_DESCRIPTION": "Sie sind im Begriff einen Benutzer endgültig zu löschen. Wollen Sie dies wirklich tun?",
"DELETE_AUTH_DESCRIPTION": "Sie sind im Begriff Ihren Account endgültig zu löschen. Wollen Sie dies wirklich tun?",
"DELETE_BTN": "Entgültig löschen"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
"TITLE": "Email Benachrichtigung senden", "TITLE": "Email Benachrichtigung senden",

View File

@ -182,7 +182,11 @@
"STATE": "Status", "STATE": "Status",
"DELETE": "Delete User", "DELETE": "Delete User",
"UNLOCK": "Unlock User", "UNLOCK": "Unlock User",
"LOCKEDDESCRIPTION": "This user has been locked out due to exceeding the maximum login attempts and must be unlocked to be used again." "LOCKEDDESCRIPTION": "This user has been locked out due to exceeding the maximum login attempts and must be unlocked to be used again.",
"DELETEACCOUNT": "Delete Account",
"DELETEACCOUNT_DESC": "If you perform this action, you will be logged out and will no longer have access to your account. This action is not reversible, so please continue with caution.",
"DELETEACCOUNT_BTN": "Delete Account",
"DELETEACCOUNT_SUCCESS": "Account deleted successfully!"
}, },
"DETAILS": { "DETAILS": {
"DATECREATED": "Created", "DATECREATED": "Created",
@ -190,7 +194,9 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "Delete User", "DELETE_TITLE": "Delete User",
"DELETE_DESCRIPTION": "You are about to permanently delete a user. Are you sure?" "DELETE_DESCRIPTION": "You are about to permanently delete a user. Are you sure?",
"DELETE_AUTH_DESCRIPTION": "You are about to permanently delete your personal account. Are you sure?",
"DELETE_BTN": "Delete permanently"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
"TITLE": "Send Email Notification", "TITLE": "Send Email Notification",

View File

@ -182,7 +182,11 @@
"STATE": "Stato", "STATE": "Stato",
"DELETE": "Elimina utente", "DELETE": "Elimina utente",
"UNLOCK": "Sblocca utente", "UNLOCK": "Sblocca utente",
"LOCKEDDESCRIPTION": "Questo utente \u00e8 stato bloccato a causa del superamento dei tentativi massimi di accesso e deve essere sbloccato per essere utilizzato di nuovo." "LOCKEDDESCRIPTION": "Questo utente \u00e8 stato bloccato a causa del superamento dei tentativi massimi di accesso e deve essere sbloccato per essere utilizzato di nuovo.",
"DELETEACCOUNT": "Elimina account personale",
"DELETEACCOUNT_DESC": "Se esegui questa azione, sarai disconnesso e non avrai più accesso al tuo account. Questa azione non può essere invertita.",
"DELETEACCOUNT_BTN": "Elimina",
"DELETEACCOUNT_SUCCESS": "Account eliminato con successo!"
}, },
"DETAILS": { "DETAILS": {
"DATECREATED": "Creato", "DATECREATED": "Creato",
@ -190,7 +194,9 @@
}, },
"DIALOG": { "DIALOG": {
"DELETE_TITLE": "Elimina utente", "DELETE_TITLE": "Elimina utente",
"DELETE_DESCRIPTION": "Stai per eliminare definitivamente un utente. Sei sicuro?" "DELETE_DESCRIPTION": "Stai per eliminare definitivamente un utente. Sei sicuro?",
"DELETE_AUTH_DESCRIPTION": "Stai per eleminare il tuo account personale in modo permanente. Vuoi continuare?",
"DELETE_BTN": "Elimina"
}, },
"SENDEMAILDIALOG": { "SENDEMAILDIALOG": {
"TITLE": "Invia una notifica via e-mail", "TITLE": "Invia una notifica via e-mail",

View File

@ -43,6 +43,18 @@ Returns my full blown user
GET: /users/me GET: /users/me
### RemoveMyUser
> **rpc** RemoveMyUser([RemoveMyUserRequest](#removemyuserrequest))
[RemoveMyUserResponse](#removemyuserresponse)
Changes the user state to deleted
DELETE: /users/me
### ListMyUserChanges ### ListMyUserChanges
> **rpc** ListMyUserChanges([ListMyUserChangesRequest](#listmyuserchangesrequest)) > **rpc** ListMyUserChanges([ListMyUserChangesRequest](#listmyuserchangesrequest))
@ -1250,6 +1262,24 @@ This is an empty request
### RemoveMyUserRequest
This is an empty request
the request parameters are read from the token-header
### RemoveMyUserResponse
| Field | Type | Description | Validation |
| ----- | ---- | ----------- | ----------- |
| details | zitadel.v1.ObjectDetails | - | |
### ResendMyEmailVerificationRequest ### ResendMyEmailVerificationRequest
This is an empty request This is an empty request

View File

@ -11,7 +11,9 @@ import (
obj_grpc "github.com/caos/zitadel/internal/api/grpc/object" obj_grpc "github.com/caos/zitadel/internal/api/grpc/object"
"github.com/caos/zitadel/internal/api/grpc/org" "github.com/caos/zitadel/internal/api/grpc/org"
user_grpc "github.com/caos/zitadel/internal/api/grpc/user" user_grpc "github.com/caos/zitadel/internal/api/grpc/user"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/eventstore/v1/models"
user_model "github.com/caos/zitadel/internal/user/model"
grant_model "github.com/caos/zitadel/internal/usergrant/model" grant_model "github.com/caos/zitadel/internal/usergrant/model"
auth_pb "github.com/caos/zitadel/pkg/grpc/auth" auth_pb "github.com/caos/zitadel/pkg/grpc/auth"
) )
@ -24,6 +26,25 @@ func (s *Server) GetMyUser(ctx context.Context, _ *auth_pb.GetMyUserRequest) (*a
return &auth_pb.GetMyUserResponse{User: user_grpc.UserToPb(user)}, nil return &auth_pb.GetMyUserResponse{User: user_grpc.UserToPb(user)}, nil
} }
func (s *Server) RemoveMyUser(ctx context.Context, _ *auth_pb.RemoveMyUserRequest) (*auth_pb.RemoveMyUserResponse, error) {
ctxData := authz.GetCtxData(ctx)
grants, err := s.repo.SearchMyUserGrants(ctx, &grant_model.UserGrantSearchRequest{Queries: []*grant_model.UserGrantSearchQuery{}})
if err != nil {
return nil, err
}
membersShips, err := s.repo.SearchMyUserMemberships(ctx, &user_model.UserMembershipSearchRequest{Queries: []*user_model.UserMembershipSearchQuery{}})
if err != nil {
return nil, err
}
details, err := s.command.RemoveUser(ctx, ctxData.UserID, ctxData.ResourceOwner, UserMembershipViewsToDomain(membersShips.Result), userGrantsToIDs(grants.Result)...)
if err != nil {
return nil, err
}
return &auth_pb.RemoveMyUserResponse{
Details: obj_grpc.DomainToChangeDetailsPb(details),
}, nil
}
func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserChangesRequest) (*auth_pb.ListMyUserChangesResponse, error) { func (s *Server) ListMyUserChanges(ctx context.Context, req *auth_pb.ListMyUserChangesRequest) (*auth_pb.ListMyUserChangesResponse, error) {
sequence, limit, asc := change.ChangeQueryToModel(req.Query) sequence, limit, asc := change.ChangeQueryToModel(req.Query)
features, err := s.query.FeaturesByOrgID(ctx, authz.GetCtxData(ctx).ResourceOwner) features, err := s.query.FeaturesByOrgID(ctx, authz.GetCtxData(ctx).ResourceOwner)
@ -137,3 +158,46 @@ func ListMyProjectOrgsRequestToModel(req *auth_pb.ListMyProjectOrgsRequest) (*gr
Queries: queries, Queries: queries,
}, nil }, nil
} }
func UserMembershipViewsToDomain(memberships []*user_model.UserMembershipView) []*domain.UserMembership {
result := make([]*domain.UserMembership, len(memberships))
for i, membership := range memberships {
result[i] = &domain.UserMembership{
UserID: membership.UserID,
MemberType: MemberTypeToDomain(membership.MemberType),
AggregateID: membership.AggregateID,
ObjectID: membership.ObjectID,
Roles: membership.Roles,
DisplayName: membership.DisplayName,
CreationDate: membership.CreationDate,
ChangeDate: membership.ChangeDate,
ResourceOwner: membership.ResourceOwner,
ResourceOwnerName: membership.ResourceOwnerName,
Sequence: membership.Sequence,
}
}
return result
}
func MemberTypeToDomain(mType user_model.MemberType) domain.MemberType {
switch mType {
case user_model.MemberTypeIam:
return domain.MemberTypeIam
case user_model.MemberTypeOrganisation:
return domain.MemberTypeOrganisation
case user_model.MemberTypeProject:
return domain.MemberTypeProject
case user_model.MemberTypeProjectGrant:
return domain.MemberTypeProjectGrant
default:
return domain.MemberTypeUnspecified
}
}
func userGrantsToIDs(userGrants []*grant_model.UserGrantView) []string {
converted := make([]string, len(userGrants))
for i, grant := range userGrants {
converted[i] = grant.ID
}
return converted
}

View File

@ -0,0 +1,103 @@
package command
import (
"context"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/repository/user"
)
type Step21 struct{}
func (s *Step21) Step() domain.Step {
return domain.Step21
}
func (s *Step21) execute(ctx context.Context, commandSide *Commands) error {
return commandSide.SetupStep21(ctx, s)
}
func (c *Commands) SetupStep21(ctx context.Context, step *Step21) error {
fn := func(iam *IAMWriteModel) ([]eventstore.EventPusher, error) {
events := make([]eventstore.EventPusher, 0)
globalMembers := newGlobalOrgMemberWriteModel(iam.GlobalOrgID, domain.RoleOrgProjectCreator)
orgAgg := OrgAggregateFromWriteModel(&globalMembers.WriteModel)
if err := c.eventstore.FilterToQueryReducer(ctx, globalMembers); err != nil {
return nil, err
}
for userID, roles := range globalMembers.members {
for i, role := range roles {
if role == domain.RoleOrgProjectCreator {
roles[i] = domain.RoleSelfManagementGlobal
}
}
events = append(events, org.NewMemberChangedEvent(ctx, orgAgg, userID, roles...))
}
return events, nil
}
return c.setup(ctx, step, fn)
}
type globalOrgMembersWriteModel struct {
eventstore.WriteModel
orgID string
role string
members map[string][]string
}
func newGlobalOrgMemberWriteModel(orgID, role string) *globalOrgMembersWriteModel {
return &globalOrgMembersWriteModel{
orgID: orgID,
role: role,
members: make(map[string][]string),
}
}
func (wm *globalOrgMembersWriteModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *org.MemberAddedEvent:
for _, role := range e.Roles {
if wm.role == role {
wm.members[e.UserID] = e.Roles
break
}
}
case *org.MemberChangedEvent:
delete(wm.members, e.UserID)
for _, role := range e.Roles {
if wm.role == role {
wm.members[e.UserID] = e.Roles
break
}
}
case *org.MemberRemovedEvent:
delete(wm.members, e.UserID)
case *org.MemberCascadeRemovedEvent:
delete(wm.members, e.UserID)
case *user.UserRemovedEvent:
delete(wm.members, e.Aggregate().ID)
}
}
return wm.WriteModel.Reduce()
}
func (wm *globalOrgMembersWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
AddQuery().
AggregateTypes(org.AggregateType).
AggregateIDs(wm.orgID).
EventTypes(
org.MemberAddedEventType,
org.MemberChangedEventType,
org.MemberRemovedEventType,
org.MemberCascadeRemovedEventType,
).
Or().
AggregateTypes(user.AggregateType).
EventTypes(user.UserRemovedType).
Builder()
}

View File

@ -16,6 +16,7 @@ const (
RoleIAMOwner = "IAM_OWNER" RoleIAMOwner = "IAM_OWNER"
RoleProjectOwner = "PROJECT_OWNER" RoleProjectOwner = "PROJECT_OWNER"
RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL" RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL"
RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL"
) )
func CheckForInvalidRoles(roles []string, rolePrefix string, validRoles []authz.RoleMapping) []string { func CheckForInvalidRoles(roles []string, rolePrefix string, validRoles []authz.RoleMapping) []string {

View File

@ -23,6 +23,7 @@ const (
Step18 Step18
Step19 Step19
Step20 Step20
Step21
//StepCount marks the the length of possible steps (StepCount-1 == last possible step) //StepCount marks the the length of possible steps (StepCount-1 == last possible step)
StepCount StepCount
) )

View File

@ -3,9 +3,10 @@ package project
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/caos/zitadel/internal/eventstore"
"time" "time"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/repository" "github.com/caos/zitadel/internal/eventstore/repository"

View File

@ -26,6 +26,7 @@ type IAMSetUp struct {
Step18 *command.Step18 Step18 *command.Step18
Step19 *command.Step19 Step19 *command.Step19
Step20 *command.Step20 Step20 *command.Step20
Step21 *command.Step21
} }
func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) { func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
@ -52,6 +53,7 @@ func (setup *IAMSetUp) Steps(currentDone domain.Step) ([]command.Step, error) {
setup.Step18, setup.Step18,
setup.Step19, setup.Step19,
setup.Step20, setup.Step20,
setup.Step21,
} { } {
if step.Step() <= currentDone { if step.Step() <= currentDone {
continue continue

View File

@ -259,7 +259,7 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
} }
resourceOwner := iam.GlobalOrgID resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator} memberRoles := []string{domain.RoleSelfManagementGlobal}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID { if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil memberRoles = nil

View File

@ -136,7 +136,7 @@ func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Reques
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, iam *iam_model.IAM, user *domain.Human, externalIDP *domain.UserIDPLink) { func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, iam *iam_model.IAM, user *domain.Human, externalIDP *domain.UserIDPLink) {
resourceOwner := iam.GlobalOrgID resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator} memberRoles := []string{domain.RoleSelfManagementGlobal}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner { if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
memberRoles = nil memberRoles = nil
@ -196,7 +196,7 @@ func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Reque
return return
} }
resourceOwner := iam.GlobalOrgID resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator} memberRoles := []string{domain.RoleSelfManagementGlobal}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID { if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil memberRoles = nil

View File

@ -68,7 +68,7 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
} }
resourceOwner := iam.GlobalOrgID resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator} memberRoles := []string{domain.RoleSelfManagementGlobal}
if authRequest.RequestedOrgID != "" && authRequest.RequestedOrgID != iam.GlobalOrgID { if authRequest.RequestedOrgID != "" && authRequest.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil memberRoles = nil

View File

@ -83,6 +83,17 @@ service AuthService {
}; };
} }
// Changes the user state to deleted
rpc RemoveMyUser(RemoveMyUserRequest) returns (RemoveMyUserResponse) {
option (google.api.http) = {
delete: "/users/me"
};
option (zitadel.v1.auth_option) = {
permission: "user.self.delete"
};
}
// Returns the history of the authorized user (each event) // Returns the history of the authorized user (each event)
rpc ListMyUserChanges(ListMyUserChangesRequest) returns (ListMyUserChangesResponse) { rpc ListMyUserChanges(ListMyUserChangesRequest) returns (ListMyUserChangesResponse) {
option (google.api.http) = { option (google.api.http) = {
@ -614,6 +625,14 @@ message GetMyUserResponse {
]; ];
} }
//This is an empty request
// the request parameters are read from the token-header
message RemoveMyUserRequest {}
message RemoveMyUserResponse{
zitadel.v1.ObjectDetails details = 1;
}
message ListMyUserChangesRequest { message ListMyUserChangesRequest {
zitadel.change.v1.ChangeQuery query = 1; zitadel.change.v1.ChangeQuery query = 1;
} }