Commit 483a3b847b8266d71e39477094add11836076219
1 parent
7af3fe00
Add tenant menu settingOAuth2
Add validation unique parameters to settings OAuth2
Showing
9 changed files
with
94 additions
and
21 deletions
@@ -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", |