Commit 7af3fe0050a674b46888cd72fc28c88622e398c7

Authored by Vladyslav_Prykhodko
1 parent 981ff1fd

Add system admin OAuth2 settings

... ... @@ -18,7 +18,13 @@ import { Injectable } from '@angular/core';
18 18 import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
19 19 import { Observable } from 'rxjs';
20 20 import { HttpClient } from '@angular/common/http';
21   -import { AdminSettings, MailServerSettings, SecuritySettings, UpdateMessage } from '@shared/models/settings.models';
  21 +import {
  22 + AdminSettings,
  23 + MailServerSettings,
  24 + OAuth2Settings,
  25 + SecuritySettings,
  26 + UpdateMessage
  27 +} from '@shared/models/settings.models';
22 28
23 29 @Injectable({
24 30 providedIn: 'root'
... ... @@ -53,6 +59,16 @@ export class AdminService {
53 59 defaultHttpOptionsFromConfig(config));
54 60 }
55 61
  62 + public getOAuth2Settings(config?: RequestConfig): Observable<OAuth2Settings> {
  63 + return this.http.get<OAuth2Settings>(`/api/oauth2/config`, defaultHttpOptionsFromConfig(config));
  64 + }
  65 +
  66 + public saveOAuth2Settings(OAuth2Setting: OAuth2Settings,
  67 + config?: RequestConfig): Observable<OAuth2Settings> {
  68 + return this.http.post<OAuth2Settings>('/api/oauth2/config', OAuth2Setting,
  69 + defaultHttpOptionsFromConfig(config));
  70 + }
  71 +
56 72 public checkUpdates(config?: RequestConfig): Observable<UpdateMessage> {
57 73 return this.http.get<UpdateMessage>(`/api/admin/updates`, defaultHttpOptionsFromConfig(config));
58 74 }
... ...
... ... @@ -95,7 +95,7 @@ export class MenuService {
95 95 name: 'admin.system-settings',
96 96 type: 'toggle',
97 97 path: '/settings',
98   - height: '120px',
  98 + height: '160px',
99 99 icon: 'settings',
100 100 pages: [
101 101 {
... ... @@ -115,6 +115,12 @@ export class MenuService {
115 115 type: 'link',
116 116 path: '/settings/security-settings',
117 117 icon: 'security'
  118 + },
  119 + {
  120 + name: 'admin.oauth2.settings',
  121 + type: 'link',
  122 + path: '/settings/oauth2-settings',
  123 + icon: 'security'
118 124 }
119 125 ]
120 126 }
... ...
... ... @@ -22,6 +22,7 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
22 22 import { Authority } from '@shared/models/authority.enum';
23 23 import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
24 24 import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
  25 +import { OAuth2SettingsComponent } from '@home/pages/admin/oauth2-settings.component';
25 26
26 27 const routes: Routes = [
27 28 {
... ... @@ -77,6 +78,19 @@ const routes: Routes = [
77 78 icon: 'security'
78 79 }
79 80 }
  81 + },
  82 + {
  83 + path: 'oauth2-settings',
  84 + component: OAuth2SettingsComponent,
  85 + canDeactivate: [ConfirmOnExitGuard],
  86 + data: {
  87 + auth: [Authority.SYS_ADMIN],
  88 + title: 'admin.oauth2.settings',
  89 + breadcrumb: {
  90 + label: 'admin.oauth2.settings',
  91 + icon: 'security'
  92 + }
  93 + }
80 94 }
81 95 ]
82 96 }
... ...
... ... @@ -23,13 +23,15 @@ import { MailServerComponent } from '@modules/home/pages/admin/mail-server.compo
23 23 import { GeneralSettingsComponent } from '@modules/home/pages/admin/general-settings.component';
24 24 import { SecuritySettingsComponent } from '@modules/home/pages/admin/security-settings.component';
25 25 import { HomeComponentsModule } from '@modules/home/components/home-components.module';
  26 +import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
