diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ca9b96403403231ff452d8fd94b9fe9207c104be..525ea473ea9619994d2809f548bcee4767e6ff8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ stages: - test - sonar - build + - mend test: stage: test @@ -64,4 +65,23 @@ build_and_push_release_image: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD $DOCKER_REPOSITORY docker build -t $DOCKER_REPOSITORY_LOCAL:$IMAGE_TAG . docker push $DOCKER_REPOSITORY_LOCAL:$IMAGE_TAG - docker logout $DOCKER_REPOSITORY \ No newline at end of file + docker logout $DOCKER_REPOSITORY + +mend: + stage: mend + image: openjdk:17-jdk-slim + only: + - /^release/ + variables: + PRODUCT_NAME: "nmaas" + PROJECT_NAME: "nmaas-portal" + script: + - | + export PRODUCT_VERSION=$(echo $CI_COMMIT_BRANCH | cut -c 9-) + export PROJECT_VERSION=$PRODUCT_VERSION + apt-get update && apt-get install -y curl nodejs npm + npm install -g @angular/cli + npm ci + chmod +x ./gradlew + curl -LJO https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar + java -jar wss-unified-agent.jar -userKey ${MEND_USER_KEY} -apiKey ${MEND_API_KEY} -projectVersion ${PROJECT_VERSION} -project ${PROJECT_NAME} -productVersion ${PRODUCT_VERSION} -product ${PRODUCT_NAME} -c ./ws/ws.config -d ./ \ No newline at end of file diff --git a/build.gradle b/build.gradle index a443e5382e46befdc5870955a7650591f7e79e96..508d65aa095ac10dfd5fe282e30b242f8b5820a8 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id "org.sonarqube" version "3.2.0" } -version = '1.7.0-SNAPSHOT' +version = '1.7.0' task buildGUI(type: Exec) { println 'Building using Angular CLI' diff --git a/package-lock.json b/package-lock.json index 881b777c79885b41a9fc397b3d9c20efa1751599..2a4d17934a1608ed75f4f025a032daf43a966821 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "license": "Apache 2.0", "dependencies": { "@angular/animations": "17.3.12", diff --git a/package.json b/package.json index 9402af9537167709179f216f116de9a9ef4cbefa..8f55d7ac4f42805f54c1b02aa107d713e937060b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nmaas-portal", - "version": "1.7.0", + "version": "1.7.1", "license": "Apache 2.0", "angular-cli": {}, "scripts": { diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 610c10ccf5c6a61d2610e4be2b3fdefe9e51fcde..31a968fec3b241c1d54e3257898a92a6e781fe71 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -6,6 +6,7 @@ import { WelcomeRoutes } from './welcome/welcome.routes'; import {ServiceUnavailableRoutes} from './service-unavailable/service-unavailable.routes'; import {PageNotFoundComponent} from './shared/page-not-found/page-not-found.component'; import {LoginSuccessComponent} from './auth/login-success/login-success.component'; +import {LinkAccountComponent} from './welcome/link-account/link-account.component'; const appRoutes: Routes = [ ...WelcomeRoutes, @@ -13,6 +14,7 @@ const appRoutes: Routes = [ ...ServiceUnavailableRoutes, { path: 'notfound', component: PageNotFoundComponent }, { path: 'login-success', component: LoginSuccessComponent }, + { path: 'login-linking', component: LinkAccountComponent}, { path: '**', redirectTo: '/welcome' }, ]; diff --git a/src/app/appmarket/appdetails/appdetails.component.ts b/src/app/appmarket/appdetails/appdetails.component.ts index e5518794a0850eb436fcafbe67025c1e78c33d75..cb156314c0447fa5f6e4530551266420d105368f 100644 --- a/src/app/appmarket/appdetails/appdetails.component.ts +++ b/src/app/appmarket/appdetails/appdetails.component.ts @@ -146,7 +146,8 @@ export class AppDetailsComponent implements OnInit { } return this.authService.hasRole(Role[Role.ROLE_SYSTEM_ADMIN]) - || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]); + || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_DOMAIN_ADMIN]) + || this.authService.hasDomainRole(this.domainId, Role[Role.ROLE_GROUP_DOMAIN_ADMIN]); } public isApplicationEnabledInDomain(): boolean { diff --git a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts index 449459e4f4b881d437f763ae6433b6b45ad4b7b7..3634c03e89d9f7d5b1a1da649ce0908e432ff86b 100644 --- a/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts +++ b/src/app/appmarket/appinstance/appinstance/appinstance.component.spec.ts @@ -186,6 +186,7 @@ describe('Component: AppInstance', () => { createdAt: new Date(), descriptiveDeploymentId: 'test-oxidized-48', domainId: 4, + domainName: "Test Domain", id: 1, internalId: 'eccbaf70-7fdd-401a-bb3e-b8659bcfbdff', name: 'oxi-virt-1', diff --git a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html index 88caaeecc4615f695012b5416a481ddead7964af..d97bd6f0962466ec5d1382e6f37d344ceb78cd61 100644 --- a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html +++ b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.html @@ -78,7 +78,7 @@ <td class="col-lg-1 col-md-1">{{appInstance?.applicationVersion}}</td> <td class="col-lg-2 col-md-2" *ngIf="domainId === undefined || domainId === domainService.getGlobalDomainId()"> - {{getDomainNameById(appInstance?.domainId)}} + {{appInstance?.domainName}} </td> <td class="col-lg-1 col-md-1">{{appInstance?.owner?.username}}</td> <td class="col-lg-2 col-md-2">{{appInstance?.createdAt | localDate:'dd-MM-yyyy HH:mm'}}</td> @@ -128,7 +128,7 @@ <td class="col-lg-2 col-md-2">{{appInstance?.applicationName}}</td> <td class="col-lg-1 col-md-1" *ngIf="domainId === undefined || domainId === domainService.getGlobalDomainId()"> - {{getDomainNameById(appInstance?.domainId)}}</td> + {{appInstance?.domainName}}</td> <td class="col-lg-1 col-md-1">{{appInstance?.owner?.username}}</td> <td class="col-lg-2 col-md-2">{{appInstance?.createdAt | localDate:'dd-MM-yyyy HH:mm'}}</td> <td class="col-lg-3 col-md-3">{{ translateState(appInstance?.state) }}</td> diff --git a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts index 1a95ba43bb1153d842a4ccd17a8969143f2dbee5..b02be4cc73d46546d2600be1885040d0bbc19528 100644 --- a/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts +++ b/src/app/appmarket/appinstance/appinstancelist/appinstancelist.component.ts @@ -52,8 +52,6 @@ export class AppInstanceListComponent implements OnInit { public selectedUsername: string; public domainId = 0; - public domains: Domain[] = []; - public searchValue = ''; public selectionOptions = [ { label: this.translateEnum(AppInstanceListSelection.ALL), value: AppInstanceListSelection.ALL }, @@ -73,9 +71,6 @@ export class AppInstanceListComponent implements OnInit { ngOnInit() { this.sessionService.registerCulture(this.translateService.currentLang); - this.domainService.getAll().subscribe(result => { - this.domains.push(...result); - }); const i = sessionStorage.getItem(this.item_number_key); if (i) { this.maxItemsOnPage = +i; @@ -110,12 +105,6 @@ export class AppInstanceListComponent implements OnInit { } - public getDomainNameById(id: number): string { - if (this.domains === undefined) { - return 'none'; - } - return this.domains.find(value => value.id === id).name; - } public translateEnum(value: AppInstanceListSelection): string { let outValue = ''; diff --git a/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts b/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts index 4a613f1394d03e28924f20c069a4f8c3e716cf28..0e2900d4ff0f8e9c11652b992abff6e0be6964e8 100644 --- a/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts +++ b/src/app/appmarket/appinstance/modals/add-members-modal/add-members-modal.component.spec.ts @@ -32,6 +32,7 @@ describe('AddMembersModalComponent', () => { createdAt: new Date(), descriptiveDeploymentId: 'test-oxidized-48', domainId: 4, + domainName: "Test Domain", id: 1, internalId: 'eccbaf70-7fdd-401a-bb3e-b8659bcfbdff', name: 'oxi-virt-1', diff --git a/src/app/appmarket/domains/domain/domain.component.html b/src/app/appmarket/domains/domain/domain.component.html index 00208f2f896690b530f2229310fa7dd3b6c375dc..c9c32db1e1e83a214897894a8ebca1958fc6dacb 100644 --- a/src/app/appmarket/domains/domain/domain.component.html +++ b/src/app/appmarket/domains/domain/domain.component.html @@ -260,6 +260,11 @@ <div class="flex justify-content-end"> <button *ngIf="!isInMode(ComponentMode.VIEW)" type="submit" class="btn btn-primary" [disabled]="!domainForm.form.valid">{{ 'DOMAIN_DETAILS.SUBMIT_BUTTON' | translate }}</button> </div> + + <br *ngIf="errorMessage"> + <div class="alert alert-danger text-left" *ngIf="errorMessage"> + {{errorMessage}} + </div> </form> </div> diff --git a/src/app/appmarket/domains/domain/domain.component.ts b/src/app/appmarket/domains/domain/domain.component.ts index eecd96f7840b8cacb482cbfaa219fc6cab1cdf15..d74fcedd3bb6e0390478aba4961df539decde186 100644 --- a/src/app/appmarket/domains/domain/domain.component.ts +++ b/src/app/appmarket/domains/domain/domain.component.ts @@ -9,7 +9,7 @@ import {User} from '../../../model'; import {Observable, of} from 'rxjs'; import {UserRole} from '../../../model/userrole'; import {AuthService} from '../../../auth/auth.service'; -import {ModalComponent} from '../../../shared'; +import {ModalComponent} from '../../../shared'; import {map, shareReplay, take} from 'rxjs/operators'; import {DcnDeploymentType} from '../../../model/dcndeploymenttype'; import {CustomerNetwork} from '../../../model/customernetwork'; @@ -48,6 +48,8 @@ export class DomainComponent extends BaseComponent implements OnInit { public annotations : Observable<DomainAnnotation[]> = of([]); + public errorMessage = ""; + constructor(public domainService: DomainService, protected userService: UserService, private router: Router, @@ -103,7 +105,14 @@ export class DomainComponent extends BaseComponent implements OnInit { if (this.domainId !== undefined) { this.updateExistingDomain(); } else { - this.domainService.add(this.domain).subscribe(() => this.router.navigate(['admin/domains/'])); + this.domainService.add(this.domain).subscribe(() => { + this.router.navigate(['admin/domains/']) + }, err => { + console.error(err); + if(err.statusCode !== 409 && err?.message !== undefined) this.errorMessage = err.message; + else this.errorMessage = err; + + }); } this.domainService.setUpdateRequiredFlag(true); } diff --git a/src/app/auth/auth.service.spec.ts b/src/app/auth/auth.service.spec.ts index e9eef99417406df03337405ecd381adfb6f5a3c7..170ece28132bc78a75fd54ad8338c48914b82ea4 100644 --- a/src/app/auth/auth.service.spec.ts +++ b/src/app/auth/auth.service.spec.ts @@ -3,19 +3,18 @@ import {TestBed, waitForAsync} from '@angular/core/testing'; import {AuthService} from './auth.service'; import {AppConfigService, ConfigurationService} from '../service'; import {JwtHelperService} from '@auth0/angular-jwt'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import {Role, UserRole} from '../model/userrole'; import {ProfileService} from '../service/profile.service'; import {Observable, of} from 'rxjs'; -import { Configuration } from '../model/configuration'; -import { HttpHandler } from '@angular/common/http'; +import {Configuration} from '../model/configuration'; describe('Service: Auth', () => { let authService: AuthService; let appConfigServiceSpy: jasmine.SpyObj<AppConfigService>; let jwtHelperServiceSpy: jasmine.SpyObj<JwtHelperService>; let maintenanceServiceSpy: jasmine.SpyObj<ConfigurationService>; - + let httpMock: HttpTestingController; let store: any = {}; beforeEach(waitForAsync(() => { @@ -23,7 +22,8 @@ describe('Service: Auth', () => { config: { apiUrl: 'http://api.url', tokenName: 'token', - } + }, + getTestInstanceModalKey: () => 'testModalKey' }; const jwtSpy = jasmine.createSpyObj('JwtHelperService', ['decodeToken', 'isTokenExpired']); jwtSpy.decodeToken.and.returnValue({ @@ -41,19 +41,19 @@ describe('Service: Auth', () => { class MockConfigurationService { protected uri: string; - + constructor() { this.uri = 'http://localhost/api'; } - + public getApiUrl(): string { return 'http://localhost/api'; } - + public getConfiguration(): Observable<Configuration> { return of<Configuration>(); } - + public updateConfiguration(configuration: Configuration): Observable<any> { return of<Configuration>(); } @@ -84,6 +84,7 @@ describe('Service: Auth', () => { ], }); + httpMock = TestBed.inject(HttpTestingController) authService = TestBed.get(AuthService); authService.profile = [userRole, userRole2] appConfigServiceSpy = TestBed.get(AppConfigService); @@ -104,6 +105,10 @@ describe('Service: Auth', () => { }); })); + afterEach(() => { + httpMock.verify(); + store = {}; + }); it('should create service', () => { expect(authService).toBeTruthy(); @@ -180,8 +185,11 @@ describe('Service: Auth', () => { }); it('should remove token on logout', () => { + store['oidc-token'] = 'some-oidc-token'; authService.logout(); expect(store['token']).not.toBeDefined(); + const req = httpMock.expectOne('http://api.url/oidc/logout/some-oidc-token'); + req.flush({}); }); it('should be logged in when token is present and valid', () => { @@ -197,4 +205,58 @@ describe('Service: Auth', () => { expect(r).toEqual(false); }); + it('should store token and oidc token in localStorage', () => { + authService.storeToken('abc123'); + expect(store['token']).toEqual('abc123'); + + authService.storeOidcToken('oidc456'); + expect(store['oidc-token']).toEqual('oidc456'); + }); + + it('should remove roles from localStorage', () => { + store['rolesToken'] = 'some_roles'; + authService.removeRoles(); + expect(store['rolesToken']).toBeUndefined(); + }); + + it('should load and parse roles from localStorage', () => { + const roles = [{domainId: 1, role: Role.ROLE_USER, domainName: 'x'}]; + store['rolesToken'] = JSON.stringify(roles); + + const result = authService.loadRoles(); + expect(result.length).toEqual(1); + expect(result[0].role).toEqual(Role.ROLE_USER); + }); + + it('should assign loaded roles to profile', () => { + const roles = [{domainId: 2, role: Role.ROLE_DOMAIN_ADMIN, domainName: 'x'}]; + store['rolesToken'] = JSON.stringify(roles); + authService.loadAndSaveRoles(); + expect(authService.profile[0].role).toEqual(Role.ROLE_DOMAIN_ADMIN); + }); + it('should stringify and store roles', () => { + const roles = [new UserRole()]; + roles[0].domainId = 1; + roles[0].role = Role.ROLE_USER; + roles[0].domainName = 'dom1'; + + authService.storeRoles(roles); + expect(store['rolesToken']).toContain('ROLE_USER'); + }); + it('should get global role from token', () => { + const result = authService.getGlobalRole(); + expect(result).toContain('ROLE_SYSTEM_ADMIN'); + }); + + it('should handle login error with catchError', waitForAsync(() => { + authService.login('user', 'pass').subscribe({ + next: () => fail('Expected error'), + error: (err) => { + expect(err.status).toEqual(401); + } + }); + + const req = httpMock.expectOne('http://api.url/auth/basic/login'); + req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' }); + })); }); diff --git a/src/app/auth/auth.service.ts b/src/app/auth/auth.service.ts index a3426c27b2b23a95efaec88574ee9c9078b05f85..6156c8e479cfd788ed7808556017f30cb57bc40f 100644 --- a/src/app/auth/auth.service.ts +++ b/src/app/auth/auth.service.ts @@ -1,13 +1,11 @@ -import {BehaviorSubject, Observable, Subject, throwError as observableThrowError, of} from 'rxjs'; +import {BehaviorSubject, Observable, of, Subject, throwError as observableThrowError} from 'rxjs'; import {catchError, debounceTime, map} from 'rxjs/operators'; import {Injectable} from '@angular/core'; import {AppConfigService, ConfigurationService} from '../service'; import {JwtHelperService} from '@auth0/angular-jwt'; import {HttpClient, HttpHeaders} from '@angular/common/http'; -import {User} from '../model'; import {ProfileService} from '../service/profile.service'; import {Role, UserRole} from '../model/userrole'; -import {interval, Subscription} from 'rxjs'; export class DomainRoles { @@ -17,10 +15,6 @@ export class DomainRoles { ) { } - public getDomainId(): number { - return this.domainId; - } - public getRoles(): string[] { return this.roles; } @@ -183,15 +177,6 @@ export class AuthService { return this.jwtHelper.decodeToken(token).global_role; } - public getDomainsRoles() { - const token = this.getToken(); - if (token == null) { - return null; - } - return this.jwtHelper.decodeToken(token).roles; - - } - public getDomainRoles(): Map<number, DomainRoles> { const domainRolesMap: Map<number, DomainRoles> = new Map<number, DomainRoles>(); @@ -253,6 +238,49 @@ export class AuthService { return domainsWithRole; } + public oidcLinkingLogin(oidcToken: string, + email: string, + password: string, + uuid: string, + firstName: string, + lastName: string) { + const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'}); + + return this.http.post(this.appConfig.config.apiUrl + '/oidc/link', + JSON.stringify( + { + 'oidcToken': oidcToken, + 'email': email, + 'password': password, + 'uuid': uuid, + 'firstName': firstName, + 'lastName': lastName, + } + ), + {headers: headers}).pipe( + debounceTime(1000), + map((res: Response) => { + const token = res && res['token']; + const oidcToken = res && res['oidcToken']; + if (token && oidcToken) { + this.storeToken(token); + this.storeOidcToken(oidcToken); + this.loginUsingSsoService = false; + this.isLoggedInSubject.next(true); + this.profileService.getRoles().subscribe(profile => { + this.profile = profile + this.storeRoles(profile); + return true; + }) + } else { + this.isLoggedInSubject.next(false); + return false; + } + } + ), + ) + } + public login(username: string, password: string): Observable<boolean> { // hack so test instance modal is shown onl after login localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True'); @@ -306,49 +334,6 @@ export class AuthService { })); } - public propagateSSOLogin(userid: string): Observable<boolean> { - console.debug('propagateSSOLogin'); - console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl); - console.debug('propagateSSOLogin ' + this.appConfig.config.apiUrl + '/auth/sso/login'); - console.debug('propagateSSOLogin ' + userid); - // hack so test instance modal is shown onl after login - localStorage.setItem(this.appConfig.getTestInstanceModalKey(), 'True'); - - if (this.maintenance) { - this.isLoggedInSubject.next(false); - return of(false); - } - - const headers = new HttpHeaders({'Content-Type': 'application/json', 'Accept': 'application/json'}); - return this.http.post(this.appConfig.config.apiUrl + '/auth/sso/login', - JSON.stringify({'userid': userid}), {headers: headers}).pipe( - debounceTime(10000), - map((response: Response) => { - console.debug('SSO login response: ' + response); - // login successful if there's a jwt token in the response - const token = response && response['token']; - - if (token) { - this.storeToken(token); - console.debug('SSO AUTH | User: ' + this.getUsername()); - console.debug('SSO AUTH | Domains: ' + this.getDomains()); - console.debug('SSO AUTH | Roles: ' + this.getRoles()); - console.debug('SSO AUTH | DomainRoles: ' + this.getDomainRoles()); - this.loginUsingSsoService = true; - this.isLoggedInSubject.next(true); - return true; - } else { - // return false to indicate failed login - this.isLoggedInSubject.next(false); - return false; - } - }), - catchError((error) => { - console.error('SSO login error: ' + error.error['message']); - return observableThrowError(error); - })); - } - public logout(): void { const oidcToken = this.getOidcToken(); this.refresh = null; @@ -366,6 +351,11 @@ export class AuthService { } } + public oidcLogout(oidcToken: string): void { + this.http.get(this.appConfig.config.apiUrl + '/oidc/logout/' + oidcToken).subscribe(() => { + }) + } + public isLogged(): boolean { const token = this.getToken(); if (token == null) { @@ -374,11 +364,6 @@ export class AuthService { return (token ? !this.jwtHelper.isTokenExpired(token) : false); } - get isLoggedIn$(): Observable<boolean> { - return this.isLoggedInSubject.pipe( - debounceTime(100), // use debounceTime to aggregate multiple emissions https://rxjs.dev/api/operators/debounceTime - ); - } public getDomainIds(): number[] { return Array.from(new Set(this.profile.map(ur => ur.domainId))); diff --git a/src/app/auth/login-success/login-success.component.ts b/src/app/auth/login-success/login-success.component.ts index e7fec74d4dcaa6d40ec0da867a432314d09eb307..4d5444dc694194d95293604a690ed49bc8ff5611 100644 --- a/src/app/auth/login-success/login-success.component.ts +++ b/src/app/auth/login-success/login-success.component.ts @@ -25,6 +25,7 @@ export class LoginSuccessComponent implements OnInit { if (refreshToken) { this.authService.storeOidcToken(oidcToken); } + this.authService.loadUser(); this.router.navigate(['/']) }) diff --git a/src/app/directive/roles-exluded.directive.ts b/src/app/directive/roles-exluded.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b003ab52b1403e88a538ac016bc9e7cdde22636 --- /dev/null +++ b/src/app/directive/roles-exluded.directive.ts @@ -0,0 +1,30 @@ +import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { AuthService } from '../auth/auth.service'; + +@Directive({ + selector: '[rolesExcluded]' +}) +export class RolesExcludedDirective { + private _excluded: Array<string> = []; + + constructor( + private _templateRef: TemplateRef<any>, + private _viewContainer: ViewContainerRef, + private authService: AuthService + ) {} + + @Input() set rolesExcluded(excludedRoles: Array<string>) { + this._excluded = excludedRoles; + this.updateState(); + } + + private updateState() { + this._viewContainer.clear(); + + const hasExcludedRole = this._excluded.some(role => this.authService.hasRole(role)); + if (!hasExcludedRole) { + // If user has exluded role hide the element + this._viewContainer.createEmbeddedView(this._templateRef); + } + } +} \ No newline at end of file diff --git a/src/app/directive/roles.directive.ts b/src/app/directive/roles.directive.ts index 68f2e12346693fe23de1f14c4f90b100cfd8b095..c2155e3c2a02438a75fd56ae119b5a12c910b3d8 100644 --- a/src/app/directive/roles.directive.ts +++ b/src/app/directive/roles.directive.ts @@ -3,10 +3,7 @@ import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core'; class RoleState { public allowed: Array<string> = new Array<string>(); - public excluded: Array<string> = new Array<string>() } - - @Directive({ selector: '[roles]', inputs: ['roles'] @@ -15,8 +12,6 @@ export class RolesDirective { private _allowed: Array<string> = new Array<string>(); - private _excluded: Array<string> = new Array<string>(); - constructor(private _templateRef: TemplateRef<any>, private _viewContainer: ViewContainerRef, private authService: AuthService) { @@ -26,53 +21,18 @@ export class RolesDirective { @Input() set roles(allowedRoles: Array<string>) { this._allowed = allowedRoles; this.updateState({ - allowed: this._allowed, - excluded: this._excluded - }) - } - - // Excluded roles have priority than allowed roles - // If user have excluded role template would not be shown - - @Input() set rolesExcluded(excluded: Array<string>) { - this._excluded = excluded; - this.updateState({ - allowed: this._allowed, - excluded: this._excluded + allowed: this._allowed }) } updateState(state: RoleState) { this._viewContainer.clear(); - - let show: boolean = false; - let notAllowed: boolean = false; - - const allowedRoles = state.allowed; - - for (let exclude of state.excluded) { - if (this.authService.hasRole(exclude)) { - notAllowed = true; - break; - } + + + const hasAllowedRole = state.allowed.some(role => this.authService.hasRole(role)); + if (hasAllowedRole) { + this._viewContainer.createEmbeddedView(this._templateRef); } - if (notAllowed) { - this._viewContainer.clear(); - } else { - for (let allowedRole of allowedRoles) { - if (this.authService.hasRole(allowedRole)) { - show = true; - break; - } - } - - if (show) { - this._viewContainer.createEmbeddedView(this._templateRef); - } else { - this._viewContainer.clear(); - } - } - } } diff --git a/src/app/model/app-instance.ts b/src/app/model/app-instance.ts index 76edca2f6b68caf22d2f7702e9a2eb6f06d5c237..27119dad519db59323259ae6544037dc14fcd478 100644 --- a/src/app/model/app-instance.ts +++ b/src/app/model/app-instance.ts @@ -28,6 +28,7 @@ export class AppInstance { public id: number = undefined; public domainId: number = undefined; + public domainName: string = undefined; public applicationId: number = undefined; public applicationName: string = undefined; public applicationVersion: string = undefined; diff --git a/src/app/shared/navbar/navbar.component.html b/src/app/shared/navbar/navbar.component.html index cead22d4afd206aea61c47f15b9c44bff96b8767..11b5cb2ef50e26662dfd5e78efe942134349321a 100644 --- a/src/app/shared/navbar/navbar.component.html +++ b/src/app/shared/navbar/navbar.component.html @@ -77,9 +77,13 @@ <li *roles="['ROLE_SYSTEM_ADMIN']"><a [routerLink]="['/admin/users']">{{ 'NAVBAR.USERS' | translate }}</a> </li> - <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_DOMAIN_ADMIN']"><a - [routerLink]="['/domain/users']">{{ 'NAVBAR.DOMAIN_USERS' | translate }}</a> + + <li *roles="['ROLE_DOMAIN_ADMIN', 'ROLE_GROUP_DOMAIN_ADMIN']"> + <a *rolesExcluded="['ROLE_SYSTEM_ADMIN']" + [routerLink]="['/domain/users']">{{ 'NAVBAR.DOMAIN_USERS' | translate }}</a> + </li> + <li *roles="['ROLE_SYSTEM_ADMIN']"><a [routerLink]="['/admin/languages']">{{ 'NAVBAR.LANGUAGES' | translate }}</a> </li> diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index a6243e0198db0195b3b444eb4e99116ce403afeb..ae6442758d5d7eed07600cb1ddacc71b8535d573 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -1,6 +1,6 @@ import {DefaultLogo} from '../directive/defaultlogo.directive'; import {RolesDirective} from '../directive/roles.directive'; -import {NgModule} from '@angular/core'; +import {NgModule, NO_ERRORS_SCHEMA} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {CommonModule, DatePipe} from '@angular/common'; @@ -65,6 +65,7 @@ import { InputGroupModule } from 'primeng/inputgroup'; import { InputGroupAddonModule } from 'primeng/inputgroupaddon'; import { ButtonModule } from 'primeng/button'; import { BrowserModule } from '@angular/platform-browser'; +import { RolesExcludedDirective } from '../directive/roles-exluded.directive'; @NgModule({ @@ -103,6 +104,7 @@ import { BrowserModule } from '@angular/platform-browser'; NavbarComponent, DefaultLogo, RolesDirective, + RolesExcludedDirective, MinLengthDirective, MaxLengthDirective, SearchComponent, @@ -179,13 +181,16 @@ import { BrowserModule } from '@angular/platform-browser'; ModalTestInstanceComponent, ModalNotificationSendComponent, DomainRolesDirective, + RolesExcludedDirective, SshKeysComponent, ModalProvideSshKeyComponent, PreferencesComponent, SortableHeaderDirective, DomainNamespaceAnnotationsComponent, AccessTokensComponent - ] + ], + schemas: [NO_ERRORS_SCHEMA], // Dodanie schematu + }) export class SharedModule { } diff --git a/src/app/shared/users/access-token/access-tokens.component.html b/src/app/shared/users/access-token/access-tokens.component.html index 9800061a0e11a4ba58e277a6b890c392c71b1b15..6ad21e0418c541ca58a6a394c32520b19778483a 100644 --- a/src/app/shared/users/access-token/access-tokens.component.html +++ b/src/app/shared/users/access-token/access-tokens.component.html @@ -6,22 +6,20 @@ <tr> <th scope="col">{{'TOKENS.TABLE.ID' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.NAME' | translate}}</th> - <th scope="col">{{'TOKENS.TABLE.VALUE' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.VALID' | translate}}</th> <th scope="col">{{'TOKENS.TABLE.ACTIONS' | translate}}</th> </tr> </thead> <tbody> <tr *ngFor="let token of tokensList"> - <td>{{token.id}}</td> - <td>{{token.name}}</td> - <td>{{token.tokenValue}}</td> - <td>{{token.valid}}</td> - <td *ngIf="token.valid"> + <td style="width: 25%;">{{token.id}}</td> + <td style="width: 25%;">{{token.name}}</td> + <td style="width: 25%;">{{token.valid}}</td> + <td style="width: 25%;" *ngIf="token.valid"> <button type="button" class="btn btn-danger" (click)="invalidate(token.id)">{{'TOKENS.BUTTON_INVALIDATE' | translate}}</button> </td> - <td *ngIf="!token.valid"> + <td style="width: 25%;" *ngIf="!token.valid"> <button type="button" class="btn btn-danger" (click)="deleteToken(token.id)">{{'TOKENS.BUTTON_DELETE' | translate}}</button> </td> diff --git a/src/app/shared/users/list/userslist.component.ts b/src/app/shared/users/list/userslist.component.ts index 7394575d57a4f6d527893ff57c82ad1d4d71b007..1ae3116b57b2d986b67eaaef9d6ca6a47f96d777 100644 --- a/src/app/shared/users/list/userslist.component.ts +++ b/src/app/shared/users/list/userslist.component.ts @@ -101,11 +101,20 @@ export class UsersListComponent extends BaseComponent implements OnInit, OnChang } public getAllDomain() { - this.domainService.getAll().subscribe(domains => { - domains.forEach(domain => { - this.domainCache.setData(domain.id, domain) + if(this.domainMode) { + this.domainService.getMyDomains().subscribe(domains => { + domains.forEach(domain => { + this.domainCache.setData(domain.id, domain) + } + ) }) + } else { + this.domainService.getAll().subscribe(domains => { + domains.forEach(domain => { + this.domainCache.setData(domain.id, domain) + }) }) - }) + } + } public getDomainName(domainId: number): Observable<string> { diff --git a/src/app/welcome/link-account/link-account.component.css b/src/app/welcome/link-account/link-account.component.css new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.css @@ -0,0 +1 @@ + diff --git a/src/app/welcome/link-account/link-account.component.html b/src/app/welcome/link-account/link-account.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2d75205238a0c5f83811c5aa3ebcb229b4d79d89 --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.html @@ -0,0 +1,51 @@ +<div style="display: flex; justify-content: center;"> + <div style=" +margin-top: 50px; + width: 60% +" class="panel panel-default"> + <div class="panel-heading">{{ 'ACCOUNT_LINKING.HEADER' | translate }}</div> + <div class="panel-body"> + <form *ngIf="user" + class="form-horizontal" #userDetailsForm="ngForm"> + <div> + <p> + {{ 'ACCOUNT_LINKING.INFO' | translate }} + </p> + </div> + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.FIRST_NAME' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.firstname }}</p> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.LAST_NAME' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.lastname }}</p> + </div> + </div> + + <div class="form-group"> + <label class="col-sm-2 control-label">{{ 'USER_DETAILS.EMAIL' | translate }}</label> + <div class="col-sm-10"> + <p class="form-control-static">{{ user.email }}</p> + </div> + </div> + + <div class="form-group"> + <label for="password" class="col-sm-2 control-label">{{ 'PASSWORD.PASSWORD' | translate }}</label> + <div class="col-sm-10"> + <input type="password" class="form-control" id="password" + name="password" [(ngModel)]="password"> + </div> + </div> + <button type="submit" class="btn btn-primary" + (click)="submit()">{{ 'ACCOUNT_LINKING.CONFIRM' | translate }} + </button> + <div *ngIf="error" class="alert alert-danger" style="margin-top: 20px">{{error}}</div> + </form> + <br> + </div> + </div> +</div> diff --git a/src/app/welcome/link-account/link-account.component.spec.ts b/src/app/welcome/link-account/link-account.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b0a9f98c9b851dbd8f044e900df4b02595ca2ca --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.spec.ts @@ -0,0 +1,57 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {LinkAccountComponent} from './link-account.component'; +import {ActivatedRoute} from '@angular/router'; +import {of} from 'rxjs'; +import {AuthService} from '../../auth/auth.service'; +import {TranslateFakeLoader, TranslateLoader, TranslateModule} from '@ngx-translate/core'; + +describe('LinkAccountComponent', () => { + let component: LinkAccountComponent; + let fixture: ComponentFixture<LinkAccountComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LinkAccountComponent], + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateFakeLoader + } + })], + providers: [ + { + provide: ActivatedRoute, + useValue: { + queryParams: of({ + oidc_token: 'mocked.jwt.token' + }), + snapshot: { + paramMap: { + get: () => null + } + } + } + }, + { + provide: AuthService, + useValue: { + isLogged: () => true, + oidcLogout: jasmine.createSpy('oidcLogout'), + oidcLinkingLogin: jasmine.createSpy('oidcLinkingLogin').and.returnValue(of({})) + } + }, + + ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LinkAccountComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/welcome/link-account/link-account.component.ts b/src/app/welcome/link-account/link-account.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb60e030f051c169a57b0f1a72394101cee6157d --- /dev/null +++ b/src/app/welcome/link-account/link-account.component.ts @@ -0,0 +1,84 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {User} from '../../model'; +import {ActivatedRoute, Router} from '@angular/router'; +import jwtDecode from 'jwt-decode'; +import {AuthService} from '../../auth/auth.service'; +import {TranslateService} from '@ngx-translate/core'; + + +@Component({ + selector: 'app-link-account', + templateUrl: './link-account.component.html', + styleUrl: './link-account.component.css' +}) +export class LinkAccountComponent implements OnInit, OnDestroy { + public user: User; + private token: string; + public password: string; + public error: string; + + constructor( + private readonly route: ActivatedRoute, + private readonly authService: AuthService, + private readonly router: Router, + private translate: TranslateService, + ) { + } + + ngOnDestroy() { + if (!this.authService.isLogged()) { + this.authService.oidcLogout(this.token) + } + } + + ngOnInit() { + this.route.queryParams.subscribe(param => { + this.token = param['oidc_token']; + const decoded: TokenPayload = jwtDecode<TokenPayload>(this.token); + this.user = new User(); + this.user.username = decoded.sub; + this.user.firstname = decoded.given_name; + this.user.lastname = decoded.family_name; + this.user.email = decoded.email; + }) + + } + + public submit(): void { + this.authService.oidcLinkingLogin( + this.token, + this.user.email, + this.password, + this.user.username, + this.user.firstname, + this.user.lastname, + ).subscribe( + () => { + this.router.navigate(['/']); + }, + err => { + this.error = this.translate.instant(this.getMessage(err)); + } + ) + } + private getMessage(err: any): string { + switch (err['status']) { + case 401: + return 'LOGIN.LOGIN_FAILURE_MESSAGE'; + case 406: + return 'LOGIN.APPLICATION_UNDER_MAINTENANCE_MESSAGE'; + case 409: + return 'GENERIC_MESSAGE.UNAVAILABLE_MESSAGE'; + default: + return 'GENERIC_MESSAGE.UNAVAILABLE_MESSAGE'; + } + } +} + + +interface TokenPayload { + sub: string; + email: string; + given_name: string; + family_name: string; +} diff --git a/src/app/welcome/login/login.component.ts b/src/app/welcome/login/login.component.ts index 62e1fc26688f7417969f4cd671b4a93317e49e2a..0a4481b66e12318164be2249a30b512a6dfcf0c9 100644 --- a/src/app/welcome/login/login.component.ts +++ b/src/app/welcome/login/login.component.ts @@ -64,8 +64,11 @@ export class LoginComponent implements OnInit { ); } + // only for use in linking accounts public triggerOIDC() { - window.location.href = this.appConfig.getOidcUrl(); + if (!this.configuration.maintenance) { + window.location.href = this.appConfig.getOidcUrl(); + } } public sendResetNotification() { diff --git a/src/app/welcome/welcome.module.ts b/src/app/welcome/welcome.module.ts index 08fc1c81e12392217f5c403431925659eea6afb8..e212c4f059d959724211aa423e911d38b087f067 100644 --- a/src/app/welcome/welcome.module.ts +++ b/src/app/welcome/welcome.module.ts @@ -20,38 +20,41 @@ import {TranslateModule} from '@ngx-translate/core'; import {PasswordResetComponent} from './passwordreset/password-reset.component'; import {PasswordStrengthMeterComponent} from 'angular-password-strength-meter'; import {PolicySubpageComponent} from './policy-subpage/policy-subpage.component'; +import {LinkAccountComponent} from './link-account/link-account.component'; @NgModule({ - declarations: [ - WelcomeComponent, - LoginComponent, - LogoutComponent, - RegistrationComponent, - ProfileComponent, - CompleteComponent, - TermsAcceptanceComponent, - PasswordResetComponent, - PolicySubpageComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - CommonModule, - RouterModule, - SharedModule, - PipesModule, - AppMarketModule, - PasswordStrengthMeterComponent, - TranslateModule.forChild() - ], - exports: [ - WelcomeComponent - ], - providers: [ - RegistrationService, - UserService, - ChangelogService, - ContentDisplayService - ] + declarations: [ + WelcomeComponent, + LoginComponent, + LogoutComponent, + RegistrationComponent, + ProfileComponent, + CompleteComponent, + TermsAcceptanceComponent, + PasswordResetComponent, + PolicySubpageComponent, + LinkAccountComponent + ], + imports: [ + FormsModule, + ReactiveFormsModule, + CommonModule, + RouterModule, + SharedModule, + PipesModule, + AppMarketModule, + PasswordStrengthMeterComponent, + TranslateModule.forChild() + ], + exports: [ + WelcomeComponent + ], + providers: [ + RegistrationService, + UserService, + ChangelogService, + ContentDisplayService + ] }) -export class WelcomeModule {} +export class WelcomeModule { +} diff --git a/ws/run_ws.sh b/ws/run_ws.sh deleted file mode 100644 index 8763a587feed5aac2fcb74b297ebaed9481b0859..0000000000000000000000000000000000000000 --- a/ws/run_ws.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -echo "Downloading WhiteSource agent..." -curl -LJO https://github.com/whitesource/unified-agent-distribution/releases/latest/download/wss-unified-agent.jar - -echo "Running WhiteSource scan..." -java -jar wss-unified-agent.jar -userKey ${USER_KEY} -apiKey ${API_KEY} -projectVersion ${PROJECT_VERSION} -projectToken ${PROJECT_TOKEN} -productVersion ${PRODUCT_VERSION} -productToken ${PRODUCT_TOKEN} -c ws.config -d ../ \ No newline at end of file