diff options
author | Martin Stockhammer <martin_s@apache.org> | 2021-01-11 16:53:37 +0100 |
---|---|---|
committer | Martin Stockhammer <martin_s@apache.org> | 2021-01-11 16:57:13 +0100 |
commit | 0dc2e8fff6e21178b62d10982e34067655ff6524 (patch) | |
tree | b4062e8967a6a5cac32f948c0aeb4023a4ad0ee2 /archiva-modules/archiva-web/archiva-webapp | |
parent | 76985db7ffa4ecce1557444eb490f9bceb06fb1a (diff) | |
download | archiva-0dc2e8fff6e21178b62d10982e34067655ff6524.tar.gz archiva-0dc2e8fff6e21178b62d10982e34067655ff6524.zip |
Improving LDAP configuration
Diffstat (limited to 'archiva-modules/archiva-web/archiva-webapp')
11 files changed, 252 insertions, 20 deletions
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts index ae1b0a45a..752291540 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/app-notification.ts @@ -23,7 +23,7 @@ export class AppNotification { header: string; body: string | TemplateRef<any>; timestamp: Date; - classname: string=''; + classname: string[]=['']; delay:number=5000; contextData:any; type:string='normal' diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/ldap-configuration.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/ldap-configuration.ts index eb01950c2..65a15cea6 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/ldap-configuration.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/ldap-configuration.ts @@ -16,6 +16,8 @@ * under the License. */ +import {PropertyMap} from "@app/model/property-map"; + export class LdapConfiguration { host_name : string = ""; port : number = 389; @@ -28,7 +30,28 @@ export class LdapConfiguration { authentication_method : string = ""; bind_authenticator_enabled : boolean = true; use_role_name_as_group : boolean = false; - properties : Map<string,string> = new Map<string, string>() + properties : PropertyMap; writable : boolean = false; available_context_factories : string[]; + + + constructor(initObj:any=null) { + if (initObj) { + this.host_name = initObj.host_name + this.port = initObj.port + this.ssl_enabled = initObj.ssl_enabled + this.context_factory = initObj.context_factory + this.base_dn = initObj.base_dn + this.groups_base_dn = initObj.groups_base_dn + this.bind_dn = initObj.bind_dn + this.bind_password = initObj.bind_password + this.authentication_method = initObj.authentication_method + this.bind_authenticator_enabled = initObj.bind_authenticator_enabled + this.use_role_name_as_group = initObj.use_role_name_as_group + this.properties = new PropertyMap(Object.entries(initObj.properties)) + this.writable = initObj.writable + this.available_context_factories = initObj.available_context_factories + } + } + } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.spec.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.spec.ts new file mode 100644 index 000000000..f0b834605 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.spec.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { PropertyMap } from './property-map'; + +describe('PropertyMap', () => { + it('should create an instance', () => { + expect(new PropertyMap()).toBeTruthy(); + }); +}); diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.ts new file mode 100644 index 000000000..036db83e8 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/property-map.ts @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 class PropertyMap extends Map<string,string> { + + constructor(entries?: readonly (readonly [string, string])[] | null) { + super(entries); + } + + toJSON() { + let object = { }; + for (let [key, value] of this) object[key] = value; + return object; + } +} diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.html index 5f2705861..f7940741f 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.html +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.html @@ -35,6 +35,7 @@ <div class="col-md-7"> <input type="text" formControlName="context_factory" id="context_factory" [ngClass]="getInputClasses('context_factory')" [ngbTypeahead]="searchContextFactory" + [placement]="top" > </div> </div> @@ -60,15 +61,41 @@ </div> </div> </div> + <div class="form-group row col-md-10" > + <div class="col-md-3">{{'security.config.ldap.attributes.properties'|translate}}</div> + <div class="col-md-7 form-row"> + <input type="text" id="prop_key" formControlName="prop_key" class="form-control col" placeholder="{{'form.button.key'|translate}}" + + > + <input type="prop_value" id="prop_value" formControlName="prop_value" + class="form-control col" placeholder="{{'form.button.value'|translate}}"> + <button type="button" class="ml-2 btn btn-primary col" (click)="addProperty()">{{'form.button.add'|translate}}</button> + </div> + </div> + <div class="form-group row col-md-10" *ngIf="ldapProperties && ldapProperties.size>0"> + <div class="col-md-3"></div> + <div class="col-md-7 pl-2 list-group"> + <div class="list-group-item" *ngFor="let propEntry of ldapProperties |keyvalue"> + <span class="float-left">{{propEntry.key}}={{propEntry.value}}</span> + <a class="float-right" [routerLink]="" (click)="removeProperty(propEntry.key)" ><i class="fas fa-trash-alt"></i></a> + </div> + </div> + </div> + <div class="row col-md-10 mt-4"> <button class="btn btn-primary col-md-2" type="submit" - [disabled]="userForm.invalid || !userForm.dirty">{{'form.button.save'|translate}}</button> + [disabled]="checkProgress|| userForm.invalid || !userForm.dirty">{{'form.button.save'|translate}}</button> <button class="btn btn-primary col-md-2 offset-1" type="button" (click)="checkLdapConnection()" - [disabled]="userForm.invalid || !userForm.dirty">{{'form.button.check'|translate}}</button> + [disabled]="checkProgress||userForm.invalid"> + <span *ngIf="checkProgress" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> + <span *ngIf="checkProgress"> {{'form.button.checking'|translate}}</span> + <span *ngIf="!checkProgress">{{'form.button.check'|translate}}</span></button> </div> <div class="row col-md-10 mt-2"> <div class="alert col-md-6 ml-1 alert-success" role="alert" + *ngIf="submitError">{{'security.config.ldap.submit_error'|translate:{error:submitError.toString()} }}</div> + <div class="alert col-md-6 ml-1 alert-success" role="alert" *ngIf="checkResult=='success'">{{'security.config.ldap.check_success'|translate}}</div> <div class="alert col-md-6 ml-1 alert-warning" role="alert" *ngIf="checkResult=='error'">{{'security.config.ldap.check_failed'|translate}}</div> diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.ts index f0c4eeb31..03bc343a2 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/security/security-configuration/ldap-security/ldap-security.component.ts @@ -26,6 +26,8 @@ import {ToastService} from "@app/services/toast.service"; import {ErrorResult} from "@app/model/error-result"; import {Observable} from 'rxjs'; import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators'; +import {HttpResponse} from "@angular/common/http"; +import {PropertyMap} from "@app/model/property-map"; @Component({ selector: 'app-ldap-security', @@ -39,7 +41,10 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> 'base_dn', 'groups_base_dn', 'bind_dn', 'bind_password', 'authentication_method', 'bind_authenticator_enabled', 'use_role_name_as_group', 'writable']; availableContextFactories = []; + ldapProperties : PropertyMap = new PropertyMap(); checkResult = null; + submitError = null; + checkProgress=false; constructor(private route: ActivatedRoute, public fb: FormBuilder, private securityService: SecurityService, private toastService: ToastService) { @@ -56,7 +61,9 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> authentication_method: ['none'], bind_authenticator_enabled: [false], use_role_name_as_group: [true], - writable: [false] + writable: [false], + prop_key:[''], + prop_value:[''] }, {})); } @@ -69,7 +76,13 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> if (ldapConfiguration.authentication_method == '') { this.userForm.controls['authentication_method'].setValue('none'); } - this.availableContextFactories = ldapConfiguration.available_context_factories + this.availableContextFactories = ldapConfiguration.available_context_factories; + console.log("Props: " + ldapConfiguration.properties + " " + typeof (ldapConfiguration.properties)); + if (ldapConfiguration.properties) { + this.ldapProperties = ldapConfiguration.properties as PropertyMap; + } else { + this.ldapProperties = new PropertyMap(); + } } ) this.userForm.controls['bind_dn'].valueChanges.subscribe(selectedValue => { @@ -79,7 +92,7 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> this.userForm.controls['authentication_method'].setValue('none', {emitEvent: false}) } }) - this.userForm.valueChanges.subscribe(val => { + this.userForm.valueChanges.subscribe(() => { this.checkResult = null; }) @@ -92,6 +105,23 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> onSubmit() { console.log("Saving configuration"); let config = this.copyFromForm(this.formFields) + if (this.ldapProperties) { + config.properties = this.ldapProperties; + } + this.securityService.updateLdapConfiguration(config).subscribe((response: HttpResponse<LdapConfiguration>)=> { + this.toastService.showSuccessByKey('ldap-security', 'security.config.ldap.submit_success'); + this.userForm.reset(); + this.submitError=null; + this.checkResult=null; + this.copyToForm(this.formFields, response.body) + }, + (error: ErrorResult) =>{ + this.toastService.showSuccessByKey('ldap-security', 'security.config.ldap.submit_error', {error:error.toString()}); + this.submitError = error; + this.checkResult=null; + } + + ); } @@ -100,7 +130,7 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> text$.pipe( debounceTime(200), distinctUntilChanged(), - map(term => term.length < 2 ? [] + map(term => term.length < 1 ? [] : this.availableContextFactories.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10)) ) @@ -116,16 +146,44 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> checkLdapConnection() { console.log("Checking LDAP connection"); let config = this.copyFromForm(this.formFields) + if (this.ldapProperties) { + config.properties = this.ldapProperties; + } + this.checkProgress=true; this.securityService.verifyLdapConfiguration(config).subscribe(() => { this.toastService.showSuccessByKey('ldap-security', 'security.config.ldap.check_success'); this.checkResult = 'success'; + this.checkProgress=false; }, (error: ErrorResult) => { this.toastService.showErrorByKey('ldap-security', error.firstMessageString()); this.checkResult = 'error'; + this.checkProgress=false; } ); } + + addProperty() { + let key = this.userForm.controls['prop_key'].value + let value = this.userForm.controls['prop_value'].value + + console.log("Prop " + key + " = " + value); + if (key && key!='') { + setTimeout(() => { + this.ldapProperties.set(key, value); + this.userForm.markAsDirty(); + }); + } + this.userForm.controls['prop_key'].setValue('') + this.userForm.controls['prop_value'].setValue('') + } + + removeProperty(key:string) { + setTimeout(()=>{ + this.ldapProperties.delete(key); + this.userForm.markAsDirty(); + }) + } } /** @@ -134,6 +192,9 @@ export class LdapSecurityComponent extends EditBaseComponent<LdapConfiguration> export function dnValidator(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } | null => { let parts = [] + if (control.value==null) { + return null; + } let value = control.value.toString() if (value == '') { return null; diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.scss b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.scss new file mode 100644 index 000000000..878354a63 --- /dev/null +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.scss @@ -0,0 +1,49 @@ +/*! + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +@import '~bootstrap/scss/functions'; +@import '~bootstrap/scss/variables'; +@import '~bootstrap/scss/mixins'; +@import '~bootstrap/scss/alert'; + +:host { + margin:.5em; + padding:1em; + position:fixed; + right:10px; + top:40px; + z-index:1200; +} + +.toast { + max-width: 350px; + font-size: 0.875rem; + background-clip: padding-box; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1); + border-radius: 0.5rem; +} + +ngb-toast.alert { + @extend .alert; +} + +@each $color, $value in $theme-colors { + ngb-toast.alert-#{$color} { + @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), theme-color-level($color, $alert-color-level)); + } +} diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts index 2873b8a6c..995e5d19c 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/shared/toast/toast.component.ts @@ -26,7 +26,7 @@ import {AppNotification} from "@app/model/app-notification"; template: ` <ngb-toast *ngFor="let toast of toastService.toasts" - [class]="toast.classname" + [ngClass]="toast.classname" [autohide]="autohide" [delay]="toast.delay || 5000" (hidden)="toastService.remove(toast); autohide=true;" @@ -41,7 +41,7 @@ import {AppNotification} from "@app/model/app-notification"; <ng-template #text>{{ toast.body }}</ng-template> </ngb-toast> `, - styles: [':host { margin:.5em; padding:1em; position:fixed; right:10px; top:40px; z-index:1200; }'] + styleUrls:['./toast.component.scss'] }) export class ToastComponent implements OnInit { @@ -53,7 +53,6 @@ export class ToastComponent implements OnInit { } isTemplate(toast:AppNotification) { - console.log("Context data: "+JSON.stringify(toast.contextData)) return toast.body instanceof TemplateRef; } } diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/security.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/security.service.ts index 8758901b9..244257ab1 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/security.service.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/security.service.ts @@ -20,7 +20,7 @@ import { Injectable } from '@angular/core'; import {ArchivaRequestService} from "@app/services/archiva-request.service"; import {SecurityConfiguration} from "@app/model/security-configuration"; import {Observable, throwError} from "rxjs"; -import {catchError} from "rxjs/operators"; +import {catchError, map} from "rxjs/operators"; import {HttpErrorResponse, HttpResponse} from "@angular/common/http"; import {BeanInformation} from "@app/model/bean-information"; import {LdapConfiguration} from "@app/model/ldap-configuration"; @@ -42,10 +42,9 @@ export class SecurityService { ); } - updateConfiguration(securityConfiguration : SecurityConfiguration) : Observable<HttpResponse<any>> { - return this.rest.executeResponseCall<any>("put", "archiva", "security/config", securityConfiguration).pipe( + updateConfiguration(securityConfiguration : SecurityConfiguration) : Observable<HttpResponse<SecurityConfiguration>> { + return this.rest.executeResponseCall<SecurityConfiguration>("put", "archiva", "security/config", securityConfiguration).pipe( catchError((error: HttpErrorResponse) => { - console.log("Error thrown " + typeof (error)); return throwError(this.rest.getTranslatedErrorResult(error)); }) ); @@ -69,12 +68,24 @@ export class SecurityService { getLdapConfiguration() : Observable<LdapConfiguration> { return this.rest.executeRestCall<LdapConfiguration>("get", "archiva", "security/config/ldap", null).pipe( + map((ldapConfig:LdapConfiguration)=> + new LdapConfiguration(ldapConfig) + ) , catchError((error: HttpErrorResponse) => { return throwError(this.rest.getTranslatedErrorResult(error)); }) ); } + updateLdapConfiguration(ldapConfiguration : LdapConfiguration) : Observable<HttpResponse<LdapConfiguration>> { + return this.rest.executeResponseCall<LdapConfiguration>("put", "archiva", "security/config/ldap", ldapConfiguration).pipe( + catchError((error: HttpErrorResponse) => { + return throwError(this.rest.getTranslatedErrorResult(error)); + }) + ); + } + + verifyLdapConfiguration(ldapConfig : LdapConfiguration) : Observable<HttpResponse<any>> { return this.rest.executeResponseCall<any>("post", "archiva", "security/config/ldap/verify", ldapConfig).pipe( catchError((error: HttpErrorResponse) => { diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts index 1b1a934aa..f4955ef1f 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/toast.service.ts @@ -47,7 +47,7 @@ export class ToastService { } public showStandard(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void { - options.classname='bg-primary' + options.classname=['alert','alert-primary'] if (!options.delay) { options.delay=8000 } @@ -65,7 +65,7 @@ export class ToastService { } public showError(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void { - options.classname='bg-warning' + options.classname=['alert','alert-danger'] options.type='error' if (!options.delay) { options.delay=10000 @@ -84,7 +84,7 @@ export class ToastService { } public showSuccess(origin:string,textOrTpl:string|TemplateRef<any>, options:any={}) : void { - options.classname='bg-info' + options.classname=['alert','alert-info'] options.type='success' if (!options.delay) { options.delay=8000 diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json index a10c2cf66..dff49c78e 100644 --- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json +++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/assets/i18n/en.json @@ -210,6 +210,8 @@ "explain": "Settings for the LDAP connection. These settings are only used, if LDAP User Manager and/or LDAP RBAC Manager is activated.", "check_success": "The LDAP settings have been verified successfully", "check_failed": "Could not connect to the LDAP server successfully", + "submit_success": "The LDAP settings have been updated", + "submit_error": "Could not update the LDAP settings: {error}", "attributes": { "host_name": "Host Name", "port": "Port", @@ -222,7 +224,8 @@ "use_role_name_as_group": "Archiva role names are LDAP group names", "writable": "Bind User can write to LDAP Server", "ssl_enabled": "Enable SSL", - "context_factory": "Context Factory" + "context_factory": "Context Factory", + "properties": "Properties" }, "flags": "Flags" @@ -253,7 +256,11 @@ "yes": "Yes", "no": "No", "save": "Save Changes", - "check": "Check configuration" + "check": "Check configuration", + "checking": "Checking ...", + "add": "Add", + "key": "Key", + "value": "Value" }, "edit": "Edit", "emptyContent": "No values", |