Commit 483a3b847b8266d71e39477094add11836076219

Authored by Vladyslav_Prykhodko
1 parent 7af3fe00

Add tenant menu settingOAuth2

Add validation unique parameters to settings OAuth2
@@ -22,6 +22,7 @@ export interface AuthPayload { @@ -22,6 +22,7 @@ export interface AuthPayload {
22 userTokenAccessEnabled: boolean; 22 userTokenAccessEnabled: boolean;
23 allowedDashboardIds: string[]; 23 allowedDashboardIds: string[];
24 forceFullscreen: boolean; 24 forceFullscreen: boolean;
  25 + allowOAuth2Configuration: boolean;
25 } 26 }
26 27
27 export interface AuthState { 28 export interface AuthState {
@@ -33,4 +34,5 @@ export interface AuthState { @@ -33,4 +34,5 @@ export interface AuthState {
33 allowedDashboardIds: string[]; 34 allowedDashboardIds: string[];
34 forceFullscreen: boolean; 35 forceFullscreen: boolean;
35 lastPublicDashboardId: string; 36 lastPublicDashboardId: string;
  37 + allowOAuth2Configuration: boolean;
36 } 38 }
@@ -22,6 +22,7 @@ const emptyUserAuthState: AuthPayload = { @@ -22,6 +22,7 @@ const emptyUserAuthState: AuthPayload = {
22 userDetails: null, 22 userDetails: null,
23 userTokenAccessEnabled: false, 23 userTokenAccessEnabled: false,
24 forceFullscreen: false, 24 forceFullscreen: false,
  25 + allowOAuth2Configuration: false,
25 allowedDashboardIds: [] 26 allowedDashboardIds: []
26 }; 27 };
27 28
@@ -425,15 +425,25 @@ export class AuthService { @@ -425,15 +425,25 @@ export class AuthService {
425 } 425 }
426 } 426 }
427 427
  428 + private loadIsOAuth2ConfigurationAllow(authUser: AuthUser): Observable<boolean> {
  429 + if (authUser.authority === Authority.TENANT_ADMIN) {
  430 + return this.http.get<boolean>('/api/oauth2/config/isAllowed', defaultHttpOptions());
  431 + } else {
  432 + return of(false);
  433 + }
  434 + }
  435 +
428 private loadSystemParams(authPayload: AuthPayload): Observable<any> { 436 private loadSystemParams(authPayload: AuthPayload): Observable<any> {
429 const sources: Array<Observable<any>> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), 437 const sources: Array<Observable<any>> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser),
430 this.fetchAllowedDashboardIds(authPayload), 438 this.fetchAllowedDashboardIds(authPayload),
  439 + this.loadIsOAuth2ConfigurationAllow(authPayload.authUser),
