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 | 22 | userTokenAccessEnabled: boolean; |
23 | 23 | allowedDashboardIds: string[]; |
24 | 24 | forceFullscreen: boolean; |
25 | + allowOAuth2Configuration: boolean; | |
25 | 26 | } |
26 | 27 | |
27 | 28 | export interface AuthState { |
... | ... | @@ -33,4 +34,5 @@ export interface AuthState { |
33 | 34 | allowedDashboardIds: string[]; |
34 | 35 | forceFullscreen: boolean; |
35 | 36 | lastPublicDashboardId: string; |
37 | + allowOAuth2Configuration: boolean; | |
36 | 38 | } | ... | ... |
... | ... | @@ -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 | 436 | private loadSystemParams(authPayload: AuthPayload): Observable<any> { |
429 | 437 | const sources: Array<Observable<any>> = [this.loadIsUserTokenAccessEnabled(authPayload.authUser), |
430 | 438 | this.fetchAllowedDashboardIds(authPayload), |
439 | + this.loadIsOAuth2ConfigurationAllow(authPayload.authUser), | |
431 | 440 | this.timeService.loadMaxDatapointsLimit()]; |
432 | 441 | return forkJoin(sources) |
433 | 442 | .pipe(map((data) => { |
434 | 443 | const userTokenAccessEnabled: boolean = data[0]; |
435 | 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 | 18 | import { AuthService } from '../auth/auth.service'; |
19 | 19 | import { select, Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '../core.state'; |
21 | -import { selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; | |
21 | +import { getCurrentAuthState, selectAuthUser, selectIsAuthenticated } from '../auth/auth.selectors'; | |
22 | 22 | import { take } from 'rxjs/operators'; |
23 | 23 | import { HomeSection, MenuSection } from '@core/services/menu.models'; |
24 | 24 | import { BehaviorSubject, Observable, Subject } from 'rxjs'; |
25 | 25 | import { Authority } from '@shared/models/authority.enum'; |
26 | 26 | import { AuthUser } from '@shared/models/user.model'; |
27 | +import { AuthState } from '@core/auth/auth.models'; | |
27 | 28 | |
28 | 29 | @Injectable({ |
29 | 30 | providedIn: 'root' |
... | ... | @@ -43,6 +44,8 @@ export class MenuService { |
43 | 44 | ); |
44 | 45 | } |
45 | 46 | |
47 | + authState: AuthState = getCurrentAuthState(this.store); | |
48 | + | |
46 | 49 | private buildMenu() { |
47 | 50 | this.store.pipe(select(selectAuthUser), take(1)).subscribe( |
48 | 51 | (authUser: AuthUser) => { |
... | ... | @@ -233,6 +236,25 @@ export class MenuService { |
233 | 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 | 258 | return sections; |
237 | 259 | } |
238 | 260 | ... | ... |
... | ... | @@ -28,7 +28,7 @@ const routes: Routes = [ |
28 | 28 | { |
29 | 29 | path: 'settings', |
30 | 30 | data: { |
31 | - auth: [Authority.SYS_ADMIN], | |
31 | + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], | |
32 | 32 | breadcrumb: { |
33 | 33 | label: 'admin.system-settings', |
34 | 34 | icon: 'settings' |
... | ... | @@ -37,7 +37,7 @@ const routes: Routes = [ |
37 | 37 | children: [ |
38 | 38 | { |
39 | 39 | path: '', |
40 | - redirectTo: 'general', | |
40 | + redirectTo: Authority.TENANT_ADMIN ? 'oauth2-settings' : 'general', | |
41 | 41 | pathMatch: 'full' |
42 | 42 | }, |
43 | 43 | { |
... | ... | @@ -84,7 +84,7 @@ const routes: Routes = [ |
84 | 84 | component: OAuth2SettingsComponent, |
85 | 85 | canDeactivate: [ConfirmOnExitGuard], |
86 | 86 | data: { |
87 | - auth: [Authority.SYS_ADMIN], | |
87 | + auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], | |
88 | 88 | title: 'admin.oauth2.settings', |
89 | 89 | breadcrumb: { |
90 | 90 | label: 'admin.oauth2.settings', | ... | ... |
... | ... | @@ -52,6 +52,9 @@ |
52 | 52 | <mat-error *ngIf="domain.get('domainName').hasError('pattern')"> |
53 | 53 | {{ 'admin.error-verification-url' | translate }} |
54 | 54 | </mat-error> |
55 | + <mat-error *ngIf="domain.get('domainName').hasError('unique')"> | |
56 | + {{ 'admin.domain-name-unique' | translate }} | |
57 | + </mat-error> | |
55 | 58 | </mat-form-field> |
56 | 59 | |
57 | 60 | <mat-form-field fxFlex class="mat-block"> |
... | ... | @@ -84,6 +87,9 @@ |
84 | 87 | <mat-error *ngIf="registration.get('registrationId').hasError('required')"> |
85 | 88 | {{ 'admin.oauth2.registration-id-required' | translate }} |
86 | 89 | </mat-error> |
90 | + <mat-error *ngIf="registration.get('registrationId').hasError('unique')"> | |
91 | + {{ 'admin.oauth2.registration-id-unique' | translate }} | |
92 | + </mat-error> | |
87 | 93 | </mat-form-field> |
88 | 94 | |
89 | 95 | <mat-form-field fxFlex class="mat-block"> |
... | ... | @@ -140,10 +146,8 @@ |
140 | 146 | <mat-form-field fxFlex class="mat-block"> |
141 | 147 | <mat-label translate>admin.oauth2.scope</mat-label> |
142 | 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 | 151 | {{scope}} |
148 | 152 | <mat-icon matChipRemove>cancel</mat-icon> |
149 | 153 | </mat-chip> |
... | ... | @@ -152,6 +156,9 @@ |
152 | 156 | matChipInputAddOnBlur |
153 | 157 | (matChipInputTokenEnd)="addScope($event, registration)"> |
154 | 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 | 162 | </mat-form-field> |
156 | 163 | |
157 | 164 | <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> | ... | ... |
... | ... | @@ -112,14 +112,44 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
112 | 112 | |
113 | 113 | private buildOAuth2SettingsForm(): void { |
114 | 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 | 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 | 155 | private buildSettingsDomain(domainParams?: DomainParams): FormGroup { |
... | ... | @@ -130,9 +160,9 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
130 | 160 | } |
131 | 161 | url += '/login/oauth2/code/'; |
132 | 162 | const formDomain = this.fb.group({ |
133 | - domainName: ['', [Validators.required, Validators.pattern('((?![:/]).)*$')]], | |
163 | + domainName: [null, [Validators.required, Validators.pattern('((?![:/]).)*$'), this.uniqueDomainValidator]], | |
134 | 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 | 168 | this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => { |
... | ... | @@ -156,7 +186,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
156 | 186 | |
157 | 187 | private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup { |
158 | 188 | const clientRegistration = this.fb.group({ |
159 | - registrationId: [null, [Validators.required]], | |
189 | + registrationId: [null, [Validators.required, this.uniqueRegistrationIdValidator]], | |
160 | 190 | clientName: [null, [Validators.required]], |
161 | 191 | loginButtonLabel: [null, [Validators.required]], |
162 | 192 | loginButtonIcon: [null], |
... | ... | @@ -202,7 +232,6 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
202 | 232 | } |
203 | 233 | |
204 | 234 | save(): void { |
205 | - console.log(this.oauth2SettingsForm.value); | |
206 | 235 | this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe( |
207 | 236 | (oauth2Settings) => { |
208 | 237 | this.oauth2Settings = oauth2Settings; |
... | ... | @@ -246,7 +275,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
246 | 275 | |
247 | 276 | const domainName = this.clientsDomainsParams.at(index).get('domainName').value; |
248 | 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 | 279 | this.translate.instant('admin.oauth2.delete-domain-text'), null, |
251 | 280 | this.translate.instant('action.delete') |
252 | 281 | ).subscribe((data) => { |
... | ... | @@ -272,7 +301,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
272 | 301 | |
273 | 302 | const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value; |
274 | 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 | 305 | this.translate.instant('admin.oauth2.delete-registration-text'), null, |
277 | 306 | this.translate.instant('action.delete') |
278 | 307 | ).subscribe((data) => { | ... | ... |
... | ... | @@ -58,7 +58,7 @@ export class TenantComponent extends ContactBasedComponent<Tenant> { |
58 | 58 | { |
59 | 59 | description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''], |
60 | 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 | 72 | this.entityForm.patchValue({additionalInfo: { |
73 | 73 | description: entity.additionalInfo ? entity.additionalInfo.description : '', |
74 | 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 | 121 | "minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative", |
122 | 122 | "user-lockout-notification-email": "In case user account lockout, send notification to email", |
123 | 123 | "domain-name": "Domain name", |
124 | + "domain-name-unique": "Domain name need to unique for the system.", | |
124 | 125 | "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", |
125 | 126 | "add-domain": "Add domain", |
126 | 127 | "new-domain": "New domain", |
... | ... | @@ -129,6 +130,7 @@ |
129 | 130 | "settings": "OAuth2 settings", |
130 | 131 | "registration-id": "Registration ID", |
131 | 132 | "registration-id-required": "Registration ID is required.", |
133 | + "registration-id-unique": "Registration ID need to unique for the system.", | |
132 | 134 | "client-name": "Client name", |
133 | 135 | "client-name-required": "Client name is required.", |
134 | 136 | "client-id": "Client ID", | ... | ... |