26 27
27 28 @NgModule({
28 29 declarations:
29 30 [
30 31 GeneralSettingsComponent,
31 32 MailServerComponent,
32   - SecuritySettingsComponent
  33 + SecuritySettingsComponent,
  34 + OAuth2SettingsComponent
33 35 ],
34 36 imports: [
35 37 CommonModule,
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-card class="settings-card">
  20 + <mat-card-title>
  21 + <div fxLayout="row">
  22 + <span class="mat-headline" translate>admin.oauth2.settings</span>
  23 + <span fxFlex></span>
  24 + <div tb-help="oauth2Settings"></div>
  25 + </div>
  26 + </mat-card-title>
  27 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  28 + </mat-progress-bar>
  29 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  30 + <mat-card-content style="padding-top: 16px;">
  31 + <form [formGroup]="oauth2SettingsForm" (ngSubmit)="save()">
  32 + <fieldset [disabled]="isLoading$ | async">
  33 + <ng-container formArrayName="clientsDomainsParams">
  34 + <div class="container">
  35 + <mat-accordion multi>
  36 + <ng-container *ngFor="let domain of clientsDomainsParams.controls; let i = index; let last = last;">
  37 + <mat-expansion-panel [formGroupName]="i" [expanded]="last">
  38 + <mat-expansion-panel-header>
  39 + <mat-panel-title fxLayoutAlign="start center">
  40 + {{ domain.get('domainName').value ? domain.get('domainName').value : ("admin.new-domain" | translate) }}
  41 + </mat-panel-title>
  42 + <mat-panel-description fxLayoutAlign="end center">
  43 + <button mat-icon-button (click)="deleteDomain($event, i)">
  44 + <mat-icon>delete</mat-icon>
  45 + </button>
  46 + </mat-panel-description>
  47 + </mat-expansion-panel-header>
  48 +
  49 + <mat-form-field class="mat-block">
  50 + <mat-label translate>admin.domain-name</mat-label>
  51 + <input matInput formControlName="domainName" required>
  52 + <mat-error *ngIf="domain.get('domainName').hasError('pattern')">
  53 + {{ 'admin.error-verification-url' | translate }}
  54 + </mat-error>
  55 + </mat-form-field>
  56 +
  57 + <mat-form-field fxFlex class="mat-block">
  58 + <mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
  59 + <input matInput formControlName="redirectUriTemplate" required>
  60 + <mat-error
  61 + *ngIf="domain.get('redirectUriTemplate').hasError('required')">
  62 + {{ 'admin.oauth2.redirect-uri-template-required' | translate }}
  63 + </mat-error>
  64 + <mat-error
  65 + *ngIf="domain.get('redirectUriTemplate').hasError('pattern')">
  66 + {{ 'admin.oauth2.uri-pattern-error' | translate }}
  67 + </mat-error>
  68 + </mat-form-field>
  69 +
  70 + <ng-container formArrayName="clientRegistrations">
  71 + <div class="container">
  72 + <mat-card *ngFor="let registration of clientDomainRegistrations(domain).controls; let j = index;"
  73 + class="registration-card">
  74 + <section [formGroupName]="j">
  75 + <div fxLayoutAlign="end center">
  76 + <button mat-icon-button (click)="deleteRegistration($event, domain, j)">
  77 + <mat-icon>delete</mat-icon>
  78 + </button>
  79 + </div>
  80 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  81 + <mat-form-field fxFlex class="mat-block">
  82 + <mat-label translate>admin.oauth2.registration-id</mat-label>
  83 + <input matInput formControlName="registrationId" required>
  84 + <mat-error *ngIf="registration.get('registrationId').hasError('required')">
  85 + {{ 'admin.oauth2.registration-id-required' | translate }}
  86 + </mat-error>
  87 + </mat-form-field>
  88 +
  89 + <mat-form-field fxFlex class="mat-block">
  90 + <mat-label translate>admin.oauth2.client-name</mat-label>
  91 + <input matInput formControlName="clientName" required>
  92 + <mat-error *ngIf="registration.get('clientName').hasError('required')">
  93 + {{ 'admin.oauth2.client-name-required' | translate }}
  94 + </mat-error>
  95 + </mat-form-field>
  96 + </div>
  97 +
  98 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  99 + <mat-form-field fxFlex class="mat-block">
  100 + <mat-label translate>admin.oauth2.client-id</mat-label>
  101 + <input matInput formControlName="clientId" required>
  102 + <mat-error *ngIf="registration.get('clientId').hasError('required')">
  103 + {{ 'admin.oauth2.client-id-required' | translate }}
  104 + </mat-error>
  105 + </mat-form-field>
  106 +
  107 + <mat-form-field fxFlex class="mat-block">
  108 + <mat-label translate>admin.oauth2.client-secret</mat-label>
  109 + <input matInput formControlName="clientSecret" required>
  110 + <mat-error *ngIf="registration.get('clientSecret').hasError('required')">
  111 + {{ 'admin.oauth2.client-secret-required' | translate }}
  112 + </mat-error>
  113 + </mat-form-field>
  114 + </div>
  115 +
  116 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  117 + <mat-form-field fxFlex class="mat-block">
  118 + <mat-label translate>admin.oauth2.access-token-uri</mat-label>
  119 + <input matInput formControlName="accessTokenUri" required>
  120 + <mat-error *ngIf="registration.get('accessTokenUri').hasError('required')">
  121 + {{ 'admin.oauth2.access-token-uri-required' | translate }}
  122 + </mat-error>
  123 + <mat-error *ngIf="registration.get('accessTokenUri').hasError('pattern')">
  124 + {{ 'admin.oauth2.uri-pattern-error' | translate }}
  125 + </mat-error>
  126 + </mat-form-field>
  127 +
  128 + <mat-form-field fxFlex class="mat-block">
  129 + <mat-label translate>admin.oauth2.authorization-uri</mat-label>
  130 + <input matInput formControlName="authorizationUri" required>
  131 + <mat-error *ngIf="registration.get('authorizationUri').hasError('required')">
  132 + {{ 'admin.oauth2.authorization-uri-required' | translate }}
  133 + </mat-error>
  134 + <mat-error *ngIf="registration.get('authorizationUri').hasError('pattern')">
  135 + {{ 'admin.oauth2.uri-pattern-error' | translate }}
  136 + </mat-error>
  137 + </mat-form-field>
  138 + </div>
  139 +
  140 + <mat-form-field fxFlex class="mat-block">
  141 + <mat-label translate>admin.oauth2.scope</mat-label>
  142 + <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)">
  147 + {{scope}}
  148 + <mat-icon matChipRemove>cancel</mat-icon>
  149 + </mat-chip>
  150 + <input [matChipInputFor]="scopeList"
  151 + [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
  152 + matChipInputAddOnBlur
  153 + (matChipInputTokenEnd)="addScope($event, registration)">
  154 + </mat-chip-list>
  155 + </mat-form-field>
  156 +
  157 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  158 + <mat-form-field fxFlex class="mat-block">
  159 + <mat-label translate>admin.oauth2.jwk-set-uri</mat-label>
  160 + <input matInput formControlName="jwkSetUri" required>
  161 + <mat-error *ngIf="registration.get('jwkSetUri').hasError('required')">
  162 + {{ 'admin.oauth2.jwk-set-uri-required' | translate }}
  163 + </mat-error>
  164 + <mat-error *ngIf="registration.get('jwkSetUri').hasError('pattern')">
  165 + {{ 'admin.oauth2.uri-pattern-error' | translate }}
  166 + </mat-error>
  167 + </mat-form-field>
  168 +
  169 + <mat-form-field fxFlex class="mat-block">
  170 + <mat-label translate>admin.oauth2.user-info-uri</mat-label>
  171 + <input matInput formControlName="userInfoUri" required>
  172 + <mat-error *ngIf="registration.get('userInfoUri').hasError('required')">
  173 + {{ 'admin.oauth2.user-info-uri-required' | translate }}
  174 + </mat-error>
  175 + <mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')">
  176 + {{ 'admin.oauth2.uri-pattern-error' | translate }}
  177 + </mat-error>
  178 + </mat-form-field>
  179 + </div>
  180 +
  181 + <mat-form-field fxFlex class="mat-block">
  182 + <mat-label translate>admin.oauth2.client-authentication-method</mat-label>
  183 + <mat-select formControlName="clientAuthenticationMethod">
  184 + <mat-option *ngFor="let clientAuthenticationMethod of clientAuthenticationMethods"
  185 + [value]="clientAuthenticationMethod">
  186 + {{ clientAuthenticationMethod | uppercase }}
  187 + </mat-option>
  188 + </mat-select>
  189 + </mat-form-field>
  190 +
  191 + <mat-form-field class="mat-block">
  192 + <mat-label translate>admin.oauth2.user-name-attribute-name</mat-label>
  193 + <input matInput formControlName="userNameAttributeName" required>
  194 + <mat-error *ngIf="registration.get('userNameAttributeName').hasError('required')">
  195 + {{ 'admin.oauth2.user-name-attribute-name-required' | translate }}
  196 + </mat-error>
  197 + </mat-form-field>
  198 +
  199 + <section formGroupName="mapperConfig">
  200 + <div fxLayout="column" fxLayoutGap="8px">
  201 + <mat-checkbox formControlName="allowUserCreation">
  202 + {{ 'admin.oauth2.allow-user-creation' | translate }}
  203 + </mat-checkbox>
  204 + <mat-checkbox formControlName="activateUser">
  205 + {{ 'admin.oauth2.activate-user' | translate }}
  206 + </mat-checkbox>
  207 + </div>
  208 +
  209 + <mat-form-field fxFlex class="mat-block">
  210 + <mat-label translate>admin.oauth2.type</mat-label>
  211 + <mat-select formControlName="type">
  212 + <mat-option *ngFor="let converterTypeExternalUser of converterTypesExternalUser"
  213 + [value]="converterTypeExternalUser">
  214 + {{ converterTypeExternalUser }}
  215 + </mat-option>
  216 + </mat-select>
  217 + </mat-form-field>
  218 +
  219 + <section formGroupName="basic"
  220 + *ngIf="registration.get('mapperConfig.type').value === 'BASIC'">
  221 + <mat-form-field class="mat-block">
  222 + <mat-label translate>admin.oauth2.email-attribute-key</mat-label>
  223 + <input matInput formControlName="emailAttributeKey" required>
  224 + <mat-error
  225 + *ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')">
  226 + {{ 'admin.oauth2.email-attribute-key-required' | translate }}
  227 + </mat-error>
  228 + </mat-form-field>
  229 +
  230 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  231 + <mat-form-field fxFlex class="mat-block">
  232 + <mat-label translate>admin.oauth2.first-name-attribute-key</mat-label>
  233 + <input matInput formControlName="firstNameAttributeKey">
  234 + </mat-form-field>
  235 +
  236 + <mat-form-field fxFlex class="mat-block">
  237 + <mat-label translate>admin.oauth2.last-name-attribute-key</mat-label>
  238 + <input matInput formControlName="lastNameAttributeKey">
  239 + </mat-form-field>
  240 + </div>
  241 +
  242 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  243 + <mat-form-field fxFlex class="mat-block">
  244 + <mat-label translate>admin.oauth2.tenant-name-strategy</mat-label>
  245 + <mat-select formControlName="tenantNameStrategy">
  246 + <mat-option *ngFor="let tenantNameStrategy of tenantNameStrategies"
  247 + [value]="tenantNameStrategy">
  248 + {{ tenantNameStrategy }}
  249 + </mat-option>
  250 + </mat-select>
  251 + </mat-form-field>
  252 +
  253 + <mat-form-field fxFlex class="mat-block">
  254 + <mat-label translate>admin.oauth2.tenant-name-pattern</mat-label>
  255 + <input matInput
  256 + formControlName="tenantNamePattern"
  257 + [required]="registration.get('mapperConfig.basic.tenantNameStrategy').value === 'CUSTOM'">
  258 + <mat-error
  259 + *ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')">
  260 + {{ 'admin.oauth2.tenant-name-pattern-required' | translate }}
  261 + </mat-error>
  262 + </mat-form-field>
  263 + </div>
  264 +
  265 + <mat-form-field fxFlex class="mat-block">
  266 + <mat-label translate>admin.oauth2.customer-name-pattern</mat-label>
  267 + <input matInput formControlName="customerNamePattern">
  268 + </mat-form-field>
  269 +
  270 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  271 + <mat-form-field fxFlex class="mat-block">
  272 + <mat-label translate>admin.oauth2.default-dashboard-name</mat-label>
  273 + <input matInput formControlName="defaultDashboardName">
  274 + </mat-form-field>
  275 +
  276 + <mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row">
  277 + {{ 'admin.oauth2.always-fullscreen' | translate}}
  278 + </mat-checkbox>
  279 + </div>
  280 +
  281 + </section>
  282 +
  283 + <section formGroupName="custom"
  284 + *ngIf="registration.get('mapperConfig.type').value === 'CUSTOM'">
  285 + <mat-form-field class="mat-block">
  286 + <mat-label translate>admin.oauth2.url</mat-label>
  287 + <input matInput formControlName="url" required>
  288 + <mat-error
  289 + *ngIf="registration.get('mapperConfig.custom.url').hasError('required')">
  290 + {{ 'admin.oauth2.url-required' | translate }}
  291 + </mat-error>
  292 + <mat-error
  293 + *ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')">
  294 + {{ 'admin.oauth2.url-pattern' | translate }}
  295 + </mat-error>
  296 + </mat-form-field>
  297 +
  298 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  299 + <mat-form-field fxFlex class="mat-block">
  300 + <mat-label translate>common.username</mat-label>
  301 + <input matInput formControlName="username">
  302 + </mat-form-field>
  303 +
  304 + <mat-form-field fxFlex class="mat-block">
  305 + <mat-label translate>common.password</mat-label>
  306 + <input matInput formControlName="password">
  307 + </mat-form-field>
  308 + </div>
  309 + </section>
  310 + </section>
  311 +
  312 + <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
  313 + <mat-form-field fxFlex class="mat-block">
  314 + <mat-label translate>admin.oauth2.login-button-label</mat-label>
  315 + <input matInput formControlName="loginButtonLabel" required>
  316 + <mat-error
  317 + *ngIf="registration.get('loginButtonLabel').hasError('required')">
  318 + {{ 'admin.oauth2.login-button-label-required' | translate }}
  319 + </mat-error>
  320 + </mat-form-field>
  321 +
  322 + <mat-form-field fxFlex class="mat-block">
  323 + <mat-label translate>admin.oauth2.login-button-icon</mat-label>
  324 + <input matInput formControlName="loginButtonIcon">
  325 + </mat-form-field>
  326 + </div>
  327 + </section>
  328 + </mat-card>
  329 + </div>
  330 + </ng-container>
  331 +
  332 + <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
  333 + <button mat-button mat-raised-button color="primary"
  334 + [disabled]="(isLoading$ | async)"
  335 + (click)="addRegistration(domain)"
  336 + type="button">
  337 + {{'admin.add-registration' | translate}}
  338 + </button>
  339 + </div>
  340 +
  341 + </mat-expansion-panel>
  342 + </ng-container>
  343 + </mat-accordion>
  344 + </div>
  345 + </ng-container>
  346 +
  347 + <div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px">
  348 + <button mat-button mat-raised-button color="primary"
  349 + [disabled]="(isLoading$ | async)"
  350 + (click)="addDomain()"
  351 + type="button">
  352 + {{'admin.add-domain' | translate}}
  353 + </button>
  354 + <button mat-button mat-raised-button color="primary"
  355 + [disabled]="(isLoading$ | async) || oauth2SettingsForm.invalid || !oauth2SettingsForm.dirty"
  356 + type="submit">
  357 + {{'action.save' | translate}}
  358 + </button>
  359 + </div>
  360 + </fieldset>
  361 + </form>
  362 + </mat-card-content>
  363 + </mat-card>
  364 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +:host{
  17 + .checkbox-row {
  18 + margin-top: 16px;
  19 + }
  20 +
  21 + .registration-card{
  22 + margin-bottom: 8px;
  23 + }
  24 +
  25 + .container{
  26 + margin-bottom: 16px;
  27 + }
  28 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
  18 +import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
  19 +import { ClientRegistration, DomainParams, OAuth2Settings } from '@shared/models/settings.models';
  20 +import { Store } from '@ngrx/store';
  21 +import { AppState } from '@core/core.state';
  22 +import { AdminService } from '@core/http/admin.service';
  23 +import { PageComponent } from '@shared/components/page.component';
  24 +import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
  25 +import { COMMA, ENTER } from '@angular/cdk/keycodes';
  26 +import { MatChipInputEvent } from '@angular/material/chips';
  27 +import { WINDOW } from '@core/services/window.service';
  28 +import { Subscription } from 'rxjs';
  29 +import { DialogService } from '@core/services/dialog.service';
  30 +import { TranslateService } from '@ngx-translate/core';
  31 +
  32 +@Component({
  33 + selector: 'tb-oauth2-settings',
  34 + templateUrl: './oauth2-settings.component.html',
  35 + styleUrls: ['./oauth2-settings.component.scss', './settings-card.scss']
  36 +})
  37 +export class OAuth2SettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
  38 +
  39 + private URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
  40 + private subscriptions: Subscription[] = [];
  41 +
  42 + readonly separatorKeysCodes: number[] = [ENTER, COMMA];
  43 +
  44 + oauth2SettingsForm: FormGroup;
  45 + oauth2Settings: OAuth2Settings;
  46 +
  47 + clientAuthenticationMethods = ['basic', 'post'];
  48 + converterTypesExternalUser = ['BASIC', 'CUSTOM'];
  49 + tenantNameStrategies = ['DOMAIN', 'EMAIL', 'CUSTOM'];
  50 +
  51 + constructor(protected store: Store<AppState>,
  52 + private adminService: AdminService,
  53 + private fb: FormBuilder,
  54 + private dialogService: DialogService,
  55 + private translate: TranslateService,
  56 + @Inject(WINDOW) private window: Window) {
  57 + super(store);
  58 + }
  59 +
  60 + ngOnInit(): void {
  61 + this.buildOAuth2SettingsForm();
  62 + this.adminService.getOAuth2Settings().subscribe(
  63 + (oauth2Settings) => {
  64 + this.oauth2Settings = oauth2Settings;
  65 + this.initOAuth2Settings(this.oauth2Settings);
  66 + this.oauth2SettingsForm.reset(this.oauth2Settings);
  67 + }
  68 + );
  69 + }
  70 +
  71 + ngOnDestroy() {
  72 + super.ngOnDestroy();
  73 + this.subscriptions.forEach((subscription) => {
  74 + subscription.unsubscribe();
  75 + })
  76 + }
  77 +
  78 + get clientsDomainsParams(): FormArray {
  79 + return this.oauth2SettingsForm.get('clientsDomainsParams') as FormArray;
  80 + }
  81 +
  82 + private get formBasicGroup(): FormGroup {
  83 + const basicGroup = this.fb.group({
  84 + emailAttributeKey: ['email', [Validators.required]],
  85 + firstNameAttributeKey: [''],
  86 + lastNameAttributeKey: [''],
  87 + tenantNameStrategy: ['DOMAIN'],
  88 + tenantNamePattern: [null],
  89 + customerNamePattern: [null],
  90 + defaultDashboardName: [null],
  91 + alwaysFullScreen: [false],
  92 + });
  93 +
  94 + this.subscriptions.push(basicGroup.get('tenantNameStrategy').valueChanges.subscribe((domain) => {
  95 + if (domain === 'CUSTOM') {
  96 + basicGroup.get('tenantNamePattern').setValidators(Validators.required);
  97 + } else {
  98 + basicGroup.get('tenantNamePattern').clearValidators();
  99 + }
  100 + }));
  101 +
  102 + return basicGroup;
  103 + }
  104 +
  105 + get formCustomGroup(): FormGroup {
  106 + return this.fb.group({
  107 + url: [null, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  108 + username: [null],
  109 + password: [null]
  110 + })
  111 + }
  112 +
  113 + private buildOAuth2SettingsForm(): void {
  114 + this.oauth2SettingsForm = this.fb.group({
  115 + clientsDomainsParams: this.fb.array([])
  116 + });
  117 + }
  118 +
  119 + private initOAuth2Settings(oauth2Settings: OAuth2Settings): void {
  120 + oauth2Settings.clientsDomainsParams.forEach((domaindomain) => {
  121 + this.clientsDomainsParams.push(this.buildSettingsDomain(domaindomain));
  122 + });
  123 + }
  124 +
  125 + private buildSettingsDomain(domainParams?: DomainParams): FormGroup {
  126 + let url = this.window.location.protocol + '//' + this.window.location.hostname;
  127 + const port = this.window.location.port;
  128 + if (port !== '80' && port !== '443') {
  129 + url += ':' + port;
  130 + }
  131 + url += '/login/oauth2/code/';
  132 + const formDomain = this.fb.group({
  133 + domainName: ['', [Validators.required, Validators.pattern('((?![:/]).)*$')]],
  134 + redirectUriTemplate: [url, [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  135 + clientRegistrations: this.fb.array([])
  136 + });
  137 +
  138 + this.subscriptions.push(formDomain.get('domainName').valueChanges.subscribe((domain) => {
  139 + if (!domain) {
  140 + domain = this.window.location.hostname
  141 + }
  142 + const uri = this.window.location.protocol + `//${domain}/login/oauth2/code/`;
  143 + formDomain.get('redirectUriTemplate').patchValue(uri);
  144 + }));
  145 +
  146 + if(domainParams){
  147 + domainParams.clientRegistrations.forEach((registration) => {
  148 + this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration(registration));
  149 + })
  150 + } else {
  151 + this.clientDomainRegistrations(formDomain).push(this.buildSettingsRegistration());
  152 + }
  153 +
  154 + return formDomain;
  155 + }
  156 +
  157 + private buildSettingsRegistration(registrationData?: ClientRegistration): FormGroup {
  158 + const clientRegistration = this.fb.group({
  159 + registrationId: [null, [Validators.required]],
  160 + clientName: [null, [Validators.required]],
  161 + loginButtonLabel: [null, [Validators.required]],
  162 + loginButtonIcon: [null],
  163 + clientId: ['', [Validators.required]],
  164 + clientSecret: ['', [Validators.required]],
  165 + accessTokenUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  166 + authorizationUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  167 + scope: this.fb.array([], [Validators.required]),
  168 + jwkSetUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  169 + userInfoUri: ['', [Validators.required, Validators.pattern(this.URL_REGEXP)]],
  170 + clientAuthenticationMethod: ['post', [Validators.required]],
  171 + userNameAttributeName: ['email', [Validators.required]],
  172 + mapperConfig: this.fb.group({
  173 + allowUserCreation: [true],
  174 + activateUser: [false],
  175 + type: ['BASIC', [Validators.required]],
  176 + basic: this.formBasicGroup
  177 + }
  178 + )
  179 + });
  180 +
  181 + this.subscriptions.push(clientRegistration.get('mapperConfig.type').valueChanges.subscribe((value) => {
  182 + const mapperConfig = clientRegistration.get('mapperConfig') as FormGroup;
  183 + if (value === 'BASIC') {
  184 + mapperConfig.removeControl('custom');
  185 + mapperConfig.addControl('basic', this.formBasicGroup);
  186 + } else {
  187 + mapperConfig.removeControl('basic');
  188 + mapperConfig.addControl('custom', this.formCustomGroup);
  189 + }
  190 + }));
  191 +
  192 + if(registrationData){
  193 + registrationData.scope.forEach(() => {
  194 + (clientRegistration.get('scope') as FormArray).push(this.fb.control(''))
  195 + })
  196 + if(registrationData.mapperConfig.type !== 'BASIC'){
  197 + clientRegistration.get('mapperConfig.type').patchValue('CUSTOM');
  198 + }
  199 + }
  200 +
  201 + return clientRegistration;
  202 + }
  203 +
  204 + save(): void {
  205 + console.log(this.oauth2SettingsForm.value);
  206 + this.adminService.saveOAuth2Settings(this.oauth2SettingsForm.value).subscribe(
  207 + (oauth2Settings) => {
  208 + this.oauth2Settings = oauth2Settings;
  209 + this.oauth2SettingsForm.markAsPristine();
  210 + this.oauth2SettingsForm.markAsUntouched();
  211 + }
  212 + );
  213 + }
  214 +
  215 + confirmForm(): FormGroup {
  216 + return this.oauth2SettingsForm;
  217 + }
  218 +
  219 + addScope(event: MatChipInputEvent, control: AbstractControl): void {
  220 + const input = event.input;
  221 + const value = event.value;
  222 + const controller = control.get('scope') as FormArray;
  223 + if ((value.trim() !== '')) {
  224 + controller.push(this.fb.control(value.trim()));
  225 + }
  226 +
  227 + if (input) {
  228 + input.value = '';
  229 + }
  230 + }
  231 +
  232 + removeScope(i: number, control: AbstractControl): void {
  233 + const controller = control.get('scope') as FormArray;
  234 + controller.removeAt(i);
  235 + }
  236 +
  237 + addDomain(): void {
  238 + this.clientsDomainsParams.push(this.buildSettingsDomain());
  239 + }
  240 +
  241 + deleteDomain($event: Event, index: number): void {
  242 + if ($event) {
  243 + $event.stopPropagation();
  244 + $event.preventDefault();
  245 + }
  246 +
  247 + const domainName = this.clientsDomainsParams.at(index).get('domainName').value;
  248 + this.dialogService.confirm(
  249 + this.translate.instant('admin.oauth2.delete-domain-title', {domainName}),
  250 + this.translate.instant('admin.oauth2.delete-domain-text'), null,
  251 + this.translate.instant('action.delete')
  252 + ).subscribe((data) => {
  253 + if (data) {
  254 + this.clientsDomainsParams.removeAt(index);
  255 + }
  256 + })
  257 + }
  258 +
  259 + clientDomainRegistrations(control: AbstractControl): FormArray {
  260 + return control.get('clientRegistrations') as FormArray;
  261 + }
  262 +
  263 + addRegistration(control: AbstractControl): void {
  264 + this.clientDomainRegistrations(control).push(this.buildSettingsRegistration());
  265 + }
  266 +
  267 + deleteRegistration($event: Event, controler: AbstractControl, index: number): void {
  268 + if ($event) {
  269 + $event.stopPropagation();
  270 + $event.preventDefault();
  271 + }
  272 +
  273 + const registrationId = this.clientDomainRegistrations(controler).at(index).get('registrationId').value;
  274 + this.dialogService.confirm(
  275 + this.translate.instant('admin.oauth2.delete-registration-title', {name: registrationId}),
  276 + this.translate.instant('admin.oauth2.delete-registration-text'), null,
  277 + this.translate.instant('action.delete')
  278 + ).subscribe((data) => {
  279 + if (data) {
  280 + this.clientDomainRegistrations(controler).removeAt(index);
  281 + }
  282 + })
  283 + }
  284 +}
... ...
... ... @@ -54,6 +54,9 @@
54 54 <mat-label translate>tenant.description</mat-label>
55 55 <textarea matInput formControlName="description" rows="2"></textarea>
56 56 </mat-form-field>
  57 + <mat-checkbox fxFlex formControlName="allowOAuth2Configuration" style="padding-bottom: 16px;">
  58 + {{ 'tenant.allow-oauth2-configuration' | translate }}
  59 + </mat-checkbox>
57 60 </div>
58 61 <tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
59 62 <div fxLayout="column">
... ...
... ... @@ -23,6 +23,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
23 23 import { TranslateService } from '@ngx-translate/core';
24 24 import { ContactBasedComponent } from '../../components/entity/contact-based.component';
25 25 import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
  26 +import { isDefined } from '@core/utils';
26 27
27 28 @Component({
28 29 selector: 'tb-tenant',
... ... @@ -55,7 +56,9 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
55 56 isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []],
56 57 additionalInfo: this.fb.group(
57 58 {
58   - description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
  59 + description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
  60 + allowOAuth2Configuration: [isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
  61 + entity.additionalInfo.allowOAuth2Configuration : true]
59 62 }
60 63 )
61 64 }
... ... @@ -66,7 +69,11 @@ export class TenantComponent extends ContactBasedComponent<Tenant> {
66 69 this.entityForm.patchValue({title: entity.title});
67 70 this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore});
68 71 this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine});
69   - this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
  72 + this.entityForm.patchValue({additionalInfo: {
  73 + description: entity.additionalInfo ? entity.additionalInfo.description : '',
  74 + allowOAuth2Configuration: isDefined(entity?.additionalInfo?.allowOAuth2Configuration) ?
  75 + entity.additionalInfo.allowOAuth2Configuration : true
  76 + }});
70 77 }
71 78
72 79 updateFormState() {
... ...
... ... @@ -58,6 +58,7 @@ export const HelpLinks = {
58 58 linksMap: {
59 59 outgoingMailSettings: helpBaseUrl + '/docs/user-guide/ui/mail-settings',
60 60 securitySettings: helpBaseUrl + '/docs/user-guide/ui/security-settings',
  61 + oauth2Settings: helpBaseUrl + '/docs/user-guide/oauth-2-support/',
61 62 ruleEngine: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/overview/',
62 63 ruleNodeCheckRelation: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node',
63 64 ruleNodeCheckExistenceFields: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node',
... ... @@ -108,7 +109,7 @@ export const HelpLinks = {
108 109 widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',
109 110 widgetsConfigRpc: helpBaseUrl + '/docs/user-guide/ui/dashboards#rpc',
110 111 widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm',
111   - widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static'
  112 + widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static',
112 113 }
113 114 };
114 115
... ...
... ... @@ -23,6 +23,10 @@ export interface AdminSettings<T> {
23 23
24 24 export declare type SmtpProtocol = 'smtp' | 'smtps';
25 25
  26 +export declare type ClientAuthenticationMethod = 'basic' | 'post';
  27 +export declare type MapperConfigType = 'BASIC' | 'CUSTOM';
  28 +export declare type TenantNameStrategy = 'DOMAIN' | 'EMAIL' | 'CUSTOM';
  29 +
26 30 export interface MailServerSettings {
27 31 mailFrom: string;
28 32 smtpProtocol: SmtpProtocol;
... ... @@ -60,3 +64,55 @@ export interface UpdateMessage {
60 64 message: string;
61 65 updateAvailable: boolean;
62 66 }
  67 +
  68 +export interface OAuth2Settings {
  69 + clientsDomainsParams: DomainParams[];
  70 +}
  71 +
  72 +export interface DomainParams {
  73 + domainName: string;
  74 + redirectUriTemplate: string;
  75 + clientRegistrations: ClientRegistration[];
  76 +}
  77 +
  78 +export interface ClientRegistration {
  79 + registrationId: string;
  80 + clientName: string;
  81 + loginButtonLabel: string;
  82 + loginButtonIcon: string;
  83 + clientId: string;
  84 + clientSecret: string;
  85 + accessTokenUri: string;
  86 + authorizationUri: string;
  87 + scope: string[];
  88 + jwkSetUri: string;
  89 + userInfoUri: string;
  90 + clientAuthenticationMethod: ClientAuthenticationMethod
  91 + userNameAttributeName: string;
  92 + mapperConfig: MapperConfig
  93 +}
  94 +
  95 +export interface MapperConfig {
  96 + allowUserCreation: boolean;
  97 + activateUser: boolean;
  98 + type: MapperConfigType;
  99 + basic?: MapperConfigBasic;
  100 + custom?: MapperConfigCustom;
  101 +}
  102 +
  103 +export interface MapperConfigBasic {
  104 + emailAttributeKey: string;
  105 + firstNameAttributeKey?: string;
  106 + lastNameAttributeKey?: string;
  107 + tenantNameStrategy: TenantNameStrategy;
  108 + tenantNamePattern?: string;
  109 + customerNamePattern?: string;
  110 + defaultDashboardName?: string;
  111 + alwaysFullScreen?: boolean;
  112 +}
  113 +
  114 +export interface MapperConfigCustom {
  115 + url: string;
  116 + username?: string;
  117 + password?: string;
  118 +}
... ...
... ... @@ -119,8 +119,62 @@
119 119 "general-policy": "General policy",
120 120 "max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
121 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"
123   - },
  122 + "user-lockout-notification-email": "In case user account lockout, send notification to email",
  123 + "domain-name": "Domain name",
  124 + "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
  125 + "add-domain": "Add domain",
  126 + "new-domain": "New domain",
  127 + "add-registration": "Add registration",
  128 + "oauth2": {
  129 + "settings": "OAuth2 settings",
  130 + "registration-id": "Registration ID",
  131 + "registration-id-required": "Registration ID is required.",
  132 + "client-name": "Client name",
  133 + "client-name-required": "Client name is required.",
  134 + "client-id": "Client ID",
  135 + "client-id-required": "Client ID is required.",
  136 + "client-secret": "Client secret",
  137 + "client-secret-required": "Client secret is required.",
  138 + "access-token-uri": "Access token URI",
  139 + "access-token-uri-required": "Access token URI is required.",
  140 + "authorization-uri": "Authorization URI",
  141 + "authorization-uri-required": "Authorization URI is required.",
  142 + "uri-pattern-error": "Invalid URI format.",
  143 + "scope": "Scope",
  144 + "redirect-uri-template": "Redirect URI template",
  145 + "redirect-uri-template-required": "Redirect URI template is required.",
  146 + "jwk-set-uri": "JSON Web Key URI",
  147 + "jwk-set-uri-required": "JSON Web Key URI is required.",
  148 + "user-info-uri": "User info URI",
  149 + "user-info-uri-required": "User info URI is required.",
  150 + "client-authentication-method": "Client authentication method",
  151 + "user-name-attribute-name": "User name attribute key",
  152 + "user-name-attribute-name-required": "User name attribute key is required",
  153 + "allow-user-creation": "Allow user creation",
  154 + "activate-user": "Activate user",
  155 + "type": "Mapper type",
  156 + "email-attribute-key": "Email attribute key",
  157 + "email-attribute-key-required": "Email attribute key is required.",
  158 + "first-name-attribute-key": "First name attribute key",
  159 + "last-name-attribute-key": "Last name attribute key",
  160 + "tenant-name-strategy": "Tenant name strategy",
  161 + "tenant-name-pattern": "Tenant name pattern",
  162 + "tenant-name-pattern-required": "Tenant name pattern is required.",
  163 + "customer-name-pattern": "Customer name pattern",
  164 + "default-dashboard-name": "Default dashboard name",
  165 + "always-fullscreen": "Always fullscreen",
  166 + "url": "URL",
  167 + "url-required": "URL is required.",
  168 + "url-pattern": "Invalid URL format.",
  169 + "login-button-label": "Login button label",
  170 + "login-button-label-required": "Login button label is required.",
  171 + "login-button-icon": "Login button icon",
  172 + "delete-domain-title": "Are you sure you want to delete the domain '{{domainName}}'?",
  173 + "delete-domain-text": "Be careful, after the confirmation a domain and all registration data will be unavailable.",
  174 + "delete-registration-title": "Are you sure you want to delete the registration '{{name}}'?",
  175 + "delete-registration-text": "Be careful, after the confirmation a registration data will be unavailable."
  176 + }
  177 + },
124 178 "alarm": {
125 179 "alarm": "Alarm",
126 180 "alarms": "Alarms",
... ... @@ -1561,7 +1615,8 @@
1561 1615 "isolated-tb-core": "Processing in isolated ThingsBoard Core container",
1562 1616 "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container",
1563 1617 "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant",
1564   - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant"
  1618 + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant",
  1619 + "allow-oauth2-configuration": "Allow OAuth2 configuration"
1565 1620 },
1566 1621 "timeinterval": {
1567 1622 "seconds-interval": "{ seconds, plural, 1 {1 second} other {# seconds} }",
... ...