[ADF-2795] SSO implicitflow (#3332)

* Enable OAUTH2

* Create SSO services

* SSO improvements

* Rollback sso login change

* Add SSO configuration from Setting component

* Refactoring

* Remove login ECM/BPM toggle and move use the userpreference instead of store

* fix host setting unit test

* Fix unit test missing instance

* use the Js api oauth

* add logout component and clean sso not used class

* fix dependencies cicle

* add translation settings

* fix style setting page

* clean

* JS APi should receive the oauth config from the userPreference and not from the config file

* change login if SSO is present

* missing spaces

* add sso test in login component

* add logout directive new properties test

* Improve host setting and remove library reference

* fix login test

* Remove unused code

* Fix authentication unit test

* fix authguard unit test

* fix csrf check login component

* fix unit test core and demo shell

* remove
This commit is contained in:
Maurizio Vitale
2018-06-07 23:19:58 +01:00
committed by Eugenio Romano
parent 3a6c12e624
commit f8e92b2fb0
57 changed files with 1295 additions and 681 deletions

View File

@@ -18,10 +18,10 @@
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { Http } from '@angular/http';
import { AuthenticationService } from '../services/authentication.service';
import { AppConfigService } from '../app-config/app-config.service';
import { BpmProductVersionModel, EcmProductVersionModel } from '../models/product-version.model';
import { DiscoveryApiService } from '../services/discovery-api.service';
import { ObjectDataTableAdapter } from '../datatable/data/object-datatable-adapter';
import { UserPreferencesService } from '../services/user-preferences.service';
@Component({
selector: 'adf-about',
@@ -44,7 +44,7 @@ export class AboutComponent implements OnInit {
bpmVersion: BpmProductVersionModel = null;
constructor(private http: Http,
private appConfig: AppConfigService,
private userPreference: UserPreferencesService,
private authService: AuthenticationService,
private discovery: DiscoveryApiService) {
}
@@ -114,8 +114,8 @@ export class AboutComponent implements OnInit {
});
this.ecmHost = this.appConfig.get<string>('ecmHost');
this.bpmHost = this.appConfig.get<string>('bpmHost');
this.ecmHost = this.userPreference.ecmHost;
this.bpmHost = this.userPreference.bpmHost;
}
private gitHubLinkCreation(alfrescoPackagesTableRepresentation): void {

View File

@@ -461,12 +461,17 @@
"oauth2": {
"description": "AUTH configuration parameters",
"type": "object",
"required": [ "host", "clientId", "secret" ],
"required": [ "host", "clientId", "secret", "scope" ],
"properties": {
"host": { "type": "string" },
"silentLogin": { "type": "boolean" },
"authPath": { "type": "string" },
"clientId": { "type": "string" },
"secret": { "type": "string" }
"secret": { "type": "string" },
"redirectUri": { "type": "string" },
"redirectUriLogout": { "type": "string" },
"silentRefreshRedirectUri": { "type": "string" },
"scope": { "type": "string" }
}
},
"adf-version-manager": {

View File

@@ -17,6 +17,7 @@
import { CommonModule, DatePipe } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule, ModuleWithProviders } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Component } from '@angular/core';
import { Component, ContentChildren } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
@@ -23,56 +23,152 @@ import { Observable } from 'rxjs/Observable';
import { AuthenticationService } from '../services';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreModule } from '../core.module';
import { LogoutDirective } from './logout.directive';
describe('LogoutDirective', () => {
@Component({
selector: 'adf-test-component',
template: '<button adf-logout></button>'
})
class TestComponent {}
describe('No input', () => {
let fixture: ComponentFixture<TestComponent>;
let router: Router;
let authService: AuthenticationService;
@Component({
selector: 'adf-test-component',
template: '<button adf-logout></button>'
})
class TestComponent {
@ContentChildren(LogoutDirective)
logoutDirective: LogoutDirective;
}
let fixture: ComponentFixture<TestComponent>;
let router: Router;
let authService: AuthenticationService;
setupTestBed({
imports: [
CoreModule.forRoot(),
RouterTestingModule
],
declarations: [
TestComponent
]
});
beforeEach(() => {
router = TestBed.get(Router);
authService = TestBed.get(AuthenticationService);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should redirect to login on click', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.of(true));
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
it('should redirect to login even on logout error', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.throw('err'));
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/login']);
});
setupTestBed({
imports: [
CoreModule.forRoot(),
RouterTestingModule
],
declarations: [
TestComponent
]
});
beforeEach(() => {
router = TestBed.get(Router);
authService = TestBed.get(AuthenticationService);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
describe('redirectUri', () => {
@Component({
selector: 'adf-test-component',
template: '<button adf-logout redirectUri="/myCustomUri"></button>'
})
class TestComponent {
@ContentChildren(LogoutDirective)
logoutDirective: LogoutDirective;
}
let fixture: ComponentFixture<TestComponent>;
let router: Router;
let authService: AuthenticationService;
setupTestBed({
imports: [
CoreModule.forRoot(),
RouterTestingModule
],
declarations: [
TestComponent
]
});
beforeEach(() => {
router = TestBed.get(Router);
authService = TestBed.get(AuthenticationService);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
it('should redirect to the the input redirectUri on click if present', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.of(true));
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith(['/myCustomUri']);
});
});
it('should redirect to login on click', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.of(true));
describe('redirectUri', () => {
const button = fixture.nativeElement.querySelector('button');
button.click();
@Component({
selector: 'adf-test-component',
template: '<button adf-logout [enabelRedirect]="false"></button>'
})
class TestComponent {
@ContentChildren(LogoutDirective)
logoutDirective: LogoutDirective;
}
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith([ '/login' ]);
});
let fixture: ComponentFixture<TestComponent>;
let router: Router;
let authService: AuthenticationService;
it('should redirect to login even on logout error', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.throw('err'));
setupTestBed({
imports: [
CoreModule.forRoot(),
RouterTestingModule
],
declarations: [
TestComponent
]
});
const button = fixture.nativeElement.querySelector('button');
button.click();
beforeEach(() => {
router = TestBed.get(Router);
authService = TestBed.get(AuthenticationService);
fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
});
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).toHaveBeenCalledWith([ '/login' ]);
it('should not redirect if enabelRedirect is false', () => {
spyOn(router, 'navigate').and.callThrough();
spyOn(authService, 'logout').and.returnValue(Observable.of(true));
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(authService.logout).toHaveBeenCalled();
expect(router.navigate).not.toHaveBeenCalled();
});
});
});

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
import { Directive, ElementRef, OnInit, Renderer2 } from '@angular/core';
import { Input, Directive, ElementRef, OnInit, Renderer2 } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/authentication.service';
@@ -24,11 +24,18 @@ import { AuthenticationService } from '../services/authentication.service';
})
export class LogoutDirective implements OnInit {
constructor(
private elementRef: ElementRef,
private renderer: Renderer2,
private router: Router,
private auth: AuthenticationService) {
/** Uri to be redirect after the logout default value login */
@Input()
redirectUri: string = '/login';
/** Enable redirect after logout */
@Input()
enabelRedirect: boolean = true;
constructor(private elementRef: ElementRef,
private renderer: Renderer2,
private router: Router,
private auth: AuthenticationService) {
}
ngOnInit() {
@@ -42,12 +49,14 @@ export class LogoutDirective implements OnInit {
logout() {
this.auth.logout().subscribe(
() => this.redirectToLogin(),
() => this.redirectToLogin()
() => this.redirectToUri(),
() => this.redirectToUri()
);
}
redirectToLogin() {
this.router.navigate(['/login']);
redirectToUri() {
if (this.enabelRedirect) {
this.router.navigate([this.redirectUri]);
}
}
}

View File

@@ -16,20 +16,28 @@
*/
import { SimpleChange } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { fakeAsync, tick, TestBed } from '@angular/core/testing';
import { NodeFavoriteDirective } from './node-favorite.directive';
import { AlfrescoApiServiceMock } from '../mock/alfresco-api.service.mock';
import { AppConfigService } from '../app-config/app-config.service';
import { StorageService } from '../services/storage.service';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { UserPreferencesService } from '../services/user-preferences.service';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreTestingModule } from '../testing/core.testing.module';
describe('NodeFavoriteDirective', () => {
let directive;
let alfrescoApiService: AlfrescoApiService;
let alfrescoApiService;
let userPreferences;
setupTestBed({
imports: [CoreTestingModule]
});
beforeEach(() => {
alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), new StorageService());
userPreferences = TestBed.get(UserPreferencesService);
alfrescoApiService = new AlfrescoApiServiceMock(new AppConfigService(null), userPreferences, new StorageService());
directive = new NodeFavoriteDirective( alfrescoApiService);
});

View File

@@ -118,6 +118,7 @@
"ERROR_PLURAL": "{{ number }} items couldn't be deleted"
},
"HOST_SETTINGS": {
"REQUIRED": "The field is required",
"CS_URL_ERROR": "Content Services address doesn't match the URL format",
"PS_URL_ERROR": "Process Services address doesn't match the URL format",
"TITLE": "Settings",
@@ -125,7 +126,11 @@
"BP-HOST": "Process Services URL",
"BACK": "Back",
"APPLY": "APPLY",
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL."
"NOT_VALID": "http(s)://host|ip:port(/path) not recognized, try a different URL.",
"REDIRECT": "Redirect Uri",
"SILENT": "Silent Login",
"SCOPE": "Scope",
"CLIENT": "ClientId"
},
"CARDVIEW": {
"VALIDATORS": {
@@ -193,7 +198,8 @@
"BUTTON": {
"LOGIN": "SIGN IN",
"CHECKING": "CHECKING",
"WELCOME": "WELCOME"
"WELCOME": "WELCOME",
"SSO": "SIGN IN SSO"
},
"ACTION": {
"HELP": "NEED HELP?",

View File

@@ -1,6 +1,6 @@
<div class="adf-login-content" [style.background-image]="'url(' + backgroundImageUrl + ')'">
<div class="ie11FixerParent">
<div class="ie11FixerChild">
<div class="ie11FixerParent">
<div class="ie11FixerChild">
<mat-card class="adf-login-card-wide">
<form id="adf-login-form" [formGroup]="form" (submit)="onSubmit(form.value)" autocomplete="off">
@@ -10,107 +10,128 @@
<div class="adf-alfresco-logo">
<!--HEADER TEMPLATE-->
<ng-template *ngIf="headerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
ngFor [ngForOf]="[data]"
[ngForTemplate]="headerTemplate">
</ng-template>
<img *ngIf="!headerTemplate" class="adf-img-logo" [src]="logoImageUrl"
alt="{{'LOGIN.LOGO' | translate }}">
alt="{{'LOGIN.LOGO' | translate }}">
</div>
</mat-card-title>
</mat-card-header>
<mat-card-content class="adf-login-controls">
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
class="error adf-error-message">
<mat-icon class="error-icon">warning</mat-icon>
<span class="login-error-message">{{errorMsg | translate }}</span>
</div>
</div>
<!--USERNAME FIELD-->
<div class="adf-login__field" [ngClass]="{'is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
type="text"
class="adf-full-width"
[formControl]="form.controls['username']"
autocapitalize="none"
id="username"
data-automation-id="username"
(blur)="trimUsername($event)"
tabindex="1">
</mat-form-field>
<span class="adf-login-validation" for="username" *ngIf="formError.username">
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError.username | translate }}</span>
</span>
</div>
<!--PASSWORD FIELD-->
<div class="adf-login__field">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
type="password"
[formControl]="form.controls['password']"
id="password"
data-automation-id="password"
tabindex="2">
<mat-icon *ngIf="isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="hide_password" (click)="toggleShowPassword()">visibility
</mat-icon>
<mat-icon *ngIf="!isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="show_password" (click)="toggleShowPassword()">visibility_off
</mat-icon>
</mat-form-field>
<span class="adf-login-validation" for="password" *ngIf="formError.password">
<span id="password-required" class="adf-login-error"
data-automation-id="password-required">{{formError.password | translate }}</span>
</span>
</div>
<!--CUSTOM CONTENT-->
<ng-content></ng-content>
<br>
<button type="submit" id="login-button" tabindex="3"
class="adf-login-button"
mat-raised-button color="primary"
[class.isChecking]="actualLoginStep === LoginSteps.Checking"
[class.isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button" [disabled]="!form.valid">
<span *ngIf="actualLoginStep === LoginSteps.Landing" class="adf-login-button-label">{{ 'LOGIN.BUTTON.LOGIN' | translate }}</span>
<div *ngIf="actualLoginStep === LoginSteps.Checking" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.CHECKING' | translate }}</span>
<div class="adf-login-spinner-container">
<mat-spinner id="checking-spinner" class="adf-login-checking-spinner" [diameter]="25"></mat-spinner>
<div *ngIf="!implicitFlow">
<!--ERRORS AREA-->
<div class="adf-error-container">
<div *ngIf="isError" id="login-error" data-automation-id="login-error"
class="error adf-error-message">
<mat-icon class="error-icon">warning</mat-icon>
<span class="login-error-message">{{errorMsg | translate }}</span>
</div>
</div>
<!--USERNAME FIELD-->
<div class="adf-login__field"
[ngClass]="{'is-invalid': isErrorStyle(form.controls.username)}">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.USERNAME' | translate }}"
type="text"
class="adf-full-width"
[formControl]="form.controls['username']"
autocapitalize="none"
id="username"
data-automation-id="username"
(blur)="trimUsername($event)"
tabindex="1">
</mat-form-field>
<div *ngIf="actualLoginStep === LoginSteps.Welcome" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="welcome-icon">done</mat-icon>
<span class="adf-login-validation" for="username" *ngIf="formError.username">
<span id="username-error" class="adf-login-error" data-automation-id="username-error">{{formError.username | translate }}</span>
</span>
</div>
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me">
<mat-checkbox id="adf-login-remember" color="primary" class="adf-login-rememberme" [checked]="rememberMe"
(change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate }}
</mat-checkbox>
<!--PASSWORD FIELD-->
<div class="adf-login__field">
<mat-form-field class="adf-full-width" floatPlaceholder="never" color="primary">
<input matInput placeholder="{{'LOGIN.LABEL.PASSWORD' | translate }}"
type="password"
[formControl]="form.controls['password']"
id="password"
data-automation-id="password"
tabindex="2">
<mat-icon *ngIf="isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="hide_password" (click)="toggleShowPassword()">
visibility
</mat-icon>
<mat-icon *ngIf="!isPasswordShow" matSuffix class="adf-login-password-icon"
data-automation-id="show_password" (click)="toggleShowPassword()">
visibility_off
</mat-icon>
</mat-form-field>
<span class="adf-login-validation" for="password" *ngIf="formError.password">
<span id="password-required" class="adf-login-error"
data-automation-id="password-required">{{formError.password | translate }}</span>
</span>
</div>
<!--CUSTOM CONTENT-->
<ng-content></ng-content>
<br>
<button type="submit" id="login-button" tabindex="3"
class="adf-login-button"
mat-raised-button color="primary"
[class.isChecking]="actualLoginStep === LoginSteps.Checking"
[class.isWelcome]="actualLoginStep === LoginSteps.Welcome"
data-automation-id="login-button" [disabled]="!form.valid">
<span *ngIf="actualLoginStep === LoginSteps.Landing" class="adf-login-button-label">{{ 'LOGIN.BUTTON.LOGIN' | translate }}</span>
<div *ngIf="actualLoginStep === LoginSteps.Checking"
class="adf-interactive-login-label">
<span
class="adf-login-button-label">{{ 'LOGIN.BUTTON.CHECKING' | translate }}</span>
<div class="adf-login-spinner-container">
<mat-spinner id="checking-spinner" class="adf-login-checking-spinner"
[diameter]="25"></mat-spinner>
</div>
</div>
<div *ngIf="actualLoginStep === LoginSteps.Welcome" class="adf-interactive-login-label">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.WELCOME' | translate }}</span>
<mat-icon class="welcome-icon">done</mat-icon>
</div>
</button>
<div *ngIf="showRememberMe" class="adf-login__remember-me">
<mat-checkbox id="adf-login-remember" color="primary" class="adf-login-rememberme"
[checked]="rememberMe"
(change)="rememberMe = !rememberMe">{{ 'LOGIN.LABEL.REMEMBER' | translate
}}
</mat-checkbox>
</div>
</div>
<div *ngIf="implicitFlow">
<button type="button" (click)="implicitLogin()" id="login-button-sso" tabindex="1"
class="adf-login-button"
mat-raised-button color="primary"
data-automation-id="login-button-sso">
<span class="adf-login-button-label">{{ 'LOGIN.BUTTON.SSO' | translate }}</span>
</button>
</div>
</mat-card-content>
<mat-card-actions *ngIf="footerTemplate || showLoginActions">
<div class="adf-login-action-container">
<!--FOOTER TEMPLATE-->
<ng-template *ngIf="footerTemplate"
ngFor [ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
ngFor [ngForOf]="[data]"
[ngForTemplate]="footerTemplate">
</ng-template>
<div class="adf-login-action" *ngIf="!footerTemplate && showLoginActions">
<div id="adf-login-action-left" class="adf-login-action-left">
@@ -131,5 +152,5 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -58,7 +58,7 @@ describe('LoginComponent', () => {
imports: [CoreTestingModule]
});
beforeEach(() => {
beforeEach(async(() => {
fixture = TestBed.createComponent(LoginComponent);
element = fixture.nativeElement;
@@ -66,15 +66,17 @@ describe('LoginComponent', () => {
component.showRememberMe = true;
component.showLoginActions = true;
usernameInput = element.querySelector('#username');
passwordInput = element.querySelector('#password');
authService = TestBed.get(AuthenticationService);
router = TestBed.get(Router);
userPreferences = TestBed.get(UserPreferencesService);
fixture.detectChanges();
});
fixture.whenStable().then(() => {
usernameInput = element.querySelector('#username');
passwordInput = element.querySelector('#password');
});
}));
afterEach(() => {
fixture.destroy();
@@ -98,7 +100,7 @@ describe('LoginComponent', () => {
});
it('should redirect to route on successful login', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
const redirect = '/home';
component.successRoute = redirect;
spyOn(router, 'navigate');
@@ -107,10 +109,10 @@ describe('LoginComponent', () => {
});
it('should redirect to previous route state on successful login', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
const redirect = '/home';
component.successRoute = redirect;
authService.setRedirect({ provider: 'ECM', navigation: ['some-route'] } );
authService.setRedirect({ provider: 'ECM', navigation: ['some-route'] });
spyOn(router, 'navigate');
@@ -158,7 +160,7 @@ describe('LoginComponent', () => {
});
it('should be changed to the "welcome key" after a successful login attempt', () => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
loginWithCredentials('fake-username', 'fake-password');
expect(getLoginButtonText()).toEqual('LOGIN.BUTTON.WELCOME');
@@ -382,7 +384,7 @@ describe('LoginComponent', () => {
});
it('should return success event after the login have succeeded', (done) => {
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket'}));
spyOn(authService, 'login').and.returnValue(Observable.of({ type: 'type', ticket: 'ticket' }));
component.providers = 'ECM';
expect(component.isError).toBe(false);
@@ -514,7 +516,7 @@ describe('LoginComponent', () => {
expect(component.isError).toBe(false);
expect(event).toEqual(
new LoginSuccessEvent({type: 'type', ticket: 'ticket'}, 'fake-username', null)
new LoginSuccessEvent({ type: 'type', ticket: 'ticket' }, 'fake-username', null)
);
});
@@ -583,4 +585,47 @@ describe('LoginComponent', () => {
loginWithCredentials('fake-username', 'fake-password');
}));
describe('SSO', () => {
beforeEach(() => {
userPreferences.oauthConfig = { implicitFlow: true };
});
afterEach(() => {
userPreferences.oauthConfig = null;
});
it('should not show login username and password if SSO implicit flow is active', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#username')).toBeNull();
expect(element.querySelector('#password')).toBeNull();
}));
it('should not show the login base auth button', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
userPreferences.oauthConfig = { implicitFlow: true };
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#login-button')).toBeNull();
}));
it('should show the login SSO button', async(() => {
spyOn(authService, 'isOauth').and.returnValue(true);
userPreferences.oauthConfig = { implicitFlow: true };
component.ngOnInit();
fixture.detectChanges();
expect(element.querySelector('#login-button-sso')).toBeDefined();
}));
});
});

View File

@@ -120,6 +120,8 @@ export class LoginComponent implements OnInit {
@Output()
executeSubmit = new EventEmitter<LoginSubmitEvent>();
implicitFlow: boolean = false;
form: FormGroup;
isError: boolean = false;
errorMsg: string;
@@ -154,6 +156,12 @@ export class LoginComponent implements OnInit {
}
ngOnInit() {
if (this.authService.isOauth()) {
if (this.userPreferences.oauthConfig && this.userPreferences.oauthConfig.implicitFlow) {
this.implicitFlow = true;
}
}
if (this.hasCustomFiledsValidation()) {
this.form = this._fb.group(this.fieldsValidation);
} else {
@@ -178,7 +186,7 @@ export class LoginComponent implements OnInit {
this.settingsService.csrfDisabled = this.disableCsrf;
this.disableError();
const args = new LoginSubmitEvent({controls : { username : this.form.controls.username} });
const args = new LoginSubmitEvent({ controls: { username: this.form.controls.username } });
this.executeSubmit.emit(args);
if (args.defaultPrevented) {
@@ -188,6 +196,10 @@ export class LoginComponent implements OnInit {
}
}
implicitLogin() {
this.authService.ssoImplictiLogin();
}
/**
* The method check the error in the form and push the error in the formError object
* @param data

View File

@@ -16,49 +16,21 @@
*/
import { Injectable } from '@angular/core';
import { AlfrescoApi } from 'alfresco-js-api';
import * as alfrescoApi from 'alfresco-js-api';
import { AppConfigService } from '../app-config/app-config.service';
import { StorageService } from '../services/storage.service';
import { AlfrescoApiService } from '../services/alfresco-api.service';
import { UserPreferencesService } from '../services/user-preferences.service';
/* tslint:disable:adf-file-name */
@Injectable()
export class AlfrescoApiServiceMock extends AlfrescoApiService {
constructor(protected appConfig: AppConfigService,
protected userPreference: UserPreferencesService,
protected storage: StorageService) {
super(appConfig, storage);
super(appConfig, userPreference, storage);
if (!this.alfrescoApi) {
this.initAlfrescoApi();
}
}
async load() {
await this.appConfig.load().then(() => {
if (!this.alfrescoApi) {
this.initAlfrescoApi();
}
});
}
async reset() {
if (this.alfrescoApi) {
this.alfrescoApi = null;
}
this.initAlfrescoApi();
}
protected initAlfrescoApi() {
this.alfrescoApi = <AlfrescoApi> new alfrescoApi({
provider: this.storage.getItem('AUTH_TYPE'),
ticketEcm: this.storage.getItem('ticket-ECM'),
ticketBpm: this.storage.getItem('ticket-BPM'),
hostEcm: this.appConfig.get<string>('ecmHost'),
hostBpm: this.appConfig.get<string>('bpmHost'),
contextRoot: 'alfresco',
disableCsrf: this.storage.getItem('DISABLE_CSRF') === 'true',
oauth2: this.appConfig.get<any>('oauth2')
});
}
}

View File

@@ -0,0 +1,29 @@
/*!
* @license
* Copyright 2016 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface OauthConfigModel {
host: string;
clientId: string;
scope: string;
implicitFlow: boolean;
redirectUri: string;
silentLogin?: boolean;
silentRefreshRedirectUri?: string;
secret?: string;
refreshTokenTimeout?: number;
redirectUriLogout?: string;
}

View File

@@ -25,6 +25,7 @@ import * as alfrescoApi from 'alfresco-js-api';
import { AppConfigService } from '../app-config/app-config.service';
import { StorageService } from './storage.service';
import { Subject } from 'rxjs/Subject';
import { UserPreferencesService } from './user-preferences.service';
/* tslint:disable:adf-file-name */
@@ -95,6 +96,7 @@ export class AlfrescoApiService {
}
constructor(protected appConfig: AppConfigService,
protected userPreference: UserPreferencesService,
protected storage: StorageService) {
}
@@ -105,23 +107,32 @@ export class AlfrescoApiService {
}
async reset() {
if (this.alfrescoApi) {
this.alfrescoApi = null;
}
this.initAlfrescoApi();
}
protected initAlfrescoApi() {
this.alfrescoApi = <AlfrescoApi> new alfrescoApi({
provider: this.storage.getItem('AUTH_TYPE'),
let oauth: any = Object.assign({}, this.userPreference.oauthConfig);
if (oauth) {
oauth.redirectUri = window.location.origin + (oauth.redirectUri || '/');
oauth.redirectUriLogout = window.location.origin + (oauth.redirectUriLogout || '/');
}
const config = {
provider: this.userPreference.providers,
ticketEcm: this.storage.getItem('ticket-ECM'),
ticketBpm: this.storage.getItem('ticket-BPM'),
hostEcm: this.appConfig.get<string>('ecmHost'),
hostBpm: this.appConfig.get<string>('bpmHost'),
hostEcm: this.userPreference.ecmHost,
hostBpm: this.userPreference.bpmHost,
contextRootBpm: this.appConfig.get<string>('contextRootBpm'),
contextRoot: this.appConfig.get<string>('contextRootEcm'),
disableCsrf: this.storage.getItem('DISABLE_CSRF') === 'true',
oauth2: this.appConfig.get<any>('oauth2')
});
oauth2: oauth
};
if (this.alfrescoApi) {
this.alfrescoApi.configureJsApi(config);
} else {
this.alfrescoApi = <AlfrescoApi> new alfrescoApi(config);
}
}
}

View File

@@ -35,6 +35,7 @@ describe('AuthGuardService BPM', () => {
});
beforeEach(() => {
localStorage.clear();
authService = TestBed.get(AuthenticationService);
authGuard = TestBed.get(AuthGuardBpm);
routerService = TestBed.get(Router);

View File

@@ -38,7 +38,7 @@ export class AuthGuardEcm implements CanActivate {
}
private isLoggedIn(): Promise<boolean> {
if (!this.authApi.isLoggedIn()) {
if (this.authApi === undefined || !this.authApi.isLoggedIn()) {
return Promise.resolve(false);
}

View File

@@ -35,6 +35,7 @@ describe('AuthGuardService', () => {
});
beforeEach(() => {
localStorage.clear();
state = { url: '' };
authService = TestBed.get(AuthenticationService);
router = TestBed.get(Router);

View File

@@ -21,22 +21,24 @@ import {
CanActivateChild, RouterStateSnapshot, Router,
PRIMARY_OUTLET, UrlTree, UrlSegmentGroup, UrlSegment
} from '@angular/router';
import { AppConfigService } from '../app-config/app-config.service';
import { AuthenticationService } from './authentication.service';
import { Observable } from 'rxjs/Observable';
import { AppConfigService } from '../app-config/app-config.service';
import { UserPreferencesService } from './user-preferences.service';
@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthenticationService,
private router: Router,
private userPreference: UserPreferencesService,
private appConfig: AppConfigService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable<boolean> {
const redirectUrl = state.url;
return this.checkLogin(redirectUrl);
}
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable<boolean> {
return this.canActivate(route, state);
}
@@ -44,23 +46,29 @@ export class AuthGuard implements CanActivate, CanActivateChild {
if (this.authService.isLoggedIn()) {
return true;
}
if (!this.authService.isOauth() || this.isOAuthWithoutSilentLogin() ) {
const navigation = this.getNavigationCommands(redirectUrl);
const navigation = this.getNavigationCommands(redirectUrl);
this.authService.setRedirect({ provider: 'ALL', navigation } );
this.authService.setRedirect({ provider: 'ALL', navigation } );
const pathToLogin = this.getRouteDestinationForLogin();
this.router.navigate(['/' + pathToLogin]);
const pathToLogin = this.getRouteDestinationForLogin();
this.router.navigate(['/' + pathToLogin]);
}
return false;
}
private getRouteDestinationForLogin(): string {
isOAuthWithoutSilentLogin() {
return this.authService.isOauth() && this.userPreference.oauthConfig.silentLogin === false;
}
public getRouteDestinationForLogin(): string {
return this.appConfig &&
this.appConfig.get<string>('loginRoute') ?
this.appConfig.get<string>('loginRoute') : 'login';
}
private getNavigationCommands(redirectUrl: string): any[] {
public getNavigationCommands(redirectUrl: string): any[] {
const urlTree: UrlTree = this.router.parseUrl(redirectUrl);
const urlSegmentGroup: UrlSegmentGroup = urlTree.root.children[PRIMARY_OUTLET];

View File

@@ -60,7 +60,8 @@ describe('AuthenticationService', () => {
describe('remember me', () => {
beforeEach(() => {
preferences.authType = 'ECM';
preferences.providers = 'ECM';
apiService.reset();
});
it('[ECM] should save the remember me cookie as a session cookie after successful login', (done) => {
@@ -123,7 +124,8 @@ describe('AuthenticationService', () => {
describe('when the setting is ECM', () => {
beforeEach(() => {
preferences.authType = 'ECM';
preferences.providers = 'ECM';
apiService.reset();
});
it('should require remember me set for ECM check', () => {
@@ -154,7 +156,7 @@ describe('AuthenticationService', () => {
});
jasmine.Ajax.requests.mostRecent().respondWith({
'status': 201,
status: 201,
contentType: 'application/json',
responseText: JSON.stringify({ 'entry': { 'id': 'fake-post-ticket', 'userId': 'admin' } })
});
@@ -274,26 +276,27 @@ describe('AuthenticationService', () => {
it('[ECM] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toEqual(['some-url']);
expect(authService.getRedirect(preferences.providers)).toEqual(['some-url']);
});
it('[ECM] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toBeNull();
expect(authService.getRedirect(preferences.providers)).toBeNull();
});
it('[ECM] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
expect(authService.getRedirect(preferences.authType)).toBeNull();
expect(authService.getRedirect(preferences.providers)).toBeNull();
});
});
describe('when the setting is BPM', () => {
beforeEach(() => {
preferences.authType = 'BPM';
preferences.providers = 'BPM';
apiService.reset();
});
it('should require remember me set for BPM check', () => {
@@ -426,26 +429,27 @@ describe('AuthenticationService', () => {
it('[BPM] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toEqual(['some-url']);
expect(authService.getRedirect(preferences.providers)).toEqual(['some-url']);
});
it('[BPM] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toBeNull();
expect(authService.getRedirect(preferences.providers)).toBeNull();
});
it('[BPM] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
expect(authService.getRedirect(preferences.authType)).toBeNull();
expect(authService.getRedirect(preferences.providers)).toBeNull();
});
});
describe('when the setting is both ECM and BPM ', () => {
beforeEach(() => {
preferences.authType = 'ALL';
preferences.providers = 'ALL';
apiService.reset();
});
it('[ALL] should return both ECM and BPM tickets after the login done', (done) => {
@@ -542,25 +546,25 @@ describe('AuthenticationService', () => {
it('[ALL] should set/get redirectUrl when provider is ALL', () => {
authService.setRedirect({ provider: 'ALL', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toEqual(['some-url']);
expect(authService.getRedirect(preferences.providers)).toEqual(['some-url']);
});
it('[ALL] should set/get redirectUrl when provider is BPM', () => {
authService.setRedirect({ provider: 'BPM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toEqual(['some-url']);
expect(authService.getRedirect(preferences.providers)).toEqual(['some-url']);
});
it('[ALL] should set/get redirectUrl when provider is ECM', () => {
authService.setRedirect({ provider: 'ECM', navigation: ['some-url'] });
expect(authService.getRedirect(preferences.authType)).toEqual(['some-url']);
expect(authService.getRedirect(preferences.providers)).toEqual(['some-url']);
});
it('[ALL] should return null as redirectUrl when redirectUrl field is not set', () => {
authService.setRedirect(null);
expect(authService.getRedirect(preferences.authType)).toBeNull();
expect(authService.getRedirect(preferences.providers)).toBeNull();
});
});

View File

@@ -54,6 +54,10 @@ export class AuthenticationService {
return !!this.alfrescoApi.getInstance().isLoggedIn();
}
isOauth(): boolean {
return this.alfrescoApi.getInstance().isOauthConfiguration();
}
/**
* Logs the user in.
* @param username Username for the login
@@ -63,19 +67,26 @@ export class AuthenticationService {
*/
login(username: string, password: string, rememberMe: boolean = false): Observable<{ type: string, ticket: any }> {
this.removeTicket();
return Observable.fromPromise(this.callApiLogin(username, password))
return Observable.fromPromise(this.alfrescoApi.getInstance().login(username, password))
.map((response: any) => {
this.saveRememberMeCookie(rememberMe);
this.saveTickets();
this.onLogin.next(response);
return {
type: this.preferences.authType,
type: this.preferences.providers,
ticket: response
};
})
.catch(err => this.handleError(err));
}
/**
* Logs the user in with SSO
*/
ssoImplictiLogin() {
this.alfrescoApi.getInstance().implicitLogin();
}
/**
* Saves the "remember me" cookie as either a long-life cookie or a session cookie.
* @param rememberMe Enables a long-life cookie
@@ -100,15 +111,6 @@ export class AuthenticationService {
return (this.cookie.getItem(REMEMBER_ME_COOKIE_KEY) === null) ? false : true;
}
/**
* Initialize the alfresco Api with user and password end call the login method
* @param username
* @param password
*/
private callApiLogin(username: string, password: string) {
return this.alfrescoApi.getInstance().login(username, password);
}
/**
* Logs the user out.
* @returns Response event called when logout is complete
@@ -213,7 +215,7 @@ export class AuthenticationService {
if (this.cookie.isEnabled() && !this.isRememberMeSet()) {
return false;
}
return this.alfrescoApi.getInstance().ecmAuth && !!this.alfrescoApi.getInstance().ecmAuth.isLoggedIn();
return this.alfrescoApi.getInstance().isEcmLoggedIn();
}
/**
@@ -224,7 +226,7 @@ export class AuthenticationService {
if (this.cookie.isEnabled() && !this.isRememberMeSet()) {
return false;
}
return this.alfrescoApi.getInstance().bpmAuth && !!this.alfrescoApi.getInstance().bpmAuth.isLoggedIn();
return this.alfrescoApi.getInstance().isBpmLoggedIn();
}
/**
@@ -232,7 +234,7 @@ export class AuthenticationService {
* @returns The ECM username
*/
getEcmUsername(): string {
return this.alfrescoApi.getInstance().ecmAuth.username;
return this.alfrescoApi.getInstance().getEcmUsername();
}
/**
@@ -240,7 +242,7 @@ export class AuthenticationService {
* @returns The BPM username
*/
getBpmUsername(): string {
return this.alfrescoApi.getInstance().bpmAuth.username;
return this.alfrescoApi.getInstance().getBpmUsername();
}
/** Sets the URL to redirect to after login.

View File

@@ -17,6 +17,7 @@
import { inject, TestBed } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { AppConfigService } from '../app-config/app-config.service';
import { PageTitleService } from './page-title.service';
@@ -54,7 +55,8 @@ class TestConfig {
get: () => this.setup.applicationName,
load: () => {
return Promise.resolve();
}
},
onLoad: Observable.of({})
}
};

View File

@@ -23,25 +23,18 @@ import { UploadService } from './upload.service';
import { AppConfigService } from '../app-config/app-config.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import { AlfrescoApiServiceMock } from '../mock/alfresco-api.service.mock';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreTestingModule } from '../testing/core.testing.module';
declare let jasmine: any;
describe('UploadService', () => {
let service: UploadService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
AppConfigModule
],
providers: [
{ provide: AlfrescoApiService, useClass: AlfrescoApiServiceMock },
StorageService,
UploadService
]
}).compileComponents();
}));
setupTestBed({
imports: [CoreTestingModule, AppConfigModule]
});
beforeEach(() => {
let appConfig: AppConfigService = TestBed.get(AppConfigService);
@@ -53,6 +46,8 @@ describe('UploadService', () => {
};
service = TestBed.get(UploadService);
service.queue = [];
service.activeTask = null;
jasmine.Ajax.install();
});
@@ -162,6 +157,8 @@ describe('UploadService', () => {
});
it('If newVersion is set, name should be a param', () => {
let uploadFileSpy = spyOn(service.apiService.getInstance().upload, 'uploadFile').and.callThrough();
let emitter = new EventEmitter();
const filesFake = new FileModel(<File> { name: 'fake-name', size: 10 }, {
@@ -170,7 +167,16 @@ describe('UploadService', () => {
service.addToQueue(filesFake);
service.uploadFilesInTheQueue(emitter);
expect(jasmine.Ajax.requests.mostRecent().params.has('name')).toBe(true);
expect(uploadFileSpy).toHaveBeenCalledWith({
name: 'fake-name',
size: 10
}, undefined, undefined, null, {
renditions: 'doclib',
overwrite: true,
majorVersion: undefined,
comment: undefined,
name: 'fake-name'
});
});
it('should use custom root folder ID given to the service', (done) => {

View File

@@ -156,9 +156,9 @@ describe('UserPreferencesService', () => {
});
it('should stream only the selected attribute changes when using select', (done) => {
preferences.disableCSRF = true;
preferences.disableCSRF = false;
preferences.select(UserPreferenceValues.DisableCSRF).subscribe((disableCSRFFlag) => {
expect(disableCSRFFlag).toBeTruthy();
expect(disableCSRFFlag).toBeFalsy();
done();
});
});

View File

@@ -20,9 +20,9 @@ import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { AppConfigService } from '../app-config/app-config.service';
import { AlfrescoApiService } from './alfresco-api.service';
import { StorageService } from './storage.service';
import 'rxjs/add/operator/distinctUntilChanged';
import { OauthConfigModel } from '../models/oauth-config.model';
export enum UserPreferenceValues {
PaginationSize = 'PAGINATION_SIZE',
@@ -50,17 +50,14 @@ export class UserPreferencesService {
* @deprecated we are grouping every value changed on the user preference in a single stream : userPreferenceValue$
*/
locale$: Observable<string>;
private localeSubject: BehaviorSubject<string> ;
private localeSubject: BehaviorSubject<string>;
private onChangeSubject: BehaviorSubject<any>;
onChange: Observable<any>;
constructor(
public translate: TranslateService,
private appConfig: AppConfigService,
private storage: StorageService,
private apiService: AlfrescoApiService
) {
constructor(public translate: TranslateService,
private appConfig: AppConfigService,
private storage: StorageService) {
this.appConfig.onLoad.subscribe(this.initUserPreferenceStatus.bind(this));
this.localeSubject = new BehaviorSubject(this.userPreferenceStatus[UserPreferenceValues.Locale]);
this.locale$ = this.localeSubject.asObservable();
@@ -106,7 +103,9 @@ export class UserPreferencesService {
* @param value New value for the property
*/
set(property: string, value: any) {
if (!property) { return; }
if (!property) {
return;
}
this.storage.setItem(
this.getPropertyKey(property),
value
@@ -149,19 +148,29 @@ export class UserPreferencesService {
}
/** Authorization type (can be "ECM", "BPM" or "ALL"). */
set authType(value: string) {
this.storage.setItem('AUTH_TYPE', value);
this.apiService.reset();
/** @deprecated in 2.4.0 */
set authType(authType: string) {
let storedAuthType = this.storage.getItem('AUTH_TYPE');
if (authType !== storedAuthType) {
this.storage.setItem('AUTH_TYPE', authType);
}
}
/** @deprecated in 2.4.0 */
get authType(): string {
return this.storage.getItem('AUTH_TYPE') || 'ALL';
}
/** Prevents the CSRF Token from being submitted if true. Only valid for Process Services. */
set disableCSRF(value: boolean) {
this.set('DISABLE_CSRF', value);
this.apiService.reset();
set disableCSRF(csrf: boolean) {
let storedCSRF = this.storage.getItem('DISABLE_CSRF');
if (csrf !== null && csrf !== undefined) {
if (csrf.toString() === storedCSRF) {
this.set('DISABLE_CSRF', csrf);
}
}
}
get disableCSRF(): boolean {
@@ -196,4 +205,56 @@ export class UserPreferencesService {
return this.appConfig.get<string>('locale') || this.translate.getBrowserLang() || 'en';
}
get providers(): string {
if (this.storage.hasItem('providers')) {
return this.storage.getItem('providers');
} else {
return this.appConfig.get('providers', 'ECM');
}
}
set providers(providers: string) {
this.storage.setItem('providers', providers);
}
get bpmHost(): string {
if (this.storage.hasItem('bpmHost')) {
return this.storage.getItem('bpmHost');
} else {
return this.appConfig.get('bpmHost');
}
}
set bpmHost(bpmHost: string) {
this.storage.setItem('bpmHost', bpmHost);
}
get ecmHost(): string {
if (this.storage.hasItem('ecmHost')) {
return this.storage.getItem('ecmHost');
} else {
return this.appConfig.get('ecmHost');
}
}
set ecmHost(ecmHost: string) {
this.storage.setItem('ecmHost', ecmHost);
}
get oauthConfig(): OauthConfigModel {
if (this.storage.hasItem('oauthConfig')) {
return JSON.parse(this.storage.getItem('oauthConfig'));
} else {
return this.appConfig.get<OauthConfigModel>('oauth2');
}
}
set oauthConfig(oauthConfig: OauthConfigModel) {
this.storage.setItem('oauthConfig', JSON.stringify(oauthConfig));
}
get sso(): boolean {
return this.providers === 'OAUTH' && this.oauthConfig.implicitFlow;
}
}

View File

@@ -1,70 +1,97 @@
<div class="adf-setting-container">
<div class="adf-setting-card-padding"></div>
<mat-toolbar color="primary" class="adf-setting-toolbar">
<h3>{{'CORE.HOST_SETTINGS.TITLE' | translate}}</h3>
</mat-toolbar>
<mat-card class="adf-setting-card">
<div *ngIf="providers==='ALL' || providers==='ECM'">
<mat-card-header>
<mat-card-subtitle>{{'CORE.HOST_SETTINGS.CS-HOST' | translate }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-form-field class="full-width">
<mat-icon class="adf-CORE.HOST_SETTINGS-link-icon" matPrefix>link</mat-icon>
<input matInput
[formControl]="urlFormControlEcm"
data-automation-id="ecmHost"
type="text"
(change)="onChangeECMHost($event)"
tabindex="2"
id="ecmHost"
value="{{ecmHost}}"
placeholder="http(s)://host|ip:port(/path)">
<mat-error *ngIf="urlFormControlEcm.hasError('pattern')">
{{ 'CORE.HOST_SETTINGS.NOT_VALID'| translate }}
</mat-error>
</mat-form-field>
<p>
</mat-card-content>
</div>
<p>
<div *ngIf="providers==='ALL' || providers==='BPM'">
<mat-card-header>
<mat-card-subtitle>{{'CORE.HOST_SETTINGS.BP-HOST' | translate }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<form id="host-form" [formGroup]="form" (submit)="onSubmit(form.value)">
<mat-form-field>
<mat-select placeholder="Provider" [formControl]="providers">
<mat-option *ngFor="let provider of providersValues" [value]="provider.value">
{{ provider.title }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="full-width">
<mat-icon class="adf-CORE.HOST_SETTINGS-link-icon" matPrefix>link</mat-icon>
<input matInput
[formControl]="urlFormControlBpm"
data-automation-id="bpmHost"
type="text"
(change)="onChangeBPMHost($event)"
tabindex="2"
id="bpmHost"
value="{{bpmHost}}"
placeholder="http(s)://host|ip:port(/path)">
<mat-error *ngIf="urlFormControlBpm.hasError('pattern')">
{{ 'CORE.HOST_SETTINGS.NOT_VALID'| translate }}
</mat-error>
</mat-form-field>
</mat-card-content>
</div>
<mat-card-actions class="adf-CORE.HOST_SETTINGS-actions">
<ng-container *ngIf="isALL() || isECM()">
<mat-card-content>
<mat-form-field class="full-width" floatLabel="{{'CORE.HOST_SETTINGS.CS-HOST' | translate }}" >
<mat-label>{{'CORE.HOST_SETTINGS.CS-HOST' | translate }}</mat-label>
<input matInput [formControl]="ecmHost" data-automation-id="ecmHost" type="text" tabindex="2" id="ecmHost" placeholder="http(s)://host|ip:port(/path)">
<mat-error *ngIf="ecmHost.hasError('pattern')">
{{ 'CORE.HOST_SETTINGS.NOT_VALID'| translate }}
</mat-error>
<mat-error *ngIf="ecmHost.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
<p>
</mat-card-content>
</ng-container>
<button mat-button onclick="window.history.back()" color="primary">
{{'CORE.HOST_SETTINGS.BACK' | translate }}
</button>
<ng-container *ngIf="isALL() || isOAUTH() || isBPM()">
<mat-card-content>
<mat-form-field class="full-width" floatLabel="{{'CORE.HOST_SETTINGS.BP-HOST' | translate }}">
<mat-label>{{'CORE.HOST_SETTINGS.BP-HOST' | translate }}</mat-label>
<input matInput [formControl]="bpmHost" data-automation-id="bpmHost" type="text" tabindex="2" id="bpmHost" placeholder="http(s)://host|ip:port(/path)">
<mat-error *ngIf="bpmHost.hasError('pattern')">
{{ 'CORE.HOST_SETTINGS.NOT_VALID'| translate }}
</mat-error>
<mat-error *ngIf="bpmHost.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
<ng-container *ngIf="isOAUTH()">
<div formGroupName="oauthConfig">
<mat-form-field class="full-width" floatLabel="Auth Host">
<mat-label>Auth Host</mat-label>
<input matInput name="host" id="oauthHost" formControlName="host" placeholder="http(s)://host|ip:port(/path)" >
<mat-error *ngIf="host.hasError('pattern')">
{{ 'CORE.HOST_SETTINGS.NOT_VALID'| translate }}
</mat-error>
<mat-error *ngIf="host.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="full-width" floatLabel="Client Id">
<mat-label>{{ 'CORE.HOST_SETTINGS.CLIENT'| translate }}d</mat-label>
<input matInput name="clientId" id="clientId" formControlName="clientId" placeholder="Client Id">
<mat-error *ngIf="clientId.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
<button mat-raised-button (click)="save($event)"
[disabled]="urlFormControlBpm.hasError('pattern') || urlFormControlEcm.hasError('pattern')"
color="primary">
{{'CORE.HOST_SETTINGS.APPLY' | translate }}
</button>
<mat-form-field class="full-width" floatLabel="Scope">
<mat-label>{{ 'CORE.HOST_SETTINGS.SCOPE'| translate }}</mat-label>
<input matInput name="{{ 'CORE.HOST_SETTINGS.SCOPE'| translate }}" formControlName="scope" placeholder="Scope Id">
<mat-error *ngIf="scope.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
</mat-card-actions>
<label for="silentLogin">{{ 'CORE.HOST_SETTINGS.SILENT'| translate }}</label>
<mat-slide-toggle class="full-width" name="silentLogin" [color]="'primary'" formControlName="silentLogin">
</mat-slide-toggle>
<mat-form-field class="full-width" floatLabel="Redirect Uri">
<mat-label>{{ 'CORE.HOST_SETTINGS.REDIRECT'| translate }}</mat-label>
<input matInput placeholder="{{ 'CORE.HOST_SETTINGS.REDIRECT'| translate }}" name="redirectUri" formControlName="redirectUri">
<mat-error *ngIf="redirectUri.hasError('required')">
{{ 'CORE.HOST_SETTINGS.REQUIRED'| translate }}
</mat-error>
</mat-form-field>
</div>
</ng-container>
</mat-card-content>
</ng-container>
<mat-card-actions class="adf-actions">
<button mat-button (click)="onCancel()" color="primary">
{{'CORE.HOST_SETTINGS.BACK' | translate }}
</button>
<button type="submit" id="host-button" tabindex="4" class="adf-login-button" mat-raised-button color="primary" data-automation-id="host-button"
[disabled]="!form.valid">
{{'CORE.HOST_SETTINGS.APPLY' | translate }}
</button>
</mat-card-actions>
</form>
</mat-card>
<div class="adf-setting-card-padding"></div>
</div>

View File

@@ -5,12 +5,10 @@
height: 100%;
align-items: center;
.adf-setting-toolbar {
width: 600px;
}
.adf-setting-container {
width: 800px;
display: table;
margin: 0 auto;
border-collapse: collapse;
border-spacing: 0;
}

View File

@@ -19,11 +19,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HostSettingsComponent } from './host-settings.component';
import { setupTestBed } from '../testing/setupTestBed';
import { CoreTestingModule } from '../testing/core.testing.module';
import { UserPreferencesService } from '../services/user-preferences.service';
describe('HostSettingsComponent', () => {
let fixture: ComponentFixture<HostSettingsComponent>;
let component: HostSettingsComponent;
let userPreferences: UserPreferencesService;
let element: any;
setupTestBed({
imports: [CoreTestingModule]
@@ -32,96 +35,262 @@ describe('HostSettingsComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(HostSettingsComponent);
component = fixture.componentInstance;
userPreferences = TestBed.get(UserPreferencesService);
element = fixture.nativeElement;
});
afterEach(() => {
fixture.destroy();
});
it('should emit an error when the ECM url inserted is wrong', (done) => {
fixture.detectChanges();
describe('BPM ', () => {
component.error.subscribe((message: string) => {
expect(message).toEqual('CORE.HOST_SETTING.CS_URL_ERROR');
done();
let ecmUrlInput;
let bpmUrlInput;
beforeEach(() => {
userPreferences.providers = 'BPM';
fixture.detectChanges();
bpmUrlInput = element.querySelector('#bpmHost');
ecmUrlInput = element.querySelector('#ecmHost');
});
const ecmUrlInput = fixture.nativeElement.querySelector('#ecmHost');
ecmUrlInput.value = 'wrong_url';
const event: any = {};
event.target = ecmUrlInput;
component.onChangeECMHost(event);
});
it('should emit ecmHostChange when the ECM url inserted is correct', (done) => {
fixture.detectChanges();
const url = 'http://localhost:9999/ecm';
component.ecmHostChange.subscribe((message: string) => {
expect(message).toEqual(url);
done();
afterEach(() => {
fixture.destroy();
});
const ecmUrlInput = fixture.nativeElement.querySelector('#ecmHost');
ecmUrlInput.value = url;
it('should have a valid form when the url inserted is correct', (done) => {
const url = 'http://localhost:9999/bpm';
const event: any = {};
event.target = ecmUrlInput;
component.onChangeECMHost(event);
});
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('VALID');
done();
});
it('should emit an error when the BPM url inserted is wrong', (done) => {
fixture.detectChanges();
component.form.valueChanges.subscribe((values) => {
expect(values.bpmHost).toEqual(url);
});
component.error.subscribe((message: string) => {
expect(message).toEqual('CORE.HOST_SETTING.PS_URL_ERROR');
done();
bpmUrlInput.value = url;
bpmUrlInput.dispatchEvent(new Event('input'));
});
const bpmUrlInput: any = fixture.nativeElement.querySelector('#bpmHost');
bpmUrlInput.value = 'wrong_url';
it('should have an invalid form when the inserted is wrong', (done) => {
const url = 'wrong';
const event: any = {};
event.target = bpmUrlInput;
component.onChangeBPMHost(event);
});
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.bpmHost.hasError('pattern')).toBeTruthy();
done();
});
it('should emit bpmHostChange when the BPM url inserted is correct', (done) => {
fixture.detectChanges();
const url = 'http://localhost:9999/bpm';
component.ecmHostChange.subscribe((message: string) => {
expect(message).toEqual(url);
done();
bpmUrlInput.value = url;
bpmUrlInput.dispatchEvent(new Event('input'));
});
const ecmUrlInput = fixture.nativeElement.querySelector('#bpmHost');
ecmUrlInput.value = url;
it('should not render the ECM url config if setting provider is BPM', () => {
expect(ecmUrlInput).toEqual(null);
expect(bpmUrlInput).toBeDefined();
});
const event: any = {};
event.target = ecmUrlInput;
component.onChangeECMHost(event);
});
it('should not render the ECM url config if setting provider is BPM', () => {
component.providers = 'BPM';
describe('ECM ', () => {
fixture.detectChanges();
let ecmUrlInput;
let bpmUrlInput;
const bpmUrlInput = fixture.nativeElement.querySelector('#bpmHost');
const ecmUrlInput = fixture.nativeElement.querySelector('#ecmHost');
expect(ecmUrlInput).toEqual(null);
expect(bpmUrlInput).toBeDefined();
beforeEach(() => {
userPreferences.providers = 'ECM';
fixture.detectChanges();
bpmUrlInput = element.querySelector('#bpmHost');
ecmUrlInput = element.querySelector('#ecmHost');
});
afterEach(() => {
fixture.destroy();
});
it('should have a valid form when the url inserted is correct', (done) => {
const url = 'http://localhost:9999/ecm';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('VALID');
done();
});
ecmUrlInput.value = url;
ecmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when the url inserted is wrong', (done) => {
const url = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.ecmHost.hasError('pattern')).toBeTruthy();
done();
});
ecmUrlInput.value = url;
ecmUrlInput.dispatchEvent(new Event('input'));
});
it('should not render the BPM url config if setting provider is BPM', () => {
expect(bpmUrlInput).toEqual(null);
expect(ecmUrlInput).toBeDefined();
});
});
it('should hide the BPM url config if setting provider is ECM', () => {
component.providers = 'ECM';
describe('ALL ', () => {
fixture.detectChanges();
let ecmUrlInput;
let bpmUrlInput;
beforeEach(() => {
userPreferences.providers = 'ALL';
fixture.detectChanges();
bpmUrlInput = element.querySelector('#bpmHost');
ecmUrlInput = element.querySelector('#ecmHost');
});
afterEach(() => {
fixture.destroy();
});
it('should have a valid form when the BPM and ECM url inserted are correct', (done) => {
const urlEcm = 'http://localhost:9999/ecm';
const urlBpm = 'http://localhost:9999/bpm';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('VALID');
done();
});
ecmUrlInput.value = urlEcm;
bpmUrlInput.value = urlBpm;
ecmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when one of the ECM url inserted is wrong', (done) => {
const url = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.ecmHost.hasError('pattern')).toBeTruthy();
done();
});
ecmUrlInput.value = url;
ecmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when one of the BPM url inserted is wrong', (done) => {
const url = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.bpmHost.hasError('pattern')).toBeTruthy();
done();
});
bpmUrlInput.value = url;
bpmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when both BPM and ECM url inserted are wrong', (done) => {
const url = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.bpmHost.hasError('pattern')).toBeTruthy();
done();
});
bpmUrlInput.value = url;
ecmUrlInput.value = url;
bpmUrlInput.dispatchEvent(new Event('input'));
});
const ecmUrlInput = fixture.nativeElement.querySelector('#ecmHost');
const bpmUrlInput = fixture.nativeElement.querySelector('#bpmHost');
expect(bpmUrlInput).toEqual(null);
expect(ecmUrlInput).toBeDefined();
});
describe('OAUTH ', () => {
let ecmUrlInput;
let bpmUrlInput;
let oauthHostUrlInput;
let clientIdInput;
beforeEach(() => {
userPreferences.providers = 'OAUTH';
userPreferences.oauthConfig = {
host: 'http://localhost:6543',
redirectUri: '/',
silentLogin: false,
implicitFlow: true,
clientId: 'activiti',
scope: 'openid',
secret: ''
};
fixture.detectChanges();
bpmUrlInput = element.querySelector('#bpmHost');
oauthHostUrlInput = element.querySelector('#oauthHost');
clientIdInput = element.querySelector('#clientId');
});
afterEach(() => {
fixture.destroy();
});
it('should have a valid form when the BPM is correct', (done) => {
const urlBpm = 'http://localhost:9999/bpm';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('VALID');
done();
});
bpmUrlInput.value = urlBpm;
bpmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when the url inserted is wrong', (done) => {
const url = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.bpmHost.hasError('pattern')).toBeTruthy();
done();
});
bpmUrlInput.value = url;
bpmUrlInput.dispatchEvent(new Event('input'));
});
it('should have an invalid form when the host is wrong', (done) => {
const hostUrl = 'wrong';
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.host.hasError('pattern')).toBeTruthy();
done();
});
oauthHostUrlInput.value = hostUrl;
oauthHostUrlInput.dispatchEvent(new Event('input'));
});
it('should have a required clientId an invalid form when the clientId is missing', (done) => {
component.form.statusChanges.subscribe((status: string) => {
expect(status).toEqual('INVALID');
expect(component.clientId.hasError('required')).toBeTruthy();
done();
});
clientIdInput.value = '';
clientIdInput.dispatchEvent(new Event('input'));
});
});
});

View File

@@ -15,12 +15,9 @@
* limitations under the License.
*/
import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { LogService } from '../services/log.service';
import { SettingsService } from '../services/settings.service';
import { StorageService } from '../services/storage.service';
import { TranslationService } from '../services/translation.service';
import { Component, EventEmitter, Output, ViewEncapsulation, OnInit } from '@angular/core';
import { Validators, FormGroup, FormBuilder, AbstractControl } from '@angular/forms';
import { UserPreferencesService } from '../services';
@Component({
selector: 'adf-host-settings',
@@ -31,20 +28,18 @@ import { TranslationService } from '../services/translation.service';
styleUrls: ['host-settings.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class HostSettingsComponent {
export class HostSettingsComponent implements OnInit {
HOST_REGEX: string = '^(http|https):\/\/.*[^/]$';
ecmHost: string;
ecmHostTmp: string;
bpmHost: string;
bpmHostTmp: string;
urlFormControlEcm = new FormControl('', [Validators.required, Validators.pattern(this.HOST_REGEX)]);
urlFormControlBpm = new FormControl('', [Validators.required, Validators.pattern(this.HOST_REGEX)]);
providersValues = [
{ title: 'ECM and BPM', value: 'ALL' },
{ title: 'BPM', value: 'BPM' },
{ title: 'ECM', value: 'ECM' },
{ title: 'OAUTH', value: 'OAUTH' }
];
/** Determines which configurations are shown. Possible valid values are "ECM", "BPM" or "ALL". */
@Input()
providers: string = 'ALL';
form: FormGroup;
/** Emitted when the URL is invalid. */
@Output()
@@ -54,56 +49,136 @@ export class HostSettingsComponent {
@Output()
ecmHostChange = new EventEmitter<string>();
@Output()
cancel = new EventEmitter<boolean>();
@Output()
success = new EventEmitter<boolean>();
/** Emitted when the bpm host URL is changed. */
@Output()
bpmHostChange = new EventEmitter<string>();
constructor(private settingsService: SettingsService,
private storage: StorageService,
private logService: LogService,
private translationService: TranslationService) {
this.ecmHostTmp = this.ecmHost = storage.getItem('ecmHost') || this.settingsService.ecmHost;
this.bpmHostTmp = this.bpmHost = storage.getItem('bpmHost') || this.settingsService.bpmHost;
constructor(private fb: FormBuilder,
private userPreference: UserPreferencesService) {
}
public onChangeECMHost(event: any): void {
let value = (<HTMLInputElement> event.target).value.trim();
if (value && this.isValidUrl(value)) {
this.logService.info(`ECM host: ${value}`);
this.ecmHostTmp = value;
this.ecmHostChange.emit(value);
} else {
this.translationService.get('CORE.HOST_SETTING.CS_URL_ERROR').subscribe((message) => {
this.error.emit(message);
ngOnInit() {
let providerSelected = this.userPreference.providers;
this.form = this.fb.group({
providers: [providerSelected, Validators.required],
ecmHost: [this.userPreference.ecmHost, [Validators.required, Validators.pattern(this.HOST_REGEX)]],
bpmHost: [this.userPreference.bpmHost, [Validators.required, Validators.pattern(this.HOST_REGEX)]]
});
const oAuthConfig = this.userPreference.oauthConfig;
if (oAuthConfig) {
const oauthGroup = this.fb.group( {
host: [oAuthConfig.host, [Validators.required, Validators.pattern(this.HOST_REGEX)]],
clientId: [oAuthConfig.clientId, Validators.required],
redirectUri: [oAuthConfig.redirectUri, Validators.required],
scope: [oAuthConfig.scope, Validators.required],
secret: oAuthConfig.secret,
silentLogin: oAuthConfig.silentLogin,
implicitFlow: oAuthConfig.implicitFlow
});
this.form.addControl('oauthConfig', oauthGroup);
}
}
public onChangeBPMHost(event: any): void {
let value = (<HTMLInputElement> event.target).value.trim();
if (value && this.isValidUrl(value)) {
this.logService.info(`BPM host: ${value}`);
this.bpmHostTmp = value;
this.bpmHostChange.emit(value);
} else {
this.translationService.get('CORE.HOST_SETTING.PS_URL_ERROR').subscribe((message) => {
this.error.emit(message);
});
}
onCancel() {
this.cancel.emit(true);
}
public save(event: KeyboardEvent): void {
if (this.bpmHost !== this.bpmHostTmp) {
this.storage.setItem(`bpmHost`, this.bpmHostTmp);
onSubmit(values: any) {
this.userPreference.providers = values.providers;
if (this.isBPM()) {
this.saveBPMValues(values);
} else if (this.isECM()) {
this.saveECMValues(values);
} else if (this.isALL()) {
this.saveECMValues(values);
this.saveBPMValues(values);
} else if (this.isOAUTH()) {
this.saveOAuthValues(values);
}
if (this.ecmHost !== this.ecmHostTmp) {
this.storage.setItem(`ecmHost`, this.ecmHostTmp);
}
window.location.href = '/';
this.success.emit(true);
}
isValidUrl(url: string) {
return /^(http|https):\/\/.*/.test(url);
saveOAuthValues(values: any) {
this.userPreference.oauthConfig = values.oauthConfig;
this.userPreference.bpmHost = values.bpmHost;
}
saveBPMValues(values: any) {
this.userPreference.bpmHost = values.bpmHost;
}
saveECMValues(values: any) {
this.userPreference.ecmHost = values.ecmHost;
}
isBPM(): boolean {
return this.providers.value === 'BPM';
}
isECM(): boolean {
return this.providers.value === 'ECM';
}
isALL(): boolean {
return this.providers.value === 'ALL';
}
isOAUTH(): boolean {
return this.providers.value === 'OAUTH';
}
get providers(): AbstractControl {
return this.form.get('providers');
}
get bpmHost(): AbstractControl {
return this.form.get('bpmHost');
}
get ecmHost(): AbstractControl {
return this.form.get('ecmHost');
}
get host(): AbstractControl {
return this.oauthConfig.get('host');
}
get clientId(): AbstractControl {
return this.oauthConfig.get('clientId');
}
get scope(): AbstractControl {
return this.oauthConfig.get('scope');
}
get secretId(): AbstractControl {
return this.oauthConfig.get('secretId');
}
get implicitFlow(): AbstractControl {
return this.oauthConfig.get('implicitFlow');
}
get silentLogin(): AbstractControl {
return this.oauthConfig.get('silentLogin');
}
get redirectUri(): AbstractControl {
return this.oauthConfig.get('redirectUri');
}
get oauthConfig(): AbstractControl {
return this.form.get('oauthConfig');
}
}

View File

@@ -36,6 +36,8 @@ export const setupTestBed = (moduleDef: TestModuleMetadata) => {
preventAngularFromResetting();
TestBed.configureTestingModule(moduleDef);
await TestBed.compileComponents();
localStorage.clear();
sessionStorage.clear();
// prevent Angular from resetting testing module
TestBed.resetTestingModule = () => TestBed;