431 this.timeService.loadMaxDatapointsLimit()]; 440 this.timeService.loadMaxDatapointsLimit()];
432 return forkJoin(sources) 441 return forkJoin(sources)
433 .pipe(map((data) => { 442 .pipe(map((data) => {
434 const userTokenAccessEnabled: boolean = data[0]; 443 const userTokenAccessEnabled: boolean = data[0];
435 const allowedDashboardIds: string[] = data[1]; 444 const allowedDashboardIds: string[] = data[1];
436 - return {userTokenAccessEnabled, allowedDashboardIds}; 445 + const allowOAuth2Configuration: boolean = data[2];
  446 + return {userTokenAccessEnabled, allowedDashboardIds, allowOAuth2Configuration};
437 })); 447 }));
438 } 448 }
439 449
@@ -18,12 +18,13 @@ import { Injectable } from '@angular/core'; @@ -18,12 +18,13 @@ import { Injectable } from '@angular/core';
18 import { AuthService } from '../auth/auth.service'; 18 import { AuthService } from '../auth/auth.service';
19 import { select, Store } from '@ngrx/store'; 19 import { select, Store } from '@ngrx/store';
20 import { AppState } from '../core.state'; 20 import { AppState } from '../core.state';
21 -import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; 21 +import { getCurrentAuthState, selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors';
22 import { take } from 'rxjs/operators'; 22 import { take } from 'rxjs/operators';
23 import { HomeSection, MenuSection } from '@core/services/menu.models'; 23 import { HomeSection, MenuSection } from '@core/services/menu.models';
24 import { BehaviorSubject, Observable, Subject } from 'rxjs'; 24 import { BehaviorSubject, Observable, Subject } from 'rxjs';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 import { AuthUser } from '@shared/models/user.model'; 26 import { AuthUser } from '@shared/models/user.model';
  27 +import { AuthState } from '@core/auth/auth.models';
27 28
28 @Injectable({ 29 @Injectable({
29 providedIn: 'root' 30 providedIn: 'root'
@@ -43,6 +44,8 @@ export class MenuService { @@ -43,6 +44,8 @@ export class MenuService {
43 ); 44 );
44 } 45 }
45 46
  47 + authState: AuthState = getCurrentAuthState(this.store);
  48 +
46 private buildMenu() { 49 private buildMenu() {
47 this.store.pipe(select(selectAuthUser), take(1)).subscribe( 50 this.store.pipe(select(selectAuthUser), take(1)).subscribe(
48 (authUser: AuthUser) => { 51 (authUser: AuthUser) => {
@@ -233,6 +236,25 @@ export class MenuService { @@ -233,6 +236,25 @@ export class MenuService {
233 icon: 'track_changes' 236 icon: 'track_changes'
234 } 237 }
235 ); 238 );
  239 +
  240 + if (this.authState.allowOAuth2Configuration) {
  241 + sections.push({
  242 + name: 'admin.system-settings',
  243 + type: 'toggle',
  244 + path: '/settings',
  245 + height: '40px',
  246 + icon: 'settings',
  247 + pages: [
  248 + {
  249 + name: 'admin.oauth2.settings',
  250 + type: 'link',
  251 + path: '/settings/oauth2-settings',
  252 + icon: 'security'
  253 + }
  254 + ]
  255 + })
  256 + }
  257 +
236 return sections; 258 return sections;
237 } 259 }
238 260
@@ -28,7 +28,7 @@ const routes: Routes = [ @@ -28,7 +28,7 @@ const routes: Routes = [
28 { 28 {
29 path: 'settings', 29 path: 'settings',
30 data: { 30 data: {
31 - auth: [Authority.SYS_ADMIN], 31 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
32 breadcrumb: { 32 breadcrumb: {
33 label: 'admin.system-settings', 33 label: 'admin.system-settings',
34 icon: 'settings' 34 icon: 'settings'
@@ -37,7 +37,7 @@ const routes: Routes = [ @@ -37,7 +37,7 @@ const routes: Routes = [
37 children: [ 37 children: [
38 { 38 {
39 path: '', 39 path: '',
40 - redirectTo: 'general', 40 + redirectTo: Authority.TENANT_ADMIN ? 'oauth2-settings' : 'general',
41 pathMatch: 'full' 41 pathMatch: 'full'
42 }, 42 },
43 { 43 {
@@ -84,7 +84,7 @@ const routes: Routes = [ @@ -84,7 +84,7 @@ const routes: Routes = [
84 component: OAuth2SettingsComponent, 84 component: OAuth2SettingsComponent,
85 canDeactivate: [ConfirmOnExitGuard], 85 canDeactivate: [ConfirmOnExitGuard],
86 data: { 86 data: {
87 - auth: [Authority.SYS_ADMIN], 87 + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
88 title: 'admin.oauth2.settings', 88 title: 'admin.oauth2.settings',
89 breadcrumb: { 89 breadcrumb: {
90 label: 'admin.oauth2.settings', 90 label: 'admin.oauth2.settings',
@@ -52,6 +52,9 @@ @@ -52,6 +52,9 @@
52 <mat-error *ngIf="domain.get('domainName').hasError('pattern')"> 52 <mat-error *ngIf="domain.get('domainName').hasError('pattern')">
53 {{ 'admin.error-verification-url' | translate }} 53 {{ 'admin.error-verification-url' | translate }}
54 </mat-error> 54 </mat-error>
  55 + <mat-error *ngIf="domain.get('domainName').hasError('unique')">
  56 + {{ 'admin.domain-name-unique' | translate }}
  57 + </mat-error>
55 </mat-form-field> 58 </mat-form-field>
56 59
57 <mat-form-field fxFlex class="mat-block"> 60 <mat-form-field fxFlex class="mat-block">
@@ -84,6 +87,9 @@ @@ -84,6 +87,9 @@
84 <mat-error *ngIf="registration.get('registrationId').hasError('required')"> 87 <mat-error *ngIf="registration.get('registrationId').hasError('required')">
85 {{ 'admin.oauth2.registration-id-required' | translate }} 88 {{ 'admin.oauth2.registration-id-required' | translate }}
86 </mat-error> 89 </mat-error>
  90 + <mat-error *ngIf="registration.get('registrationId').hasError('unique')">
  91 + {{ 'admin.oauth2.registration-id-unique' | translate }}
  92 + </mat-error>
87 </mat-form-field> 93 </mat-form-field>
88 94
89 <mat-form-field fxFlex class="mat-block"> 95 <mat-form-field fxFlex class="mat-block">
@@ -140,10 +146,8 @@ @@ -140,10 +146,8 @@
140 <mat-form-field fxFlex class="mat-block"> 146 <mat-form-field fxFlex class="mat-block">
141 <mat-label translate>admin.oauth2.scope</mat-label> 147 <mat-label translate>admin.oauth2.scope</mat-label>
142 <mat-chip-list #scopeList> 148 <mat-chip-list #scopeList>
143 - <mat-chip  
144 - *ngFor="let scope of registration.get('scope').value; let k = index;"  
145 - removable  
146 - (removed)="removeScope(k, registration)"> 149 + <mat-chip *ngFor="let scope of registration.get('scope').value; let k = index;"
  150 + removable (removed)="removeScope(k, registration)">
147 {{scope}} 151 {{scope}}
148 <mat-icon matChipRemove>cancel</mat-icon> 152 <mat-icon matChipRemove>cancel</mat-icon>
149 </mat-chip> 153 </mat-chip>
@@ -152,6 +156,9 @@ @@ -152,6 +156,9 @@
152 matChipInputAddOnBlur 156 matChipInputAddOnBlur
153 (matChipInputTokenEnd)="addScope($event, registration)"> 157 (matChipInputTokenEnd)="addScope($event, registration)">
154 </mat-chip-list> 158 </mat-chip-list>
  159 + <mat-error *ngIf="registration.get('scope').hasError('required')">
  160 + {{ 'admin.oauth2.jwk-set-uri-required' | translate }}
  161 + </mat-error>
155 </mat-form-field> 162 </mat-form-field>
156 163
157 <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> 164 <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
@@ -112,14 +112,44 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -112,14 +112,44 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
112 112
113 private buildOAuth2SettingsForm(): void { 113 private buildOAuth2SettingsForm(): void {
114 this.oauth2SettingsForm = this.fb.group({ 114 this.oauth2SettingsForm = this.fb.group({
115 - clientsDomainsParams: this.fb.array([]) 115 + clientsDomainsParams: this.fb.array([], Validators.required)
116 }); 116 });
117 } 117 }
118 118
119 private initOAuth2Settings(oauth2Settings: OAuth2Settings): void { 119 private initOAuth2Settings(oauth2Settings: OAuth2Settings): void {
120 - oauth2Settings.clientsDomainsParams.forEach((domaindomain) => {  
121 - this.clientsDomainsParams.push(this.buildSettingsDomain(domaindomain));  
122 - }); 120 + if(oauth2Settings.clientsDomainsParams) {
  121 + oauth2Settings.clientsDomainsParams.forEach((domaindomain) => {
  122 + this.clientsDomainsParams.push(this.buildSettingsDomain(domaindomain));
  123 + });
  124 + }
  125 + }
  126 +
  127 + private uniqueDomainValidator(control: AbstractControl): { [key: string]: boolean } | null {
  128 + if (control.value !== null && control?.root) {
  129 + const listDomainName = [];
  130 + control.root.value.clientsDomainsParams.forEach((domain) => {
  131 + listDomainName.push(domain.domainName);
  132 + })
  133 + if (listDomainName.indexOf(control.value) > -1) {
  134 + return {unique: true};
  135 + }
  136 + }
  137 + return null;
  138 + }
  139 +
  140 + private uniqueRegistrationIdValidator(control: AbstractControl): { [key: string]: boolean } | null {
  141 + if (control.value !== null && control?.root) {
  142 + const listRegistration = [];
  143 + control.root.value.clientsDomainsParams.forEach((domain) => {
  144 + domain.clientRegistrations.forEach((client) => {
  145 + listRegistration.push(client.registrationId);
  146 + })
  147 + })
  148 + if (listRegistration.indexOf(control.value) > -1) {
  149 + return {unique: true};
  150 + }
  151 + }
  152 + return null;
123 } 153 }
124 154
125 private buildSettingsDomain(domainParams?: DomainParams): FormGroup { 155 private buildSettingsDomain(domainParams?: DomainParams): FormGroup {
@@ -130,9 +160,9 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -130,9 +160,9 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
130 } 160 }
131 url += '/login/oauth2/code/'; 161 url += '/login/oauth2/code/';
132 const formDomain = this.fb.group({ 162 const formDomain = this.fb.group({
133 - domainName: ['', [Validators.required, Validators.pattern('((?![:/]).)*$')]], 163 + domainName: [null, [Validators.required, Validators.pattern('((?![:/]).)*$'), this.uniqueDomainValidator]],
134 redirectUriTemplate: [url, [Validators.required, Validators.pattern(this.URL_REGEXP)]], 164 redirectUriTemplate: [url, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
135 - clientRegistrations: this.fb.array([]) 165 + clientRegistrations: this.fb.array([], Validators.required)
136 }); 166 });
137 167
138 this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => { 168 this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => {
@@ -156,7 +186,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -156,7 +186,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
156 186
157 private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup { 187 private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup {
158 const clientRegistration = this.fb.group({ 188 const clientRegistration = this.fb.group({
159 - registrationId: [null, [Validators.required]], 189 + registrationId: [null, [Validators.required, this.uniqueRegistrationIdValidator]],
160 clientName: [null, [Validators.required]], 190 clientName: [null, [Validators.required]],
161 loginButtonLabel: [null, [Validators.required]], 191 loginButtonLabel: [null, [Validators.required]],
162 loginButtonIcon: [null], 192 loginButtonIcon: [null],
@@ -202,7 +232,6 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -202,7 +232,6 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
202 } 232 }
203 233
204 save(): void { 234 save(): void {
205 - console.log(this.oauth2SettingsForm.value);  
206 this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe( 235 this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe(
207 (oauth2Settings) => { 236 (oauth2Settings) => {
208 this.oauth2Settings = oauth2Settings; 237 this.oauth2Settings = oauth2Settings;
@@ -246,7 +275,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -246,7 +275,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
246 275
247 const domainName = this.clientsDomainsParams.at(index).get('domainName').value; 276 const domainName = this.clientsDomainsParams.at(index).get('domainName').value;
248 this.dialogService.confirm( 277 this.dialogService.confirm(
249 - this.translate.instant('admin.oauth2.delete-domain-title', {domainName}), 278 + this.translate.instant('admin.oauth2.delete-domain-title', {domainName: domainName || ''}),
250 this.translate.instant('admin.oauth2.delete-domain-text'), null, 279 this.translate.instant('admin.oauth2.delete-domain-text'), null,
251 this.translate.instant('action.delete') 280 this.translate.instant('action.delete')
252 ).subscribe((data) => { 281 ).subscribe((data) => {
@@ -272,7 +301,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha @@ -272,7 +301,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
272 301
273 const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value; 302 const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value;
274 this.dialogService.confirm( 303 this.dialogService.confirm(
275 - this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId}), 304 + this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId || ''}),
276 this.translate.instant('admin.oauth2.delete-registration-text'), null, 305 this.translate.instant('admin.oauth2.delete-registration-text'), null,
277 this.translate.instant('action.delete') 306 this.translate.instant('action.delete')
278 ).subscribe((data) => { 307 ).subscribe((data) => {
@@ -58,7 +58,7 @@ export class TenantComponent extends ContactBasedComponent<Tenant> { @@ -58,7 +58,7 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
58 { 58 {
59 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], 59 description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
60 allowOAuth2Configuration: [isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ? 60 allowOAuth2Configuration: [isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
61 - entity.additionalInfo.allowOAuth2Configuration : true] 61 + entity.additionalInfo.allowOAuth2Configuration : false]
62 } 62 }
63 ) 63 )
64 } 64 }
@@ -72,7 +72,7 @@ export class TenantComponent extends ContactBasedComponent<Tenant> { @@ -72,7 +72,7 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
72 this.entityForm.patchValue({additionalInfo: { 72 this.entityForm.patchValue({additionalInfo: {
73 description: entity.additionalInfo ? entity.additionalInfo.description : '', 73 description: entity.additionalInfo ? entity.additionalInfo.description : '',
74 allowOAuth2Configuration: isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ? 74 allowOAuth2Configuration: isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
75 - entity.additionalInfo.allowOAuth2Configuration : true 75 + entity.additionalInfo.allowOAuth2Configuration : false
76 }}); 76 }});
77 } 77 }
78 78
@@ -121,6 +121,7 @@ @@ -121,6 +121,7 @@
121 "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", 121 "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
122 "user-lockout-notification-email": "In case user account lockout, send notification to email", 122 "user-lockout-notification-email": "In case user account lockout, send notification to email",
123 "domain-name": "Domain name", 123 "domain-name": "Domain name",
  124 + "domain-name-unique": "Domain name need to unique for the system.",
124 "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", 125 "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
125 "add-domain": "Add domain", 126 "add-domain": "Add domain",
126 "new-domain": "New domain", 127 "new-domain": "New domain",
@@ -129,6 +130,7 @@ @@ -129,6 +130,7 @@
129 "settings": "OAuth2 settings", 130 "settings": "OAuth2 settings",
130 "registration-id": "Registration ID", 131 "registration-id": "Registration ID",
131 "registration-id-required": "Registration ID is required.", 132 "registration-id-required": "Registration ID is required.",
  133 + "registration-id-unique": "Registration ID need to unique for the system.",
132 "client-name": "Client name", 134 "client-name": "Client name",
133 "client-name-required": "Client name is required.", 135 "client-name-required": "Client name is required.",
134 "client-id": "Client ID", 136 "client-id": "Client ID",