You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by pz...@apache.org on 2018/03/09 18:37:42 UTC

[3/4] knox git commit: KNOX-1040 - Initial new descriptor and provider config wizard support

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/provider-config-wizard/provider-config-wizard.component.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/provider-config-wizard/provider-config-wizard.component.ts b/gateway-admin-ui/src/app/provider-config-wizard/provider-config-wizard.component.ts
new file mode 100644
index 0000000..8cb4fb9
--- /dev/null
+++ b/gateway-admin-ui/src/app/provider-config-wizard/provider-config-wizard.component.ts
@@ -0,0 +1,254 @@
+/*
+ * 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 {Component, OnInit, ViewChild} from '@angular/core';
+import {ResourceTypesService} from "../resourcetypes/resourcetypes.service";
+import {ResourceService} from "../resource/resource.service";
+import {BsModalComponent} from "ng2-bs3-modal";
+import {ProviderConfig} from "../resource-detail/provider-config";
+import {AuthenticationWizard} from "./authentication-wizard";
+import {CategoryWizard} from "./category-wizard";
+import {AuthorizationWizard} from "./authorization-wizard";
+import {IdentityAssertionWizard} from "./identity-assertion-wizard";
+import {HaWizard} from "./ha-wizard";
+import {Resource} from "../resource/resource";
+import {DisplayBindingProviderConfig} from "./display-binding-provider-config";
+
+@Component({
+  selector: 'app-provider-config-wizard',
+  templateUrl: './provider-config-wizard.component.html',
+  styleUrls: ['./provider-config-wizard.component.css']
+})
+export class ProviderConfigWizardComponent implements OnInit {
+
+  private static CATEGORY_STEP = 1;
+  private static TYPE_STEP     = 2;
+  private static PARAMS_STEP   = 3;
+
+  // Provider Categories
+  private static CATEGORY_AUTHENTICATION: string  = 'Authentication';
+  private static CATEGORY_AUTHORIZATION: string   = 'Authorization';
+  private static CATEGORY_ID_ASSERTION: string    = 'Identity Assertion';
+  private static CATEGORY_HA: string              = 'HA';
+  private static providerCategories: string[] = [ ProviderConfigWizardComponent.CATEGORY_AUTHENTICATION,
+                                                  ProviderConfigWizardComponent.CATEGORY_AUTHORIZATION,
+                                                  ProviderConfigWizardComponent.CATEGORY_ID_ASSERTION,
+                                                  ProviderConfigWizardComponent.CATEGORY_HA
+                                                ];
+
+  private static CATEGORY_TYPES: Map<string, CategoryWizard> =
+            new Map([
+              [ProviderConfigWizardComponent.CATEGORY_AUTHENTICATION, new AuthenticationWizard() as CategoryWizard],
+              [ProviderConfigWizardComponent.CATEGORY_AUTHORIZATION,  new AuthorizationWizard() as CategoryWizard],
+              [ProviderConfigWizardComponent.CATEGORY_ID_ASSERTION,   new IdentityAssertionWizard() as CategoryWizard],
+              [ProviderConfigWizardComponent.CATEGORY_HA,             new HaWizard() as CategoryWizard]
+            ]);
+
+  @ViewChild('newProviderConfigModal')
+  childModal: BsModalComponent;
+
+  private step: number = 0;
+
+  name: String = '';
+
+  providers: Array<ProviderConfig> = [];
+
+  selectedCategory: string;
+
+  constructor(private resourceTypesService: ResourceTypesService, private resourceService: ResourceService) { }
+
+  ngOnInit() {
+    this.selectedCategory = ProviderConfigWizardComponent.CATEGORY_AUTHENTICATION; // Default to authentication
+  }
+
+  open(size?: string) {
+    this.reset();
+    this.childModal.open(size ? size : 'lg');
+  }
+
+  reset() {
+    this.step = 0;
+    this.name = '';
+    this.providers = [];
+    this.selectedCategory = ProviderConfigWizardComponent.CATEGORY_AUTHENTICATION;
+  }
+
+  onFinishAdd() {
+    console.debug('Selected provider category: ' + this.selectedCategory);
+
+    let catWizard = this.getCategoryWizard(this.selectedCategory);
+    let type = catWizard ? catWizard.getSelectedType() : 'undefined';
+    console.debug('Selected provider type: ' + type);
+
+    if (catWizard) {
+      let pc: ProviderConfig = catWizard.getProviderConfig();
+      if (pc) {
+        this.providers.push(pc);
+        console.debug('\tProvider Name: ' + pc.name);
+        console.debug('\tProvider Role: ' + pc.role);
+        console.debug('\tProvider Enabled: ' + pc.enabled);
+        if (pc.params) {
+          for (let name of Object.getOwnPropertyNames(pc.params)) {
+            console.debug('\t\tParam: ' + name + ' = ' + pc.params[name]);
+          }
+        } else {
+          console.debug('\tNo Params');
+        }
+      }
+    }
+
+    this.step = 0; // Return to the beginning
+  }
+
+  onClose() {
+    console.debug('Provider Configuration: ' + this.name);
+
+    for (let pc of this.providers) {
+      console.debug('\tProvider: ' + pc.name + ' (' + pc.role + ')');
+    }
+
+    // Identify the new resource
+    let newResource = new Resource();
+    newResource.name = this.name + '.json';
+
+    // Persist the new provider configuration
+    this.resourceService.createResource('Provider Configurations',
+                                        newResource,
+                                        this.resourceService.serializeProviderConfiguration(this.providers, 'json'))
+                        .then(() => {
+                          // Reload the resource list presentation
+                          this.resourceTypesService.selectResourceType('Provider Configurations');
+
+                          // Set the new descriptor as the selected resource
+                          this.resourceService.getProviderConfigResources().then(resources => {
+                            for (let res of resources) {
+                              if (res.name === newResource.name) {
+                                this.resourceService.selectedResource(res);
+                                break;
+                              }
+                            }
+                          });
+                        });
+  }
+
+  getStep(): number {
+    return this.step;
+  }
+
+  onNextStep() {
+    ++this.step;
+  }
+
+  onPreviousStep() {
+    --this.step;
+  }
+
+  hasMoreSteps(): boolean {
+    let result = false;
+    let catWizard = this.getCategoryWizard(this.selectedCategory);
+    if (catWizard) {
+      result = (this.step < (catWizard.getSteps() - 1));
+    }
+    return result;
+  }
+
+  isRootStep(): boolean {
+    return (this.step === 0);
+  }
+
+  isProviderCategoryStep(): boolean {
+    return (this.step === ProviderConfigWizardComponent.CATEGORY_STEP);
+  }
+
+  isProviderTypeStep(): boolean {
+    return (this.step === ProviderConfigWizardComponent.TYPE_STEP);
+  }
+
+  isProviderParamsStep(): boolean {
+    return (this.step === ProviderConfigWizardComponent.PARAMS_STEP);
+  }
+
+  getProviderCategories() : string[] {
+    return ProviderConfigWizardComponent.providerCategories;
+  }
+
+  getCategoryWizard(category?: string): CategoryWizard {
+    return ProviderConfigWizardComponent.CATEGORY_TYPES.get(category ? category : this.selectedCategory);
+  }
+
+  getProviderTypes(category?: string) : string[] {
+    let catWizard = this.getCategoryWizard(category);
+    if (catWizard) {
+      return catWizard.getTypes();
+    } else {
+      console.debug('Unresolved category wizard for ' + (category ? category : this.selectedCategory));
+    }
+    return [];
+  }
+
+  getProviderParams(): string[] {
+    let catWizard = this.getCategoryWizard();
+    if (catWizard) {
+      let pc = catWizard.getProviderConfig();
+      if (pc) {
+        if (pc instanceof DisplayBindingProviderConfig) {
+          let dispPC = pc as DisplayBindingProviderConfig;
+          return dispPC.getDisplayPropertyNames();
+        } else {
+          console.debug('Got Vanilla ProviderConfig');
+          return [];
+        }
+      } else {
+        console.log('No provider config from category wizard ' + typeof(catWizard));
+      }
+    } else {
+      console.debug('Unresolved category wizard for ' + this.selectedCategory);
+    }
+    return [];
+  }
+
+  setProviderParamBinding(name: string, value: string) {
+    let catWizard = this.getCategoryWizard();
+    if (catWizard) {
+      let pc = catWizard.getProviderConfig();
+      if (pc) {
+        if (pc instanceof DisplayBindingProviderConfig) {
+          let dispPC = pc as DisplayBindingProviderConfig;
+          let property = dispPC.getDisplayNamePropertyBinding(name);
+          pc.setParam(property, value);
+          console.debug('Set ProviderConfig param value: ' + property + '=' + value);
+        }
+      }
+    }
+  }
+
+  getProviderParamBinding(name: string): string {
+    let catWizard = this.getCategoryWizard();
+    if (catWizard) {
+      let pc = catWizard.getProviderConfig();
+      if (pc) {
+        if (pc instanceof DisplayBindingProviderConfig) {
+          let dispPC = pc as DisplayBindingProviderConfig;
+          let value = pc.getParam(dispPC.getDisplayNamePropertyBinding(name));
+          return (value ? value : '');
+        }
+      }
+    }
+    return '';
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/provider-config-wizard/regex-idassertion-provider-config.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/provider-config-wizard/regex-idassertion-provider-config.ts b/gateway-admin-ui/src/app/provider-config-wizard/regex-idassertion-provider-config.ts
new file mode 100644
index 0000000..a3a4e9b
--- /dev/null
+++ b/gateway-admin-ui/src/app/provider-config-wizard/regex-idassertion-provider-config.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 {IdentityAssertionProviderConfig} from "./identity-assertion-provider-config";
+
+export class RegexAssertionProviderConfig extends IdentityAssertionProviderConfig {
+
+  static INPUT        = 'Input';
+  static OUTPUT       = 'Output';
+  static LOOKUP       = "Lookup";
+  static ORIG_ON_FAIL = "User Original Lookup on Failure";
+
+  private static displayPropertyNames = [ RegexAssertionProviderConfig.INPUT,
+                                          RegexAssertionProviderConfig.OUTPUT,
+                                          RegexAssertionProviderConfig.LOOKUP,
+                                          RegexAssertionProviderConfig.ORIG_ON_FAIL
+                                        ];
+
+  private static displayPropertyNameBindings: Map<string, string> =
+                                        new Map([
+                                          [RegexAssertionProviderConfig.INPUT,        'input'],
+                                          [RegexAssertionProviderConfig.OUTPUT,       'output'],
+                                          [RegexAssertionProviderConfig.LOOKUP,       'lookup'],
+                                          [RegexAssertionProviderConfig.ORIG_ON_FAIL, 'use.original.on.lookup.failure']
+                                        ]);
+
+  constructor() {
+    super('Regex');
+  }
+
+  getDisplayPropertyNames(): string[] {
+    return RegexAssertionProviderConfig.displayPropertyNames;
+  }
+
+  getDisplayNamePropertyBinding(name: string) {
+    return RegexAssertionProviderConfig.displayPropertyNameBindings.get(name);
+  }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/provider-config-wizard/saml-provider-config.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/provider-config-wizard/saml-provider-config.ts b/gateway-admin-ui/src/app/provider-config-wizard/saml-provider-config.ts
new file mode 100644
index 0000000..c9cffc1
--- /dev/null
+++ b/gateway-admin-ui/src/app/provider-config-wizard/saml-provider-config.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 {AuthenticationProviderConfig} from "./authentication-provider-config";
+
+export class SAMLProviderConfig extends AuthenticationProviderConfig {
+
+  static CALLBACK_URL          = 'Callback URL';
+  static KEYSTORE_PATH         = 'Keystore Path';
+  static KEYSTORE_PASS         = 'Keystore Password';
+  static PK_PASS               = 'Private Key Password';
+  static ID_PROVIDER_META      = 'Identity Provider Metadata Path';
+  static MAX_AUTH_LIFETIME     = 'Maximum Authentication Lifetime';
+  static SERVICE_PROVIDER_ID   = 'Service Provider Identity';
+  static SERVICE_PROVIDER_META = 'Service Provider Metadata Path';
+  static COOKIE_DOMAIN_SUFFIX = 'Cookie Domain Suffix';
+
+
+  private static displayPropertyNames = [ SAMLProviderConfig.CALLBACK_URL,
+                                          SAMLProviderConfig.KEYSTORE_PATH,
+                                          SAMLProviderConfig.KEYSTORE_PASS,
+                                          SAMLProviderConfig.PK_PASS,
+                                          SAMLProviderConfig.ID_PROVIDER_META,
+                                          SAMLProviderConfig.MAX_AUTH_LIFETIME,
+                                          SAMLProviderConfig.SERVICE_PROVIDER_ID,
+                                          SAMLProviderConfig.SERVICE_PROVIDER_META,
+                                          SAMLProviderConfig.COOKIE_DOMAIN_SUFFIX
+                                        ];
+
+  private static displayPropertyNameBindings: Map<string, string> =
+                                    new Map([
+                                      [SAMLProviderConfig.CALLBACK_URL,          'pac4j.callbackUrl'],
+                                      [SAMLProviderConfig.COOKIE_DOMAIN_SUFFIX,  'pac4j.cookie.domain.suffix'],
+                                      [SAMLProviderConfig.KEYSTORE_PATH,         'saml.keystorePath'],
+                                      [SAMLProviderConfig.KEYSTORE_PASS,         'saml.keystorePassword'],
+                                      [SAMLProviderConfig.PK_PASS,               'saml.privateKeyPassword'],
+                                      [SAMLProviderConfig.ID_PROVIDER_META,      'saml.identityProviderMetadataPath'],
+                                      [SAMLProviderConfig.MAX_AUTH_LIFETIME,     'saml.maximumAuthenticationLifetime'],
+                                      [SAMLProviderConfig.SERVICE_PROVIDER_ID,   'saml.serviceProviderEntityId'],
+                                      [SAMLProviderConfig.SERVICE_PROVIDER_META, 'saml.serviceProviderMetadataPath']
+                                    ]);
+
+
+  constructor() {
+    super('pac4j', AuthenticationProviderConfig.FEDERATION_ROLE);
+  }
+
+  getDisplayPropertyNames(): string[] {
+    return SAMLProviderConfig.displayPropertyNames;
+  }
+
+  getDisplayNamePropertyBinding(name: string) {
+    return SAMLProviderConfig.displayPropertyNameBindings.get(name);
+  }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/provider-config-wizard/sso-cookie-provider-config.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/provider-config-wizard/sso-cookie-provider-config.ts b/gateway-admin-ui/src/app/provider-config-wizard/sso-cookie-provider-config.ts
new file mode 100644
index 0000000..bc54e6a
--- /dev/null
+++ b/gateway-admin-ui/src/app/provider-config-wizard/sso-cookie-provider-config.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 {AuthenticationProviderConfig} from "./authentication-provider-config";
+
+export class SSOCookieProviderConfig extends AuthenticationProviderConfig {
+
+  static PROVIDER_URL  = 'Provider URL';
+
+  private static displayPropertyNames = [ SSOCookieProviderConfig.PROVIDER_URL ];
+
+  private static displayPropertyNameBindings: Map<string, string> =
+                    new Map([ [SSOCookieProviderConfig.PROVIDER_URL, 'sso.authentication.provider.url'] ]);
+
+
+  constructor() {
+    console.debug('new SSOCookieProviderConfig()');
+    super('SSOCookieProvider', AuthenticationProviderConfig.FEDERATION_ROLE);
+  }
+
+  getDisplayPropertyNames(): string[] {
+    return SSOCookieProviderConfig.displayPropertyNames;
+  }
+
+  getDisplayNamePropertyBinding(name: string) {
+    return SSOCookieProviderConfig.displayPropertyNameBindings.get(name);
+  }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/provider-config-wizard/switchcase-idassertion-provider-config.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/provider-config-wizard/switchcase-idassertion-provider-config.ts b/gateway-admin-ui/src/app/provider-config-wizard/switchcase-idassertion-provider-config.ts
new file mode 100644
index 0000000..5284f13
--- /dev/null
+++ b/gateway-admin-ui/src/app/provider-config-wizard/switchcase-idassertion-provider-config.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 {IdentityAssertionProviderConfig} from "./identity-assertion-provider-config";
+
+export class SwitchCaseAssertionProviderConfig extends IdentityAssertionProviderConfig {
+
+  static PRINCIPAL_CASE       = 'Principal Case';
+  static GROUP_PRINCIPAL_CASE = 'Group Principal Case';
+
+  private static displayPropertyNames = [ SwitchCaseAssertionProviderConfig.PRINCIPAL_CASE,
+                                          SwitchCaseAssertionProviderConfig.GROUP_PRINCIPAL_CASE
+                                        ];
+
+  private static displayPropertyNameBindings: Map<string, string> =
+                                    new Map([
+                                      [SwitchCaseAssertionProviderConfig.PRINCIPAL_CASE,       'principal.case'],
+                                      [SwitchCaseAssertionProviderConfig.GROUP_PRINCIPAL_CASE, 'group.principal.case']
+                                    ]);
+
+  constructor() {
+    super('SwitchCase');
+  }
+
+  getDisplayPropertyNames(): string[] {
+    return SwitchCaseAssertionProviderConfig.displayPropertyNames;
+  }
+
+  getDisplayNamePropertyBinding(name: string) {
+    return SwitchCaseAssertionProviderConfig.displayPropertyNameBindings.get(name);
+  }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/resource-detail/resource-detail.component.html
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/resource-detail/resource-detail.component.html b/gateway-admin-ui/src/app/resource-detail/resource-detail.component.html
index fc8d1d7..27a09b6 100644
--- a/gateway-admin-ui/src/app/resource-detail/resource-detail.component.html
+++ b/gateway-admin-ui/src/app/resource-detail/resource-detail.component.html
@@ -46,8 +46,8 @@
       <div class="panel-body" *ngIf="isShowProvider(provider)">
         <strong>Role</strong> {{ provider.role }}<br>
         <strong>Enabled</strong>&nbsp;<input type="checkbox"
-                                                     [checked]="isProviderEnabled(provider)"
-                                                     (click)="onProviderEnabled(provider)"><br>
+                                             [checked]="isProviderEnabled(provider)"
+                                             (click)="onProviderEnabled(provider)"><br>
         <div>
           <span [class]="'clickable inline-glyph glyhpicon glyphicon-' + (isShowProviderParams(provider) ? 'minus' : 'plus')"
                 (click)="toggleShowProviderParams(provider)"></span>
@@ -137,7 +137,7 @@
       <div class="panel panel-default col-md-12">
         <span class="col-md-12 pull-left">
           <span class="col-md-sm"><strong>Provider Configuration</strong>&nbsp;</span>
-          <span class="col-md-sm inline-editable" (click)="editModePC=true" *ngIf="!editModePC">{{ descriptor.providerConfig}}</span>
+          <span class="col-md-sm inline-editable" (click)="editModePC=true" *ngIf="!editModePC">{{descriptor.providerConfig}}</span>
           <span class="col-md-sm inline-editor inlineEditForm" *ngIf="editModePC">
             <input type="text" size="40" [(ngModel)]="descriptor.providerConfig">
             <button class="btn btn-xs" (click)="editModePC=false;descriptor.setDirty()">

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/resource-detail/resource-detail.component.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/resource-detail/resource-detail.component.ts b/gateway-admin-ui/src/app/resource-detail/resource-detail.component.ts
index 14a5213..2ffaf77 100644
--- a/gateway-admin-ui/src/app/resource-detail/resource-detail.component.ts
+++ b/gateway-admin-ui/src/app/resource-detail/resource-detail.component.ts
@@ -79,6 +79,7 @@ export class ResourceDetailComponent implements OnInit {
 
   setResource(res: Resource) {
       //console.debug('ResourceDetailComponent --> setResource() --> ' + ((res) ? res.name : 'null'));
+      this.referencedProviderConfigError = false;
       this.resource = res;
       this.providers = [];
       this.changedProviders = null;
@@ -201,12 +202,12 @@ export class ResourceDetailComponent implements OnInit {
     let ext = this.resource.name.split('.').pop();
     switch(ext) {
       case 'json': {
-        content = this.serializeProviderConfiguration(this.providers, 'json');
+        content = this.resourceService.serializeProviderConfiguration(this.providers, 'json');
         break;
       }
       case 'yaml':
       case 'yml': {
-        content = this.serializeProviderConfiguration(this.providers, 'yaml');
+        content = this.resourceService.serializeProviderConfiguration(this.providers, 'yaml');
         break;
       }
       case 'xml': {
@@ -215,7 +216,7 @@ export class ResourceDetailComponent implements OnInit {
         console.debug('Replacing XML provider configuration ' + this.resource.name + ' with JSON...');
 
         // Generate the JSON representation of the updated provider configuration
-        content = this.serializeProviderConfiguration(this.providers, 'json');
+        content = this.resourceService.serializeProviderConfiguration(this.providers, 'json');
 
         let replacementResource = new Resource();
         replacementResource.name = this.resource.name.slice(0, -4) + '.json';
@@ -259,12 +260,12 @@ export class ResourceDetailComponent implements OnInit {
     let ext = this.resource.name.split('.').pop();
     switch(ext) {
       case 'json': {
-        content = this.serializeDescriptor(this.descriptor, 'json');
+        content = this.resourceService.serializeDescriptor(this.descriptor, 'json');
         break;
       }
       case 'yaml':
       case 'yml': {
-        content = this.serializeDescriptor(this.descriptor, 'yaml');
+        content = this.resourceService.serializeDescriptor(this.descriptor, 'yaml');
         break;
       }
     }
@@ -281,82 +282,6 @@ export class ResourceDetailComponent implements OnInit {
   }
 
 
-  serializeDescriptor(desc: Descriptor, format: string): string {
-      let serialized: string;
-
-      let tmp = {};
-      if (desc.discoveryAddress) {
-        tmp['discovery-address'] = desc.discoveryAddress;
-      }
-      if (desc.discoveryUser) {
-        tmp['discovery-user'] = desc.discoveryUser;
-      }
-      if (desc.discoveryPassAlias) {
-        tmp['discovery-pwd-alias'] = desc.discoveryPassAlias;
-      }
-      if (desc.discoveryCluster) {
-        tmp['cluster'] = desc.discoveryCluster;
-      }
-      tmp['provider-config-ref'] = desc.providerConfig;
-      tmp['services'] = desc.services;
-
-      switch(format) {
-          case 'json': {
-              serialized =
-                  JSON.stringify(tmp,
-                                 (key, value) => {
-                                   let result = value;
-                                   switch(typeof value) {
-                                     case 'string': // Don't serialize empty string value properties
-                                       result = (value.length > 0) ? value : undefined;
-                                       break;
-                                     case 'object':
-                                       if (Array.isArray(value)) {
-                                         // Don't serialize empty array value properties
-                                         result = (value.length) > 0 ? value : undefined;
-                                       } else {
-                                         // Don't serialize object value properties
-                                         result = (Object.getOwnPropertyNames(value).length > 0) ? value : undefined;
-                                       }
-                                       break;
-                                   }
-                                   return result;
-                                 }, 2);
-              break;
-          }
-          case 'yaml': {
-              let yaml = require('js-yaml');
-              serialized = '---\n' + yaml.safeDump(tmp);
-              break;
-          }
-      }
-
-      return serialized;
-  }
-
-
-  serializeProviderConfiguration(providers: Array<ProviderConfig>, format: string): string {
-    let serialized: string;
-
-    let tmp = {};
-    tmp['providers'] = providers;
-
-    switch(format) {
-        case 'json': {
-            serialized = JSON.stringify(tmp, null, 2);
-            break;
-        }
-        case 'yaml': {
-            let yaml = require('js-yaml');
-            serialized = '---\n' + yaml.dump(tmp);
-            break;
-        }
-    }
-
-    return serialized;
-  }
-
-
   discardChanges() {
     this.resourceService.selectedResource(this.resource);
   }

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/resource/resource.component.html
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/resource/resource.component.html b/gateway-admin-ui/src/app/resource/resource.component.html
index e964edf..5e41a3b 100644
--- a/gateway-admin-ui/src/app/resource/resource.component.html
+++ b/gateway-admin-ui/src/app/resource/resource.component.html
@@ -3,7 +3,22 @@
     <table class="table table-hover">
       <thead>
       <tr>
-        <th>{{ getResourceTypeSingularDisplayName(resourceType) }}</th>
+        <th>
+          <span>
+          {{ getResourceTypeSingularDisplayName(resourceType) }}
+          </span>
+          <span class="clickable inline-glyph glyphicon glyphicon-plus-sign btn btn-xs pull-right"
+                *ngIf="resourceType === 'Provider Configurations'"
+                (click)="newProviderConfigModal.open()"
+                title="Create New Provider Configuration"
+                data-toggle="tooltip"></span>
+          <span class="clickable inline-glyph glyphicon glyphicon-plus-sign btn btn-xs pull-right"
+                (click)="isAddingService=true"
+                *ngIf="resourceType === 'Descriptors'"
+                (click)="newDescriptorModal.open()"
+                title="Create New Descriptor"
+                data-toggle="tooltip"></span>
+        </th>
         <th *ngIf="resourceType === 'Topologies'">Timestamp</th>
       </tr>
       </thead>
@@ -19,4 +34,8 @@
       </tbody>
     </table>
   </div>
+  <div>
+    <app-new-desc-wizard #newDescriptorModal></app-new-desc-wizard>
+    <app-provider-config-wizard #newProviderConfigModal></app-provider-config-wizard>
+  </div>
 </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-admin-ui/src/app/resource/resource.service.ts
----------------------------------------------------------------------
diff --git a/gateway-admin-ui/src/app/resource/resource.service.ts b/gateway-admin-ui/src/app/resource/resource.service.ts
index 1b36823..88b8de5 100644
--- a/gateway-admin-ui/src/app/resource/resource.service.ts
+++ b/gateway-admin-ui/src/app/resource/resource.service.ts
@@ -20,6 +20,7 @@ import 'rxjs/add/operator/toPromise';
 import { Subject } from 'rxjs/Subject';
 import { Resource } from './resource';
 import {ProviderConfig} from "../resource-detail/provider-config";
+import {Descriptor} from "../resource-detail/descriptor";
 
 
 @Injectable()
@@ -136,7 +137,7 @@ export class ResourceService {
         headers = this.addCsrfHeaders(headers);
         //this.logHeaders(headers);
 
-        let url = ((resType === 'Descriptors') ? this.descriptorsUrl : this.providersUrl) + '/' + name;
+        let url = ((resType === 'Descriptors') ? this.descriptorsUrl : this.providersUrl) + '/' + resource.name;
         return this.http.put(url, content, {headers: headers})
                         .toPromise()
                         .then(() => content)
@@ -160,6 +161,83 @@ export class ResourceService {
             });
     }
 
+
+    serializeDescriptor(desc: Descriptor, format: string): string {
+        let serialized: string;
+
+        let tmp = {};
+        if (desc.discoveryAddress) {
+            tmp['discovery-address'] = desc.discoveryAddress;
+        }
+        if (desc.discoveryUser) {
+            tmp['discovery-user'] = desc.discoveryUser;
+        }
+        if (desc.discoveryPassAlias) {
+            tmp['discovery-pwd-alias'] = desc.discoveryPassAlias;
+        }
+        if (desc.discoveryCluster) {
+            tmp['cluster'] = desc.discoveryCluster;
+        }
+        tmp['provider-config-ref'] = desc.providerConfig;
+        tmp['services'] = desc.services;
+
+        switch(format) {
+            case 'json': {
+                serialized =
+                    JSON.stringify(tmp,
+                        (key, value) => {
+                            let result = value;
+                            switch(typeof value) {
+                                case 'string': // Don't serialize empty string value properties
+                                    result = (value.length > 0) ? value : undefined;
+                                    break;
+                                case 'object':
+                                    if (Array.isArray(value)) {
+                                        // Don't serialize empty array value properties
+                                        result = (value.length) > 0 ? value : undefined;
+                                    } else {
+                                        // Don't serialize object value properties
+                                        result = (Object.getOwnPropertyNames(value).length > 0) ? value : undefined;
+                                    }
+                                    break;
+                            }
+                            return result;
+                        }, 2);
+                break;
+            }
+            case 'yaml': {
+                let yaml = require('js-yaml');
+                serialized = '---\n' + yaml.safeDump(tmp);
+                break;
+            }
+        }
+
+        return serialized;
+    }
+
+
+    serializeProviderConfiguration(providers: Array<ProviderConfig>, format: string): string {
+        let serialized: string;
+
+        let tmp = {};
+        tmp['providers'] = providers;
+
+        switch(format) {
+            case 'json': {
+                serialized = JSON.stringify(tmp, null, 2);
+                break;
+            }
+            case 'yaml': {
+                let yaml = require('js-yaml');
+                serialized = '---\n' + yaml.dump(tmp);
+                break;
+            }
+        }
+
+        return serialized;
+    }
+
+
     addHeaders(headers: HttpHeaders, resName: string): HttpHeaders {
         let ext = resName.split('.').pop();
         switch(ext) {
@@ -200,6 +278,7 @@ export class ResourceService {
     }
 
     selectedResourceType(value: string) {
+        //console.debug('ResourceService --> selectedResourceType(\'' + value +'\')')
         this.selectedResourceTypeSource.next(value);
     }
 

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-applications/src/main/resources/applications/admin-ui/app/index.html
----------------------------------------------------------------------
diff --git a/gateway-applications/src/main/resources/applications/admin-ui/app/index.html b/gateway-applications/src/main/resources/applications/admin-ui/app/index.html
index 6f5a825..aae1ecd 100644
--- a/gateway-applications/src/main/resources/applications/admin-ui/app/index.html
+++ b/gateway-applications/src/main/resources/applications/admin-ui/app/index.html
@@ -11,4 +11,4 @@
   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.
---><!doctype html><html><head><meta charset="utf-8"><title>Apache Knox Manager</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"><meta name="viewport" content="width=device-width,initial-scale=1"><!-- Latest compiled and minified CSS --><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"><!-- Optional theme --><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"><!-- Custom styles for this template --><link href="assets/sticky-footer.css" rel="stylesheet"><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script><!-- Latest compiled and minified JavaScript --><scr
 ipt src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script><script src="assets/vkbeautify.js"></script><link href="styles.2ee5b7f4cd59a6cf015e.bundle.css" rel="stylesheet"/></head><body><div class="navbar-wrapper"><div class="container-fluid"><nav class="navbar navbar-inverse navbar-static-top"><div class="container-fluid"><div class="navbar-header"><button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"><span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span></button> <a class="navbar-brand" href="#"><img style="max-width:200px; margin-top: -9px;" src="assets/knox-logo-transparent.gif" alt="Apache Knox Manager"></a></div></div></nav></div><!-- Content --><resource-management></res
 ource-management><footer class="footer"><div>Knox Manager Version 0.1.0</div><gateway-version></gateway-version></footer><script type="text/javascript" src="inline.49efa231bf249ddc231a.bundle.js"></script><script type="text/javascript" src="scripts.c50bb762c438ae0f8842.bundle.js"></script><script type="text/javascript" src="main.511817c8d904b468f742.bundle.js"></script></div></body></html>
\ No newline at end of file
+--><!doctype html><html><head><meta charset="utf-8"><title>Apache Knox Manager</title><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" type="image/x-icon" href="favicon.ico"><meta name="viewport" content="width=device-width,initial-scale=1"><!-- Latest compiled and minified CSS --><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"><!-- Optional theme --><link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"><!-- Custom styles for this template --><link href="assets/sticky-footer.css" rel="stylesheet"><script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script><!-- Latest compiled and minified JavaScript --><scr
 ipt src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script><script src="assets/vkbeautify.js"></script><link href="styles.2ee5b7f4cd59a6cf015e.bundle.css" rel="stylesheet"/></head><body><div class="navbar-wrapper"><div class="container-fluid"><nav class="navbar navbar-inverse navbar-static-top"><div class="container-fluid"><div class="navbar-header"><button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"><span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span></button> <a class="navbar-brand" href="#"><img style="max-width:200px; margin-top: -9px;" src="assets/knox-logo-transparent.gif" alt="Apache Knox Manager"></a></div></div></nav></div><!-- Content --><resource-management></res
 ource-management><footer class="footer"><div>Knox Manager Version 0.1.0</div><gateway-version></gateway-version></footer><script type="text/javascript" src="inline.01f49f7d13670ad68dea.bundle.js"></script><script type="text/javascript" src="scripts.c50bb762c438ae0f8842.bundle.js"></script><script type="text/javascript" src="main.db638922a84cdef35de7.bundle.js"></script></div></body></html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-applications/src/main/resources/applications/admin-ui/app/inline.01f49f7d13670ad68dea.bundle.js
----------------------------------------------------------------------
diff --git a/gateway-applications/src/main/resources/applications/admin-ui/app/inline.01f49f7d13670ad68dea.bundle.js b/gateway-applications/src/main/resources/applications/admin-ui/app/inline.01f49f7d13670ad68dea.bundle.js
new file mode 100644
index 0000000..7cbd9f2
--- /dev/null
+++ b/gateway-applications/src/main/resources/applications/admin-ui/app/inline.01f49f7d13670ad68dea.bundle.js
@@ -0,0 +1 @@
+!function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,c,u){for(var a,i,f,l=0,s=[];l<r.length;l++)t[i=r[l]]&&s.push(t[i][0]),t[i]=0;for(a in c)Object.prototype.hasOwnProperty.call(c,a)&&(e[a]=c[a]);for(n&&n(r,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=o(o.s=u[l]);return f};var r={},t={2:0};function o(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.e=function(e){var n=t[e];if(0===n)return new Promise(function(e){e()});if(n)return n[2];var r=new Promise(function(r,o){n=t[e]=[r,o]});n[2]=r;var c=document.getElementsByTagName("head")[0],u=document.createElement("script");u.type="text/javascript",u.charset="utf-8",u.async=!0,u.timeout=12e4,o.nc&&u.setAttribute("nonce",o.nc),u.src=o.p+""+e+"."+{0:"db638922a84cdef35de7",1:"aed76669724804835353"}[e]+".chunk.js";var a=setTimeout(i,12e4);function i(){u.onerror=u.onload=null,clearTimeout(a);var n=t[e];0!==n&&(n&&n[1](new Error("Loading chu
 nk "+e+" failed.")),t[e]=void 0)}return u.onerror=u.onload=i,c.appendChild(u),r},o.m=e,o.c=r,o.d=function(e,n,r){o.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="",o.oe=function(e){throw console.error(e),e}}([]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/knox/blob/167053bd/gateway-applications/src/main/resources/applications/admin-ui/app/inline.49efa231bf249ddc231a.bundle.js
----------------------------------------------------------------------
diff --git a/gateway-applications/src/main/resources/applications/admin-ui/app/inline.49efa231bf249ddc231a.bundle.js b/gateway-applications/src/main/resources/applications/admin-ui/app/inline.49efa231bf249ddc231a.bundle.js
deleted file mode 100644
index 965479e..0000000
--- a/gateway-applications/src/main/resources/applications/admin-ui/app/inline.49efa231bf249ddc231a.bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,c,u){for(var a,i,f,l=0,s=[];l<r.length;l++)t[i=r[l]]&&s.push(t[i][0]),t[i]=0;for(a in c)Object.prototype.hasOwnProperty.call(c,a)&&(e[a]=c[a]);for(n&&n(r,c,u);s.length;)s.shift()();if(u)for(l=0;l<u.length;l++)f=o(o.s=u[l]);return f};var r={},t={2:0};function o(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,o),t.l=!0,t.exports}o.e=function(e){var n=t[e];if(0===n)return new Promise(function(e){e()});if(n)return n[2];var r=new Promise(function(r,o){n=t[e]=[r,o]});n[2]=r;var c=document.getElementsByTagName("head")[0],u=document.createElement("script");u.type="text/javascript",u.charset="utf-8",u.async=!0,u.timeout=12e4,o.nc&&u.setAttribute("nonce",o.nc),u.src=o.p+""+e+"."+{0:"511817c8d904b468f742",1:"aed76669724804835353"}[e]+".chunk.js";var a=setTimeout(i,12e4);function i(){u.onerror=u.onload=null,clearTimeout(a);var n=t[e];0!==n&&(n&&n[1](new Error("Loading chu
 nk "+e+" failed.")),t[e]=void 0)}return u.onerror=u.onload=i,c.appendChild(u),r},o.m=e,o.c=r,o.d=function(e,n,r){o.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,"a",n),n},o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.p="",o.oe=function(e){throw console.error(e),e}}([]);
\ No newline at end of file