feat: support client_credentials for service users (#5134)

Request an access_token for service users with OAuth 2.0 Client Credentials Grant. Added functionality to generate and remove a secret on service users.
This commit is contained in:
Stefan Benz
2023-01-31 20:52:47 +01:00
committed by GitHub
parent 7c7c93117b
commit e2fdd3f077
48 changed files with 2113 additions and 311 deletions

View File

@@ -55,6 +55,7 @@ import { PasswordComponent } from './password/password.component';
import { PasswordlessComponent } from './user-detail/passwordless/passwordless.component';
import { UserDetailComponent } from './user-detail/user-detail.component';
import { UserMfaComponent } from './user-detail/user-mfa/user-mfa.component';
import { MachineSecretDialogComponent } from './user-detail/machine-secret-dialog/machine-secret-dialog.component';
import { MetadataModule } from 'src/app/modules/metadata/metadata.module';
import { QRCodeModule } from 'angularx-qrcode';
@@ -75,6 +76,7 @@ import { QRCodeModule } from 'angularx-qrcode';
DialogU2FComponent,
DialogPasswordlessComponent,
AuthFactorDialogComponent,
MachineSecretDialogComponent,
],
imports: [
ChangesModule,

View File

@@ -0,0 +1,50 @@
<h1 mat-dialog-title>
<span class="title">{{ 'USER.SECRETDIALOG.CLIENTSECRET' | translate }}</span>
</h1>
<p class="desc cnsl-secondary-text">{{ 'USER.SECRETDIALOG.CLIENTSECRET_DESCRIPTION' | translate }}</p>
<div mat-dialog-content>
<div class="flex" *ngIf="data.clientId">
<span class="overflow-auto" data-e2e="client-id"><span class="desc">ClientId:</span> {{ data.clientId }}</span>
<button
color="primary"
[disabled]="copied === data.clientId"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientId"
(copiedValue)="this.copied = $event"
mat-icon-button
data-e2e="client-id-copy"
>
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>
</button>
</div>
<div *ngIf="data.clientSecret" class="flex">
<span class="overflow-auto"><span class="desc cnsl-secondary-text">ClientSecret:</span> {{ data.clientSecret }}</span>
<button
color="primary"
[disabled]="copied === data.clientSecret"
matTooltip="copy to clipboard"
cnslCopyToClipboard
[valueToCopy]="data.clientSecret"
(copiedValue)="this.copied = $event"
mat-icon-button
>
<i *ngIf="copied !== data.clientSecret" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientSecret" class="las la-clipboard-check"></i>
</button>
</div>
</div>
<div mat-dialog-actions class="action">
<button
cdkFocusInitial
color="primary"
mat-raised-button
class="ok-button"
(click)="closeDialog()"
data-e2e="close-dialog"
>
{{ 'ACTIONS.CLOSE' | translate }}
</button>
</div>

View File

@@ -0,0 +1,37 @@
.title {
font-size: 1.2rem;
}
.desc {
font-size: 0.9rem;
}
.full-width {
width: 100%;
}
.action {
display: flex;
justify-content: flex-end;
.ok-button {
margin-left: 0.5rem;
}
}
.flex {
display: flex;
align-items: center;
border: 1px solid #ffffff20;
border-radius: 0.5rem;
padding-left: 0.5rem;
justify-content: space-between;
.overflow-auto {
overflow: auto;
.desc {
font-size: 14px;
}
}
}

View File

@@ -0,0 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MachineSecretDialogComponent } from './machine-secret-dialog.component';
describe('MachineSecretDialogComponent', () => {
let component: MachineSecretDialogComponent;
let fixture: ComponentFixture<MachineSecretDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MachineSecretDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MachineSecretDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,19 @@
import { Component, Inject } from '@angular/core';
import {
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef,
} from '@angular/material/legacy-dialog';
@Component({
selector: 'cnsl-machine-secret-dialog',
templateUrl: './machine-secret-dialog.component.html',
styleUrls: ['./machine-secret-dialog.component.scss'],
})
export class MachineSecretDialogComponent {
public copied: string = '';
constructor(public dialogRef: MatDialogRef<MachineSecretDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {}
public closeDialog(): void {
this.dialogRef.close(false);
}
}

View File

@@ -10,6 +10,12 @@
[hasActions]="['user.write$', 'user.write:' + user.id] | hasRole | async"
>
<ng-template topActions cnslHasRole [hasRole]="['user.write$', 'user.write:' + user.id]">
<button mat-menu-item color="warn" *ngIf="user?.machine" (click)="generateMachineSecret()">
{{ 'USER.PAGES.GENERATESECRET' | translate }}
</button>
<button mat-menu-item color="warn" *ngIf="user?.machine?.hasSecret" (click)="removeMachineSecret()">
{{ 'USER.PAGES.REMOVESECRET' | translate }}
</button>
<button mat-menu-item color="warn" *ngIf="user?.state === UserState.USER_STATE_LOCKED" (click)="unlockUser()">
{{ 'USER.PAGES.UNLOCK' | translate }}
</button>

View File

@@ -21,6 +21,7 @@ import { Buffer } from 'buffer';
import { EditDialogComponent, EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
import { ResendEmailDialogComponent } from '../auth-user-detail/resend-email-dialog/resend-email-dialog.component';
import { LoginPolicy } from 'src/app/proto/generated/zitadel/policy_pb';
import { MachineSecretDialogComponent } from './machine-secret-dialog/machine-secret-dialog.component';
const GENERAL: SidenavSetting = { id: 'general', i18nKey: 'USER.SETTINGS.GENERAL' };
const GRANTS: SidenavSetting = { id: 'grants', i18nKey: 'USER.SETTINGS.USERGRANTS' };
@@ -189,6 +190,38 @@ export class UserDetailComponent implements OnInit {
});
}
public generateMachineSecret(): void {
this.mgmtUserService
.generateMachineSecret(this.user.id)
.then((resp) => {
this.toast.showInfo('USER.TOAST.SECRETGENERATED', true);
console.log(resp.clientSecret);
this.dialog.open(MachineSecretDialogComponent, {
data: {
clientId: resp.clientId,
clientSecret: resp.clientSecret,
},
width: '400px',
});
this.refreshUser();
})
.catch((error) => {
this.toast.showError(error);
});
}
public removeMachineSecret(): void {
this.mgmtUserService
.removeMachineSecret(this.user.id)
.then((resp) => {
this.toast.showInfo('USER.TOAST.SECRETREMOVED', true);
this.refreshUser();
})
.catch((error) => {
this.toast.showError(error);
});
}
public changeState(newState: UserState): void {
if (newState === UserState.USER_STATE_ACTIVE) {
this.mgmtUserService

View File

@@ -98,6 +98,8 @@ import {
DeactivateUserResponse,
DeleteActionRequest,
DeleteActionResponse,
GenerateMachineSecretRequest,
GenerateMachineSecretResponse,
GenerateOrgDomainValidationRequest,
GenerateOrgDomainValidationResponse,
GetActionRequest,
@@ -310,6 +312,8 @@ import {
RemoveIDPFromLoginPolicyResponse,
RemoveMachineKeyRequest,
RemoveMachineKeyResponse,
RemoveMachineSecretRequest,
RemoveMachineSecretResponse,
RemoveMultiFactorFromLoginPolicyRequest,
RemoveMultiFactorFromLoginPolicyResponse,
RemoveOrgDomainRequest,
@@ -717,6 +721,18 @@ export class ManagementService {
return this.grpcService.mgmt.unlockUser(req, null).then((resp) => resp.toObject());
}
public generateMachineSecret(userId: string): Promise<GenerateMachineSecretResponse.AsObject> {
const req = new GenerateMachineSecretRequest();
req.setUserId(userId);
return this.grpcService.mgmt.generateMachineSecret(req, null).then((resp) => resp.toObject());
}
public removeMachineSecret(userId: string): Promise<RemoveMachineSecretResponse.AsObject> {
const req = new RemoveMachineSecretRequest();
req.setUserId(userId);
return this.grpcService.mgmt.removeMachineSecret(req, null).then((resp) => resp.toObject());
}
public getPrivacyPolicy(): Promise<GetPrivacyPolicyResponse.AsObject> {
const req = new GetPrivacyPolicyRequest();
return this.grpcService.mgmt.getPrivacyPolicy(req, null).then((resp) => resp.toObject());

View File

@@ -240,6 +240,8 @@
"STATE": "Status",
"DELETE": "Benutzer löschen",
"UNLOCK": "Benutzer entsperren",
"GENERATESECRET": "Client Secret generieren",
"REMOVESECRET": "Client Secret löschen",
"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.",
@@ -265,6 +267,10 @@
"DESCRIPTION": "Klicken Sie den untenstehenden Button um ein Verifizierung-E-Mail an die aktuelle Adresse zu versenden oder ändern Sie die Emailadresse in dem Feld.",
"NEWEMAIL": "Neue Email"
},
"SECRETDIALOG": {
"CLIENTSECRET": "Client Secret",
"CLIENTSECRET_DESCRIPTION": "Verwahre das Client Secret an einem sicheren Ort, da es nicht mehr angezeigt werden kann, sobald der Dialog geschlossen wird."
},
"TABLE": {
"DEACTIVATE": "Deaktivieren",
"ACTIVATE": "Aktivieren",
@@ -589,7 +595,9 @@
"MACHINEADDED": "Service User erstellt!",
"DELETED": "Benutzer erfolgreich gelöscht!",
"UNLOCKED": "Benutzer erfolgreich freigeschaltet!",
"PASSWORDLESSREGISTRATIONSENT": "Link via email versendet."
"PASSWORDLESSREGISTRATIONSENT": "Link via email versendet.",
"SECRETGENERATED": "Secret erfolgreich generiert!",
"SECRETREMOVED": "Secret erfolgreich gelöscht!"
},
"MEMBERSHIPS": {
"TITLE": "ZITADEL Manager-Rollen",

View File

@@ -240,6 +240,8 @@
"STATE": "Status",
"DELETE": "Delete User",
"UNLOCK": "Unlock User",
"GENERATESECRET": "Generate Client Secret",
"REMOVESECRET": "Remove Client Secret",
"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.",
@@ -265,6 +267,10 @@
"DESCRIPTION": "Click the button below to send a notification to the current email address or change the email address in the field.",
"NEWEMAIL": "New email address"
},
"SECRETDIALOG": {
"CLIENTSECRET": "Client Secret",
"CLIENTSECRET_DESCRIPTION": "Keep your client secret at a safe place as it will disappear once the dialog is closed."
},
"TABLE": {
"DEACTIVATE": "Deactivate",
"ACTIVATE": "Activate",
@@ -589,7 +595,9 @@
"MACHINEADDED": "Service User created!",
"DELETED": "User deleted successfully!",
"UNLOCKED": "User unlocked successfully!",
"PASSWORDLESSREGISTRATIONSENT": "Registration Link sent successfully."
"PASSWORDLESSREGISTRATIONSENT": "Registration Link sent successfully.",
"SECRETGENERATED": "Secret generated successfully!",
"SECRETREMOVED": "Secret removed successfully!"
},
"MEMBERSHIPS": {
"TITLE": "ZITADEL Manager Roles",

View File

@@ -240,6 +240,8 @@
"STATE": "Statut",
"DELETE": "Supprimer l'utilisateur",
"UNLOCK": "Déverrouiller l'utilisateur",
"GENERATESECRET": "Générer Client Secret",
"REMOVESECRET": "Supprimer Client Secret",
"LOCKEDDESCRIPTION": "Cet utilisateur a été verrouillé pour avoir dépassé le nombre maximum de tentatives de connexion et doit être déverrouillé pour être à nouveau utilisé.",
"DELETEACCOUNT": "Supprimer le compte",
"DELETEACCOUNT_DESC": "Si vous effectuez cette action, vous serez déconnecté et n'aurez plus accès à votre compte. Cette action n'est pas réversible, veuillez donc continuer avec prudence.",
@@ -265,6 +267,10 @@
"DESCRIPTION": "Cliquez sur le bouton ci-dessous pour envoyer une notification à l'adresse e-mail actuelle ou modifier l'adresse e-mail dans le champ.",
"NEWEMAIL": "Nouvelle adresse e-mail"
},
"SECRETDIALOG": {
"CLIENTSECRET": "Client Secret",
"CLIENTSECRET_DESCRIPTION": "Conservez votre secret client dans un endroit sûr car il disparaîtra une fois la boîte de dialogue fermée."
},
"TABLE": {
"DEACTIVATE": "Désactiver",
"ACTIVATE": "Activer",
@@ -589,7 +595,9 @@
"MACHINEADDED": "Utilisateur de service créé !",
"DELETED": "Utilisateur supprimé avec succès !",
"UNLOCKED": "Utilisateur déverrouillé avec succès !",
"PASSWORDLESSREGISTRATIONSENT": "Lien d'enregistrement envoyé avec succès."
"PASSWORDLESSREGISTRATIONSENT": "Lien d'enregistrement envoyé avec succès.",
"SECRETGENERATED": "Secret généré avec succès !",
"SECRETREMOVED": "Secret supprimé avec succès !"
},
"MEMBERSHIPS": {
"TITLE": "Rôles du gestionnaire ZITADEL",

View File

@@ -240,6 +240,8 @@
"STATE": "Stato",
"DELETE": "Elimina utente",
"UNLOCK": "Sblocca utente",
"GENERATESECRET": "Genera Client Secret",
"REMOVESECRET": "Elimina Client Secret",
"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.",
@@ -265,6 +267,10 @@
"DESCRIPTION": "Clicca il pulsante qui sotto per inviare una notifica all'indirizzo email corrente o cambiare l'indirizzo email nel campo.",
"NEWEMAIL": "Nuovo indirizzo e-mail"
},
"SECRETDIALOG": {
"CLIENTSECRET": "Client Secret",
"CLIENTSECRET_DESCRIPTION": "Salvate il Client Secret in un luogo sicuro, perch\u00e9 non sarà più disponibile dopo aver chiuso la finestra di dialogo"
},
"TABLE": {
"DEACTIVATE": "Disattiva",
"ACTIVATE": "Attiva",
@@ -589,7 +595,9 @@
"MACHINEADDED": "Utente di servizio creato!",
"DELETED": "Utente cancellato con successo!",
"UNLOCKED": "Utente sbloccato con successo!",
"PASSWORDLESSREGISTRATIONSENT": "Link per la registrazione inviato con successo."
"PASSWORDLESSREGISTRATIONSENT": "Link per la registrazione inviato con successo.",
"SECRETGENERATED": "Secret generato con successo!",
"SECRETREMOVED": "Secret rimosso con successo!"
},
"MEMBERSHIPS": {
"TITLE": "Memberships di ZITADEL",

View File

@@ -240,6 +240,8 @@
"STATE": "状态",
"DELETE": "删除用户",
"UNLOCK": "解锁用户",
"GENERATESECRET": "生成客户密匙",
"REMOVESECRET": "删除客户密匙",
"LOCKEDDESCRIPTION": "此用户因超过最大登录尝试次数而被锁定,必须解锁才能再次使用。",
"DELETEACCOUNT": "删除账户",
"DELETEACCOUNT_DESC": "如果您执行此操作,您将被注销并且无法再访问您的帐户。此操作不可逆,因此请谨慎操作。",
@@ -265,6 +267,10 @@
"DESCRIPTION": "单击下面的按钮可向当前电子邮件地址发送通知或更改电子邮件地址。",
"NEWEMAIL": "新的电子邮件地址"
},
"SECRETDIALOG": {
"CLIENTSECRET": "客户端秘钥",
"CLIENTSECRET_DESCRIPTION": "将您的客户保密在一个安全的地方,因为一旦对话框关闭,便无法再次查看。"
},
"TABLE": {
"DEACTIVATE": "停用",
"ACTIVATE": "启用",
@@ -589,7 +595,9 @@
"MACHINEADDED": "服务用户已创建成功!",
"DELETED": "用户删除成功!",
"UNLOCKED": "用户解锁成功!",
"PASSWORDLESSREGISTRATIONSENT": "注册链接发送成功。"
"PASSWORDLESSREGISTRATIONSENT": "注册链接发送成功。",
"SECRETGENERATED": "秘密成功生成!",
"SECRETREMOVED": "秘密被成功删除!"
},
"MEMBERSHIPS": {
"TITLE": "CITADEL 管理角色",