Integrate EPSDK-FW library for auth and users 37/537/12
authorLott, Christopher (cl778h) <cl778h@att.com>
Wed, 17 Jul 2019 18:28:11 +0000 (14:28 -0400)
committerLott, Christopher (cl778h) <cl778h@att.com>
Mon, 29 Jul 2019 18:41:07 +0000 (14:41 -0400)
Change-Id: I8cba9e80e50b0e890783610d769e275091f942a7
Signed-off-by: Lott, Christopher (cl778h) <cl778h@att.com>
58 files changed:
README.md
docs/release-notes.rst
webapp-backend/.gitignore
webapp-backend/README.md
webapp-backend/config/.gitignore [new file with mode: 0644]
webapp-backend/config/key.properties.template [new file with mode: 0644]
webapp-backend/config/portal.properties.template [new file with mode: 0644]
webapp-backend/pom.xml
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AdminController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java [new file with mode: 0644]
webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java [new file with mode: 0644]
webapp-backend/src/main/resources/ESAPI.properties [new file with mode: 0644]
webapp-backend/src/main/resources/application.properties
webapp-backend/src/main/resources/logback.xml
webapp-backend/src/main/resources/validation.properties [new file with mode: 0644]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1MediatorMockConfiguration.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/A1MediatorMockConfiguration.java with 85% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AnrXappMockConfiguration.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AnrXappMockConfiguration.java with 85% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AppManagerMockConfiguration.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AppManagerMockConfiguration.java with 79% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/E2ManagerMockConfiguration.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/E2ManagerMockConfiguration.java with 84% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java [new file with mode: 0644]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AbstractControllerTest.java with 87% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AcXappControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AcXappControllerTest.java with 88% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AdminControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AdminControllerTest.java with 80% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AnrXappControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AnrXappControllerTest.java with 88% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AppManagerControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AppManagerControllerTest.java with 84% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/DefaultContextTest.java with 96% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/E2ManagerControllerTest.java [moved from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/E2ManagerControllerTest.java with 81% similarity]
webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java [new file with mode: 0644]
webapp-backend/src/test/resources/key.properties [new file with mode: 0644]
webapp-backend/src/test/resources/portal.properties [new file with mode: 0644]
webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts
webapp-frontend/src/app/app-control/app-control.component.ts
webapp-frontend/src/app/app-control/app-control.datasource.ts
webapp-frontend/src/app/catalog/catalog.component.ts
webapp-frontend/src/app/catalog/catalog.datasource.ts
webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html
webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts
webapp-frontend/src/app/ran-control/ran-control.component.ts
webapp-frontend/src/app/ran-control/ran-control.datasource.ts
webapp-frontend/src/app/services/stats/stats.service.ts
webapp-frontend/src/app/user/user.datasource.ts

index 3cec2d6..e70617f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,46 +1,39 @@
 # O-RAN-SC RIC Dashboard Web Application
 
-This webapp is built with Angular 7 and Spring-Boot 2.
+The O-RAN SC RIC Dashboard provides administrative and operator functions
+for a disaggregated radio access network (RAN) controller.
+The web app is built as a single-page app using an Angular 8 front end
+and a Spring-Boot 2 back end.
 
 ## Deployment configuration
 
-The application expects an application.properties file to be provided,
-probably mounted as a file from a Kubernetes configuration map, with
-the following content:
+The application expects the following configuration files,
+usually mounted as files from Kubernetes configuration maps:
 
-    # A1 Mediator
-    a1med.url = http://A1-URL
-    # ANR xApp
-    anrxapp.url = http://ANR-URL
-    # E2 Manager
-    e2mgr.url = http://E2-URL
-    # Xapp Manager
-    xappmgr.url = http://MGR-URL
+    application.properties (in launch directory)
+    key.properties (on classpath)
+    portal.properties (on classpath)
+
+Sample files are in directory src/main/resources and src/test/resources.
 
 ## Development guide
 
 This section gives a quickstart guide for developers.
 
-### Check prerequisites
+### Prerequisites
 
-1. Java development kit (JDK), version 1.8 or later
+1. Java development kit (JDK), version 11 or later
 2. Maven dependency-management tool, version 3.4 or later
 
 ### Build and launch the web app
 
-    mvn -Ddocker.skip=true clean install
-    cd webapp-backend
-    mvn spring-boot:run
-
-Then open a browser on http://localhost:8080
-
-In addition to the above, you can run the Angular server
-for debugging the frontend and backend separately:
-
-    cd webapp-frontend
-    ./ng serve --proxy-config proxy.conf.json
+Instructions for launching a backend Sprint-Boot server
+are available in the webapp-backend README file.
+After launching, open a browser on http://localhost:8080
 
-Then open a browser on http://localhost:4200
+Instructions for launching a frontend Angular server (only for development)
+are available in the webapp-frontend README file.
+After launching, open a browser on http://localhost:4200
 
 ## License
 
index b6eb3f2..1056039 100644 (file)
@@ -20,7 +20,7 @@
 RIC Dashboard Release Notes
 ===========================
 
-Version 1.2.0, 17 July 2019
+Version 1.2.0, 26 July 2019
 ---------------------------
 * Split URL properties into prefix/suffix parts
 * Add jacoco plugin to back-end for code coverage
@@ -29,6 +29,7 @@ Version 1.2.0, 17 July 2019
 * Drop mock RAN names feature that supported R1 testing
 * Extend mock endpoints to simulate delay seen in tests
 * Move mock configuration classes into test area
+* Add EPSDK-FW user management and Portal security
 
 Version 1.0.5, 5 July 2019
 --------------------------
index 0367d23..891095d 100644 (file)
@@ -30,3 +30,5 @@
 /build/
 
 /application-tlab2.properties
+/application.properties
+/users.json
index 8a142d7..6751ef9 100644 (file)
 # RIC Dashboard Web Application Backend
 
-## Launch server
+The RIC Dashboard back-end provides REST services to the Dashboard
+front-end Typescript features running in the user's browser.  For
+production use, this server also offers the Angular application files.
 
-Run `mvn -Dspring.config.name=application-abc spring-boot:run` to run a server configured
-by the file 'application-abc.properties' in the local directory.
+The server uses the ONAP Portal's "EPSDK-FW" library to support
+single-sign-on (SSO) feature, which requires users to authenticate
+at the ONAP Portal UI.  Authentication features including SSO are
+excluded by Spring profiles when running the back-end as a development
+server, see below.
 
-## Development server
+## Launch production server
 
-Set an environment variable via JVM argument "-Dorg.oransc.ric.portal.dashboard=mock"
-and run the JUnit test case DashboardServerTest for a development server to run standalone
-with mock configuration and data that simulates the behavior of remote endpoints.
+This server requires several configuration files:
+
+    application.properties (in launch directory)
+    key.properties (on Java classpath)
+    portal.properties (on Java classpath)
+
+These steps are required:
+
+1. Check the set of properties files in the config folder, and create
+   files from templates as needed.  E.g., copy
+   "key.properties.template" to "key.properties".
+2. Add the config folder to the Java classpath
+3a. Launch the server with this command-line invocation:
+
+    java -cp config:target/ric-dash-be-1.2.0-SNAPSHOT.jar \
+        -Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication \
+        org.springframework.boot.loader.PropertiesLauncher
+
+3b. To use the configuration in the "application-abc.properties" file, addd a
+key-value pair for "spring.config.name" and launch with an invocation like this:
+
+    java -cp config:target/ric-dash-be-1.2.0-SNAPSHOT.jar \
+        -Dspring.config.name=application-abc \
+        -Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication \
+        org.springframework.boot.loader.PropertiesLauncher
+
+### Production user authentication
+
+The regular server authenticates requests using cookies that are set
+by the ONAP Portal:
+
+     EPService
+     UserId
+
+The EPService value is not checked.  The UserId value is decrypted
+using a secret key shared with the ONAP Portal to yield a user ID.
+That ID must match a user's loginId defined in the user manager.
+
+The regular server checks requests for the following granted
+authorities (role names), as defined in the DashboardConstants class.
+A standard user can read (GET) all methods but not make changes.
+An administrator can read (GET) and write (POST PUT DELETE) all data.
+
+    Standard_User
+       System_Administrator
+
+Use the following structure in a JSON file to publish a user for the
+user manager:
+
+    [
+     {
+      "orgId":null,
+      "managerId":null,
+      "firstName":"Demo",
+      "middleInitial":null,
+      "lastName":"User",
+      "phone":null,
+      "email":null,
+      "hrid":null,
+      "orgUserId":null,
+      "orgCode":null,
+      "orgManagerUserId":null,
+      "jobTitle":null,
+      "loginId":"demo",
+      "active":true,
+      "roles":[
+         {
+            "id":null,
+            "name":"Standard_User",
+            "roleFunctions":null
+         }
+      ]
+     }
+    ]
+
+
+## Launch development server
+
+The development server uses local configuration and serves mock data
+that simulates the behavior of remote endpoints.  The directory
+src/main/resources contains usable versions of the required property
+files.  These steps are required to launch:
+
+1. Set an environment variable via JVM argument: "-Dorg.oransc.ric.portal.dashboard=mock"
+2. Run the JUnit test case DashboardServerTest -- not exactly a "test" because it never finishes.
+
+Both steps can be done with this command-line invocation:
+
+     mvn -Dorg.oransc.ric.portal.dashboard=mock -Dtest=DashboardTestServer test
+
+### Development user authentication
+
+The development server requires basic HTTP user authentication for all requests. Like
+the production server, it requires HTTP headers with authentication for Portal API
+requests.  The credentials are in constants in this Java class in the src/test/java
+folder:
+
+    org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration
+
+Like the production server, the development server also performs role-based
+authentication on requests. The user name-role  name associations are defined
+in the class shown above.
 
 ## Swagger API documentation
 
-View the server's API documentation at URL `http://localhost:8080/swagger-ui.html`.
+Both a regular and a development server publish API documentation at URL `http://localhost:8080/swagger-ui.html`.
 
 ## License
 
diff --git a/webapp-backend/config/.gitignore b/webapp-backend/config/.gitignore
new file mode 100644 (file)
index 0000000..edd66f1
--- /dev/null
@@ -0,0 +1,2 @@
+/key.properties
+/portal.properties
diff --git a/webapp-backend/config/key.properties.template b/webapp-backend/config/key.properties.template
new file mode 100644 (file)
index 0000000..6ba89b1
--- /dev/null
@@ -0,0 +1,21 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+# Template for the file that provides a secret key for the RIC Dashboard.
+
+cipher.enc.key =
diff --git a/webapp-backend/config/portal.properties.template b/webapp-backend/config/portal.properties.template
new file mode 100644 (file)
index 0000000..601793c
--- /dev/null
@@ -0,0 +1,34 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+# Template for the file that provides properties for the EPSDK-FW library.
+# This file must be present on the Java classpath.
+
+# The following properties are the same in every deployment
+
+portal.api.impl.class = org.oransc.ric.portal.dashboard.portalapi.PortalRestCentralServiceImpl
+role_access_centralized = remote
+
+# The following properties are DIFFERENT in every deployment
+
+# URL of portal login screen
+ecomp_redirect_url = http://localhost/portal
+# URL of portal API
+ecomp_rest_url = http://localhost/portal
+# Value assigned by portal instance
+ueb_app_key = abcdef1234567890
index e96777f..40db771 100644 (file)
@@ -34,6 +34,13 @@ limitations under the License.
                <!-- Set by Jenkins -->
                <build.number>0</build.number>
        </properties>
+       <repositories>
+               <repository>
+                       <id>onap-releases</id>
+                       <name>ONAP - Release Repository</name>
+                       <url>https://nexus.onap.org/content/repositories/releases</url>
+               </repository>
+       </repositories>
        <dependencies>
                <!-- xApps -->
                <dependency>
@@ -57,6 +64,55 @@ limitations under the License.
                        <artifactId>e2-mgr-client</artifactId>
                        <version>20190703-SNAPSHOT</version>
                </dependency>
+               <dependency>
+                       <groupId>org.onap.portal.sdk</groupId>
+                       <artifactId>epsdk-fw</artifactId>
+                       <version>2.4.0</version>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>commons-logging</groupId>
+                                       <artifactId>commons-logging</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>log4j</groupId>
+                                       <artifactId>log4j</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>log4j</groupId>
+                                       <artifactId>apache-log4j-extras</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.slf4j</groupId>
+                                       <artifactId>slf4j-log4j12</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>junit</groupId>
+                                       <artifactId>junit</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>commons-fileupload</groupId>
+                                       <artifactId>commons-fileupload</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>commons-beanutils</groupId>
+                                       <artifactId>commons-beanutils</artifactId>
+                               </exclusion>
+                               <!-- EELF omits "test" scope on this dependency -->
+                               <exclusion>
+                                       <groupId>org.powermock</groupId>
+                                       <artifactId>powermock-module-junit4</artifactId>
+                               </exclusion>
+                               <!-- EELF omits "test" scope on this dependency -->
+                               <exclusion>
+                                       <groupId>org.powermock</groupId>
+                                       <artifactId>powermock-api-mockito</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+               <dependency>
+                       <groupId>org.springframework.boot</groupId>
+                       <artifactId>spring-boot-starter-security</artifactId>
+               </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
@@ -65,6 +121,11 @@ limitations under the License.
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
                </dependency>
+               <!-- Bridge uses of Apache commons logging, like EPSDK-FW -->
+               <dependency>
+                       <groupId>org.slf4j</groupId>
+                       <artifactId>jcl-over-slf4j</artifactId>
+               </dependency>
                <dependency>
                        <groupId>ch.qos.logback</groupId>
                        <artifactId>logback-classic</artifactId>
@@ -230,12 +291,21 @@ limitations under the License.
                                                                </assembly>
                                                                <runCmds>
                                                                        <!-- Ensure logs dir exists and is world writable -->
-                                                                       <runCmd>mkdir /maven/logs</runCmd>
-                                                                       <runCmd>chmod -R 777 /maven</runCmd>
+                                                                       <runCmd>mkdir /logs</runCmd>
+                                                                       <runCmd>chmod -R 777 /logs</runCmd>
                                                                </runCmds>
                                                                <cmd>
-                                                                       <!-- CDATA prevents Eclipse formatter from breaking line -->
-                                                                       <shell><![CDATA[cd /maven; java -Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom -jar ${project.artifactId}-${project.version}.${project.packaging}]]></shell>
+                                                                       <!-- Include maven dir on classpath for prop files -->
+                                                                       <exec>
+                                                                               <arg>java</arg>
+                                                                               <arg>-cp</arg>
+                                                                               <arg>maven:maven/${project.artifactId}-${project.version}.${project.packaging}</arg>
+                                                                               <arg>-Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication</arg>
+                                                                               <arg>-Xms128m</arg>
+                                                                               <arg>-Xmx256m</arg>
+                                                                               <arg>-Djava.security.egd=file:/dev/./urandom</arg>
+                                                                               <arg>org.springframework.boot.loader.PropertiesLauncher</arg>
+                                                                       </exec>
                                                                </cmd>
                                                        </build>
                                                </image>
index 05778dc..4819e34 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.oransc.ric.portal.dashboard;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.lang.invoke.MethodHandles;
 
 import org.slf4j.Logger;
@@ -34,8 +36,19 @@ public class DashboardApplication {
 
        private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-       public static void main(String[] args) {
+       // Unfortunately these names are not available as constants
+       private static final String[] propertyFiles = { "ESAPI.properties", "key.properties", "portal.properties",
+                       "validation.properties" };
+
+       public static void main(String[] args) throws IOException {
                SpringApplication.run(DashboardApplication.class, args);
+               for (String pf : propertyFiles) {
+                       InputStream in = MethodHandles.lookup().lookupClass().getClassLoader().getResourceAsStream(pf);
+                       if (in == null)
+                               logger.warn("Failed to find property file on classpath: {}", pf);
+                       else
+                               in.close();
+               }
                // Force this onto the console by using level WARN
                logger.warn("main: version '{}' successful start",
                                getImplementationVersion(MethodHandles.lookup().lookupClass()));
index 9b80864..bb093cd 100644 (file)
@@ -27,7 +27,18 @@ public abstract class DashboardConstants {
 
        public static final String ENDPOINT_PREFIX = "/api";
 
+       public static final String LOGIN_PAGE = "/login.html";
+
        // Factor out method names used in multiple controllers
        public static final String VERSION_METHOD = "version";
 
+       // The role names are defined by ONAP Portal.
+       // The prefix "ROLE_" is required by Spring.
+       // These are used in Java code annotations that require constants.
+       public static final String ROLE_NAME_STANDARD = "Standard_User";
+       public static final String ROLE_NAME_ADMIN = "System_Administrator";
+       private static final String ROLE_PREFIX = "ROLE_";
+       public static final String ROLE_ADMIN = ROLE_PREFIX + ROLE_NAME_ADMIN;
+       public static final String ROLE_STANDARD = ROLE_PREFIX + ROLE_NAME_STANDARD;
+
 }
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java
new file mode 100644 (file)
index 0000000..fe58e93
--- /dev/null
@@ -0,0 +1,116 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.URLEncoder;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.onap.portalsdk.core.onboarding.util.PortalApiProperties;
+import org.oransc.ric.portal.dashboard.portalapi.PortalAuthenticationFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+
+/**
+ * Serves a login page that contains a link from configuration to ONAP Portal.
+ * This avoids the immediate redirect to Portal that is confusing to users and
+ * infuriating to developers.
+ * 
+ * Basically this is do-it-yourself JSP :)
+ */
+public class LoginServlet extends HttpServlet {
+
+       private static final long serialVersionUID = 1191385178190976568L;
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       @Override
+       public void init(ServletConfig servletConfig) throws ServletException {
+               logger.debug("init");
+               super.init(servletConfig);
+               final String portalURL = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
+               if (portalURL == null || portalURL.length() == 0)
+                       throw new ServletException("Failed to get property " + PortalApiConstants.ECOMP_REDIRECT_URL);
+       }
+
+       @Override
+       protected void doGet(HttpServletRequest request, HttpServletResponse response)
+                       throws IOException, ServletException {
+               logger.debug("doGet {}", request.getRequestURI());
+               // The original page URL should arrive as a query parameter
+               String appUrl = request.getParameter(PortalAuthenticationFilter.REDIRECT_URL_KEY);
+               // If a user bookmarks the login page, then nothing arrives;
+               // use the original URL without the login suffix.
+               if (appUrl == null || appUrl.isEmpty()) {
+                       String loginUrl = request.getRequestURL().toString();
+                       int indexOfLogin = loginUrl.indexOf(DashboardConstants.LOGIN_PAGE);
+                       appUrl = loginUrl.substring(0, indexOfLogin);
+               }
+               String encodedAppUrl = URLEncoder.encode(appUrl, "UTF-8");
+               String portalBaseUrl = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
+               String redirectUrl = portalBaseUrl + "?" + PortalAuthenticationFilter.REDIRECT_URL_KEY + "=" + encodedAppUrl;
+               String aHref = "<a href=\"" + redirectUrl + "\">";
+               // If only Java had "here" documents.
+               String body = String.join(//
+                               System.getProperty("line.separator"), //
+                               "<html>", //
+                               "<head>", //
+                               "<title>RIC Dashboard</title>", //
+                               "<style>", //
+                               "html, body { ", //
+                               "  font-family: Helvetica, Arial, sans-serif;", //
+                               "}", //
+                               "</style>", //
+                               "</head>", //
+                               "<body>", //
+                               "<h2>RIC Dashboard</h2>", //
+                               "<h4>Please log in.</h4>", //
+                               "<p>", //
+                               aHref, "Click here to authenticate at the ONAP Portal</a>", //
+                               "</p>", //
+                               "</body>", //
+                               "</html>");
+               writeAndFlush(response, MediaType.TEXT_HTML_VALUE, body);
+       }
+
+       /**
+        * Sets the content type and writes the response.
+        * 
+        * @param response
+        * @param contentType
+        * @param responseBody
+        * @throws IOException
+        */
+       private void writeAndFlush(HttpServletResponse response, String contentType, String responseBody)
+                       throws IOException {
+               response.setContentType(contentType);
+               response.getWriter().print(responseBody);
+               response.getWriter().flush();
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java
new file mode 100644 (file)
index 0000000..2f1d1f6
--- /dev/null
@@ -0,0 +1,44 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.config;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * Allows non-Spring-managed classes to obtain the Spring application context.
+ */
+@Component
+public class SpringContextCache implements ApplicationContextAware {
+
+       private static ApplicationContext applicationContext = null;
+
+       @Override
+       public void setApplicationContext(final ApplicationContext appContext) throws BeansException {
+               applicationContext = appContext;
+       }
+
+       public static ApplicationContext getApplicationContext() {
+               return applicationContext;
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java
new file mode 100644 (file)
index 0000000..9357a1c
--- /dev/null
@@ -0,0 +1,172 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.config;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+
+import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.LoginServlet;
+import org.oransc.ric.portal.dashboard.controller.AcXappController;
+import org.oransc.ric.portal.dashboard.controller.AdminController;
+import org.oransc.ric.portal.dashboard.controller.AnrXappController;
+import org.oransc.ric.portal.dashboard.controller.AppManagerController;
+import org.oransc.ric.portal.dashboard.controller.E2ManagerController;
+import org.oransc.ric.portal.dashboard.controller.SimpleErrorController;
+import org.oransc.ric.portal.dashboard.portalapi.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.portalapi.PortalAuthManager;
+import org.oransc.ric.portal.dashboard.portalapi.PortalAuthenticationFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(securedEnabled = true)
+@Profile("!test")
+public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       // Although constructor arguments are recommended over field injection,
+       // this results in fewer lines of code.
+       @Value("${userfile}")
+       private String userFilePath;
+       @Value("${portalapi.appname}")
+       private String appName;
+       @Value("${portalapi.username}")
+       private String userName;
+       @Value("${portalapi.password}")
+       private String password;
+       @Value("${portalapi.decryptor}")
+       private String decryptor;
+       @Value("${portalapi.usercookie}")
+       private String userCookie;
+
+       protected void configure(HttpSecurity http) throws Exception {
+               logger.debug("configure");
+               // A chain of ".and()" always baffles me
+               http.authorizeRequests().anyRequest().authenticated();
+               // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
+               http.addFilterBefore(portalAuthenticationFilterBean(), BasicAuthenticationFilter.class);
+       }
+
+       /**
+        * Resource paths that do not require authentication, especially including
+        * Swagger-generated documentation.
+        */
+       public static final String[] OPEN_PATHS = { //
+                       "/v2/api-docs", //
+                       "/swagger-resources/**", //
+                       "/swagger-ui.html", //
+                       "/webjars/**", //
+                       PortalApiConstants.API_PREFIX + "/**", //
+                       AcXappController.CONTROLLER_PATH + "/" + AcXappController.VERSION_METHOD, //
+                       AdminController.CONTROLLER_PATH + "/" + AdminController.HEALTH_METHOD, //
+                       AdminController.CONTROLLER_PATH + "/" + AdminController.VERSION_METHOD, //
+                       AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.HEALTH_ALIVE_METHOD, //
+                       AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.HEALTH_READY_METHOD, //
+                       AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.VERSION_METHOD, //
+                       AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.HEALTH_ALIVE_METHOD, //
+                       AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.HEALTH_READY_METHOD, //
+                       AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.VERSION_METHOD, //
+                       E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.HEALTH_METHOD, //
+                       E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.VERSION_METHOD, //
+                       DashboardConstants.LOGIN_PAGE, //
+                       SimpleErrorController.ERROR_PATH };
+
+       @Override
+       public void configure(WebSecurity web) throws Exception {
+               // This disables Spring security, but not the app's filter.
+               web.ignoring().antMatchers(OPEN_PATHS);
+       }
+
+       @Bean
+       public PortalAuthManager portalAuthManagerBean()
+                       throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
+               return new PortalAuthManager(appName, userName, password, decryptor, userCookie);
+       }
+
+       @Bean
+       public DashboardUserManager dashboardUserManagerBean() throws IOException {
+               return new DashboardUserManager(userFilePath);
+       }
+
+       /*
+        * If this is annotated with @Bean, it is created automatically AND REGISTERED,
+        * and Spring processes annotations in the source of the class. However, the
+        * filter is added in the chain apparently in the wrong order. Alternately, with
+        * no @Bean and added to the chain up in the configure() method in the desired
+        * order, the ignoring() matcher pattern configured above causes Spring to
+        * bypass this filter, which seems to me means the filter participates
+        * correctly.
+        */
+       public PortalAuthenticationFilter portalAuthenticationFilterBean()
+                       throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {
+               PortalAuthenticationFilter portalAuthenticationFilter = new PortalAuthenticationFilter(portalAuthManagerBean(),
+                               dashboardUserManagerBean());
+               return portalAuthenticationFilter;
+       }
+
+       /**
+        * Instantiates the EPSDK-FW servlet. Needed because this app is not configured
+        * to scan the EPSDK-FW packages; there's also a chance that Spring-Boot does
+        * not automatically process @WebServlet annotations.
+        * 
+        * @return Servlet registration bean for the Portal Rest API proxy servlet.
+        */
+       @Bean
+       public ServletRegistrationBean<PortalRestAPIProxy> portalApiProxyServletBean() {
+               PortalRestAPIProxy servlet = new PortalRestAPIProxy();
+               final ServletRegistrationBean<PortalRestAPIProxy> servletBean = new ServletRegistrationBean<>(servlet,
+                               PortalApiConstants.API_PREFIX + "/*");
+               servletBean.setName("PortalRestApiProxyServlet");
+               return servletBean;
+       }
+
+       /**
+        * Instantiates a trivial login servlet that serves a basic page with a link to
+        * authenticate at Portal. The login filter redirects to this page instead of
+        * Portal.
+        * 
+        * @return Servlet registration bean for the Dashboard login servlet.
+        */
+       @Bean
+       public ServletRegistrationBean<LoginServlet> loginServletBean() {
+               LoginServlet servlet = new LoginServlet();
+               final ServletRegistrationBean<LoginServlet> servletBean = new ServletRegistrationBean<>(servlet,
+                               DashboardConstants.LOGIN_PAGE);
+               servletBean.setName("LoginServlet");
+               return servletBean;
+       }
+
+}
index cdb99b0..655b47a 100644 (file)
@@ -31,6 +31,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PutMapping;
@@ -77,6 +78,7 @@ public class AcXappController {
 
        @ApiOperation(value = "Gets the A1 client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class)
        @GetMapping(VERSION_METHOD)
+       // No role required
        public SuccessTransport getA1MediatorClientVersion() {
                return new SuccessTransport(200, DashboardApplication.getImplementationVersion(A1MediatorApi.class));
        }
@@ -86,6 +88,7 @@ public class AcXappController {
         */
        @ApiOperation(value = "Gets the admission control policy for AC xApp via the A1 Mediator")
        @GetMapping(ADMCTRL_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public Object getAdmissionControlPolicy(HttpServletResponse response) {
                logger.debug("getAdmissionControlPolicy");
                response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
@@ -98,6 +101,7 @@ public class AcXappController {
         */
        @ApiOperation(value = "Sets the admission control policy for AC xApp via the A1 Mediator")
        @PutMapping(ADMCTRL_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void setAdmissionControlPolicy(@ApiParam(value = "Admission control policy") @RequestBody JsonNode acPolicy, //
                        HttpServletResponse response) {
                logger.debug("setAdmissionControlPolicy {}", acPolicy);
index 86a7700..6f28254 100644 (file)
@@ -28,6 +28,7 @@ import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.MediaType;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
@@ -66,6 +67,7 @@ public class AdminController {
 
        @ApiOperation(value = "Gets the Dashboard MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class)
        @GetMapping(VERSION_METHOD)
+       // No role required
        public SuccessTransport getVersion() {
                logger.debug("getVersion");
                return new SuccessTransport(200,
@@ -74,6 +76,7 @@ public class AdminController {
 
        @ApiOperation(value = "Checks the health of the application.", response = SuccessTransport.class)
        @GetMapping(HEALTH_METHOD)
+       // No role required
        public SuccessTransport getHealth() {
                logger.debug("getHealth");
                return new SuccessTransport(200, "Dashboard is healthy!");
@@ -81,6 +84,7 @@ public class AdminController {
 
        @ApiOperation(value = "Gets the list of application users.", response = DashboardUser.class, responseContainer = "List")
        @GetMapping(USER_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public DashboardUser[] getUsers() {
                logger.debug("getUsers");
                return users;
index c6a6b90..55b4212 100644 (file)
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -93,12 +94,14 @@ public class AnrXappController {
 
        @ApiOperation(value = "Gets the ANR client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class)
        @GetMapping(VERSION_METHOD)
+       // No role required
        public SuccessTransport getClientVersion() {
                return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthApi.class));
        }
 
        @ApiOperation(value = "Performs a liveness probe on the ANR xApp, result expressed as the response code.")
        @GetMapping(HEALTH_ALIVE_METHOD)
+       // No role required
        public void getHealthAlive(HttpServletResponse response) {
                logger.debug("getHealthAlive");
                healthApi.getHealthAlive();
@@ -107,6 +110,7 @@ public class AnrXappController {
 
        @ApiOperation(value = "Performs a readiness probe on the ANR xApp, result expressed as the response code.")
        @GetMapping(HEALTH_READY_METHOD)
+       // No role required
        public void getHealthReady(HttpServletResponse response) {
                logger.debug("getHealthReady");
                healthApi.getHealthReady();
@@ -115,6 +119,7 @@ public class AnrXappController {
 
        @ApiOperation(value = "Returns list of gNodeB IDs based on NCRT in ANR", response = GgNodeBTable.class)
        @GetMapping(GNODEBS_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public GgNodeBTable getGnodebs() {
                logger.debug("getGnodebs");
                return ncrtApi.getgNodeB();
@@ -122,6 +127,7 @@ public class AnrXappController {
 
        @ApiOperation(value = "Returns neighbor cell relation table for all gNodeBs or based on query parameters", response = NeighborCellRelationTable.class)
        @GetMapping(NCRT_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public NeighborCellRelationTable getNcrt( //
                        @RequestParam(name = QP_NODEB, required = false) String ggnbId, //
                        @RequestParam(name = QP_SERVING, required = false) String servingCellNrcgi, //
@@ -134,6 +140,7 @@ public class AnrXappController {
        // /ncrt/servingcells/{servCellNrcgi}/neighborcells/{neighCellNrpci} :
        @ApiOperation(value = "Modify neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI")
        @PutMapping(NCRT_METHOD + "/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void modifyNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, //
                        @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, //
                        @RequestBody NeighborCellRelationMod ncrMod, HttpServletResponse response) {
@@ -145,6 +152,7 @@ public class AnrXappController {
 
        @ApiOperation(value = "Delete neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI")
        @DeleteMapping(NCRT_METHOD + "/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void deleteNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, //
                        @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, //
                        HttpServletResponse response) {
index 40e03db..f71c6bd 100644 (file)
@@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -95,12 +96,14 @@ public class AppManagerController {
 
        @ApiOperation(value = "Gets the XApp manager client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class)
        @GetMapping(VERSION_METHOD)
+       // No role required
        public SuccessTransport getClientVersion() {
                return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthApi.class));
        }
 
        @ApiOperation(value = "Health check of xApp Manager - Liveness probe.")
        @GetMapping(HEALTH_ALIVE_METHOD)
+       // No role required
        public void getHealth(HttpServletResponse response) {
                logger.debug("getHealthAlive");
                healthApi.getHealthAlive();
@@ -109,6 +112,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Readiness check of xApp Manager - Readiness probe.")
        @GetMapping(HEALTH_READY_METHOD)
+       // No role required
        public void getHealthReady(HttpServletResponse response) {
                logger.debug("getHealthReady");
                healthApi.getHealthReady();
@@ -117,6 +121,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Returns the configuration of all xapps.", response = AllXappConfig.class)
        @GetMapping(CONFIG_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public AllXappConfig getAllXappConfig() {
                logger.debug("getAllXappConfig");
                return xappApi.getAllXappConfig();
@@ -124,6 +129,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Create xApp config.", response = XAppConfig.class)
        @PostMapping(CONFIG_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public XAppConfig createXappConfig(@RequestBody XAppConfig xAppConfig) {
                logger.debug("createXappConfig {}", xAppConfig);
                return xappApi.createXappConfig(xAppConfig);
@@ -131,6 +137,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Modify xApp config.", response = XAppConfig.class)
        @PutMapping(CONFIG_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public XAppConfig modifyXappConfig(@RequestBody XAppConfig xAppConfig) {
                logger.debug("modifyXappConfig {}", xAppConfig);
                return xappApi.modifyXappConfig(xAppConfig);
@@ -138,6 +145,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Delete xApp configuration.")
        @DeleteMapping(CONFIG_METHOD + "/{" + PP_XAPP_NAME + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void deleteXappConfig(@RequestBody ConfigMetadata configMetadata, HttpServletResponse response) {
                logger.debug("deleteXappConfig {}", configMetadata);
                xappApi.deleteXappConfig(configMetadata);
@@ -146,6 +154,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Returns a list of deployable xapps.", response = DashboardDeployableXapps.class)
        @GetMapping(XAPPS_LIST_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public Object getAvailableXapps() {
                logger.debug("getAvailableXapps");
                AllDeployableXapps appNames = xappApi.listAllXapps();
@@ -160,6 +169,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Returns the status of all deployed xapps.", response = AllDeployedXapps.class)
        @GetMapping(XAPPS_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public AllDeployedXapps getDeployedXapps() {
                logger.debug("getDeployedXapps");
                return xappApi.getAllXapps();
@@ -167,6 +177,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Returns the status of a given xapp.", response = Xapp.class)
        @GetMapping(XAPPS_METHOD + "/{" + PP_XAPP_NAME + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public Xapp getXapp(@PathVariable("xAppName") String xAppName) {
                logger.debug("getXapp {}", xAppName);
                return xappApi.getXappByName(xAppName);
@@ -174,6 +185,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Deploy a xapp.", response = Xapp.class)
        @PostMapping(XAPPS_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public Xapp deployXapp(@RequestBody XAppInfo xAppInfo) {
                logger.debug("deployXapp {}", xAppInfo);
                return xappApi.deployXapp(xAppInfo);
@@ -181,6 +193,7 @@ public class AppManagerController {
 
        @ApiOperation(value = "Undeploy an existing xapp.")
        @DeleteMapping(XAPPS_METHOD + "/{" + PP_XAPP_NAME + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void undeployXapp(@PathVariable("xAppName") String xAppName, HttpServletResponse response) {
                logger.debug("undeployXapp {}", xAppName);
                xappApi.undeployXapp(xAppName);
index 062d04f..b1ac2e8 100644 (file)
@@ -24,8 +24,10 @@ import java.lang.invoke.MethodHandles;
 import org.oransc.ric.portal.dashboard.model.ErrorTransport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.client.HttpStatusCodeException;
@@ -55,25 +57,45 @@ public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptio
        // Superclass has "logger" that is exposed here, so use a different name
        private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
-       /**
+       /*
         * Generates the response when a REST controller method takes an
-        * HttpStatusCodeException. Confusingly, the container first redirects to /error
-        * which invokes the
-        * {@link org.oransc.ric.portal.dashboard.controller.SimpleErrorController}
-        * method, and that response arrives here as the response body.
+        * HttpStatusCodeException.
+        * 
+        * It appears that the container internally redirects to /error because the web
+        * request that arrives here has URI /error, and {@link
+        * org.oransc.ric.portal.dashboard.controller.SimpleErrorController} runs before
+        * this.
+        * 
+        * @param ex The exception
+        * 
+        * @param request The original request
         * 
-        * @param ex
-        *                    The exception
-        * @param request
-        *                    The orignal request
         * @return A response entity with status code 502 plus some details in the body.
         */
        @ExceptionHandler(HttpStatusCodeException.class)
        public final ResponseEntity<ErrorTransport> handleHttpStatusCodeException(HttpStatusCodeException ex,
                        WebRequest request) {
-               log.warn("Request {} failed, status code {}", request.getDescription(false), ex.getStatusCode());
+               log.warn("handleHttpStatusCodeException: request {}, status code {}", request.getDescription(false),
+                               ex.getStatusCode());
                return new ResponseEntity<>(new ErrorTransport(ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex),
                                HttpStatus.BAD_GATEWAY);
        }
 
+       /*
+        * This exception also happens when Spring security denies access to a method
+        * due to missing/wrong roles (granted authorities). Override the method to
+        * answer permission denied, even though that may obscure a genuine developer
+        * error.
+        * 
+        * The web request that arrives here has URI /error; how to obtain the URI of
+        * the original request?!?
+        */
+       @Override
+       public final ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
+                       HttpHeaders headers, HttpStatus status, WebRequest request) {
+               log.warn("handleHttpRequestMethodNotSupported: answering 'permission denied' for method {}", ex.getMethod());
+               return new ResponseEntity<Object>(new ErrorTransport(HttpStatus.UNAUTHORIZED.value(),
+                               "Permission denied for method " + ex.getMethod(), ex), HttpStatus.UNAUTHORIZED);
+       }
+
 }
index e252398..b200c9a 100644 (file)
@@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
+import org.springframework.security.access.annotation.Secured;
 import org.springframework.util.Assert;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -91,12 +92,14 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Gets the E2 manager client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class)
        @GetMapping(VERSION_METHOD)
+       // No role required
        public SuccessTransport getClientVersion() {
                return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthCheckApi.class));
        }
 
        @ApiOperation(value = "Gets the health from the E2 manager, expressed as the response code.")
        @GetMapping(HEALTH_METHOD)
+       // No role required
        public void healthGet(HttpServletResponse response) {
                logger.debug("healthGet");
                e2HealthCheckApi.healthGet();
@@ -106,6 +109,7 @@ public class E2ManagerController {
        // This calls other methods to simplify the task of the front-end.
        @ApiOperation(value = "Gets all RAN identities and statuses from the E2 manager.", response = RanDetailsTransport.class, responseContainer = "List")
        @GetMapping(RAN_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public List<RanDetailsTransport> getRanDetails() {
                logger.debug("getRanDetails");
                List<NodebIdentity> nodebIdList = e2NodebApi.getNodebIdList();
@@ -127,6 +131,7 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Get RAN identities list.", response = NodebIdentity.class, responseContainer = "List")
        @GetMapping(NODEB_LIST_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public List<NodebIdentity> getNodebIdList() {
                logger.debug("getNodebIdList");
                return e2NodebApi.getNodebIdList();
@@ -134,6 +139,7 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Get RAN by name.", response = GetNodebResponse.class)
        @GetMapping(NODEB_METHOD + "/{" + PP_RANNAME + "}")
+       @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
        public GetNodebResponse getNb(@PathVariable(PP_RANNAME) String ranName) {
                logger.debug("getNb {}", ranName);
                return e2NodebApi.getNb(ranName);
@@ -141,6 +147,7 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Close all connections to the RANs and delete the data from the nodeb-rnib DB.")
        @DeleteMapping(NODEB_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void nodebDelete(HttpServletResponse response) {
                logger.debug("nodebDelete");
                e2NodebApi.nodebDelete();
@@ -149,6 +156,7 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Sets up an EN-DC RAN connection via the E2 manager.")
        @PostMapping(ENDC_SETUP_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void endcSetup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) {
                logger.debug("endcSetup {}", setupRequest);
                e2NodebApi.endcSetup(setupRequest);
@@ -157,6 +165,7 @@ public class E2ManagerController {
 
        @ApiOperation(value = "Sets up an X2 RAN connection via the E2 manager.")
        @PostMapping(X2_SETUP_METHOD)
+       @Secured({ DashboardConstants.ROLE_ADMIN })
        public void x2Setup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) {
                logger.debug("x2Setup {}", setupRequest);
                e2NodebApi.x2Setup(setupRequest);
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java
new file mode 100644 (file)
index 0000000..8091983
--- /dev/null
@@ -0,0 +1,85 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import org.onap.portalsdk.core.restful.domain.EcompRole;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class EcompUserDetails implements UserDetails {
+
+       private static final long serialVersionUID = 1L;
+       private final EcompUser ecompUser;
+
+       // This is the default Spring role-name prefix.
+       private static final String ROLEP = "ROLE_";
+
+       public EcompUserDetails(EcompUser ecompUser) {
+               this.ecompUser = ecompUser;
+       }
+
+       /*
+        * Gets a list of authorities (roles) for this user. To keep Spring happy, every
+        * item has prefix ROLE_.
+        */
+       public Collection<? extends GrantedAuthority> getAuthorities() {
+               List<GrantedAuthority> roleList = new ArrayList<>();
+               Iterator<EcompRole> roleIter = ecompUser.getRoles().iterator();
+               while (roleIter.hasNext()) {
+                       EcompRole role = roleIter.next();
+                       // Add the prefix if the ONAP portal doesn't supply it.
+                       final String roleName = role.getName().startsWith(ROLEP) ? role.getName() : ROLEP + role.getName();
+                       roleList.add(new SimpleGrantedAuthority(roleName));
+               }
+               return roleList;
+       }
+
+       public String getPassword() {
+               return null;
+       }
+
+       public String getUsername() {
+               return ecompUser.getLoginId();
+       }
+
+       public boolean isAccountNonExpired() {
+               return true;
+       }
+
+       public boolean isAccountNonLocked() {
+               return true;
+       }
+
+       public boolean isCredentialsNonExpired() {
+               return true;
+       }
+
+       public boolean isEnabled() {
+               return ecompUser.isActive();
+       }
+
+}
index 035b9ef..2d3a5c2 100644 (file)
@@ -26,7 +26,7 @@ package org.oransc.ric.portal.dashboard.model;
 public class ErrorTransport implements IDashboardResponse {
 
        private Integer status;
-       private String error;
+       private String message;
        private String exception;
 
        /**
@@ -62,7 +62,7 @@ public class ErrorTransport implements IDashboardResponse {
         */
        public ErrorTransport(int statusCode, String errMsg, Exception exception) {
                this.status = statusCode;
-               this.error = errMsg;
+               this.message = errMsg;
                if (exception != null) {
                        final int enough = 512;
                        String exString = exception.toString();
@@ -79,12 +79,12 @@ public class ErrorTransport implements IDashboardResponse {
                this.status = status;
        }
 
-       public String getError() {
-               return error;
+       public String getMessage() {
+               return message;
        }
 
-       public void setError(String error) {
-               this.error = error;
+       public void setMessage(String error) {
+               this.message = error;
        }
 
        public String getException() {
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java
new file mode 100644 (file)
index 0000000..b02d026
--- /dev/null
@@ -0,0 +1,122 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Provides user-management services.
+ * 
+ * This first implementation serializes user details to a file. TODO: migrate to
+ * a database.
+ */
+public class DashboardUserManager {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       private final File userFile;
+       private final List<EcompUser> users;
+
+       public DashboardUserManager(final String userFilePath) throws IOException {
+               logger.debug("ctor: userfile {}", userFilePath);
+               if (userFilePath == null)
+                       throw new IllegalArgumentException("Missing or empty user file property");
+               userFile = new File(userFilePath);
+               logger.debug("ctor: managing users in file {}", userFile.getAbsolutePath());
+               if (userFile.exists()) {
+                       final ObjectMapper mapper = new ObjectMapper();
+                       users = mapper.readValue(userFile, new TypeReference<List<EcompUser>>() {
+                       });
+               } else {
+                       users = new ArrayList<>();
+               }
+       }
+
+       /**
+        * Gets the user with the specified login Id
+        * 
+        * @param loginId
+        *                    Desired login Id
+        * @return User object; null if Id is not known
+        */
+       public EcompUser getUser(String loginId) {
+               for (EcompUser u : this.users) {
+                       if (u.getLoginId().equals(loginId)) {
+                               logger.debug("getUser: match on {}", loginId);
+                               return u;
+                       }
+               }
+               logger.debug("getUser: no match on {}", loginId);
+               return null;
+       }
+
+       private void saveUsers() throws JsonGenerationException, JsonMappingException, IOException {
+               final ObjectMapper mapper = new ObjectMapper();
+               mapper.writeValue(userFile, users);
+       }
+
+       /*
+        * Allow at most one thread to create a user at one time.
+        */
+       public synchronized void createUser(EcompUser user) throws PortalAPIException {
+               logger.debug("createUser: loginId is " + user.getLoginId());
+               if (users.contains(user))
+                       throw new PortalAPIException("User exists: " + user.getLoginId());
+               users.add(user);
+               try {
+                       saveUsers();
+               } catch (Exception ex) {
+                       throw new PortalAPIException("Save failed", ex);
+               }
+       }
+
+       /*
+        * Allow at most one thread to modify a user at one time. We still have
+        * last-edit-wins of course.
+        */
+       public synchronized void updateUser(String loginId, EcompUser user) throws PortalAPIException {
+               logger.debug("editUser: loginId is " + loginId);
+               int index = users.indexOf(user);
+               if (index < 0)
+                       throw new PortalAPIException("User does not exist: " + user.getLoginId());
+               users.remove(index);
+               users.add(user);
+               try {
+                       saveUsers();
+               } catch (Exception ex) {
+                       throw new PortalAPIException("Save failed", ex);
+               }
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java
new file mode 100644 (file)
index 0000000..9862e16
--- /dev/null
@@ -0,0 +1,41 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import org.onap.portalsdk.core.onboarding.exception.CipherUtilException;
+
+/**
+ * Supports an upgrade path among methods in CipherUtil because the PortalSDK is
+ * changing encryption methods.
+ */
+public interface IPortalSdkDecryptor {
+
+       /**
+        * Decrypts the specified value using a known key.
+        * 
+        * @param cipherText
+        *                       Encrypted value
+        * @return Clear text on success, null otherwise.
+        * @throws CipherUtilException
+        *                                 if any decryption step fails
+        */
+       String decrypt(String cipherText) throws CipherUtilException;
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java
new file mode 100644 (file)
index 0000000..e471447
--- /dev/null
@@ -0,0 +1,118 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import org.onap.portalsdk.core.onboarding.exception.CipherUtilException;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides services to authenticate requests from/to ONAP Portal.
+ */
+public class PortalAuthManager {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       final Map<String, String> credentialsMap;
+       private final IPortalSdkDecryptor portalSdkDecryptor;
+       private final String userIdCookieName;
+
+       public PortalAuthManager(final String appName, final String username, final String password,
+                       final String decryptorClassName, final String userCookie)
+                       throws ClassNotFoundException, InstantiationException, IllegalAccessException {
+               credentialsMap = new HashMap<>();
+               // The map keys are hardcoded in EPSDK-FW, no constants are defined :(
+               credentialsMap.put("appName", appName);
+               credentialsMap.put("username", username);
+               credentialsMap.put("password", password);
+               this.userIdCookieName = userCookie;
+               // Instantiate here so configuration errors are detected at app-start time
+               logger.debug("ctor: using decryptor class {}", decryptorClassName);
+               Class<?> decryptorClass = Class.forName(decryptorClassName);
+               portalSdkDecryptor = (IPortalSdkDecryptor) decryptorClass.newInstance();
+       }
+
+       /**
+        * @return A map of key-value pairs with application name, user name and
+        *         password.
+        */
+       public Map<String, String> getAppCredentials() {
+               return credentialsMap;
+       }
+
+       /**
+        * Searches the request for a cookie with the specified name.
+        *
+        * @param request
+        *                       HttpServletRequest
+        * @param cookieName
+        *                       Cookie name
+        * @return Cookie, or null if not found.
+        */
+       private Cookie getCookie(HttpServletRequest request, String cookieName) {
+               Cookie[] cookies = request.getCookies();
+               if (cookies != null)
+                       for (Cookie cookie : cookies)
+                               if (cookie.getName().equals(cookieName))
+                                       return cookie;
+               return null;
+       }
+
+       /**
+        * Validates whether the ECOMP Portal sign-on process has completed. Checks for
+        * the ECOMP cookie first, then the user cookie.
+        * 
+        * @param request
+        *                    HttpServletRequest
+        * @return User ID if the ECOMP cookie is present and the sign-on process
+        *         established a user ID; else null.
+        */
+       public String valdiateEcompSso(HttpServletRequest request) {
+               // Check ECOMP Portal cookie
+               Cookie ep = getCookie(request, PortalApiConstants.EP_SERVICE);
+               if (ep == null) {
+                       logger.debug("valdiateEcompSso: cookie not found: {}", PortalApiConstants.EP_SERVICE);
+                       return null;
+               }
+               logger.trace("validateEcompSso: found cookie {}", PortalApiConstants.EP_SERVICE);
+               Cookie user = getCookie(request, userIdCookieName);
+               if (user == null) {
+                       logger.debug("valdiateEcompSso: cookie not found: {}", userIdCookieName);
+                       return null;
+               }
+               logger.trace("validateEcompSso: user cookie {}", userIdCookieName);
+               String userid = null;
+               try {
+                       userid = portalSdkDecryptor.decrypt(user.getValue());
+               } catch (CipherUtilException e) {
+                       throw new IllegalArgumentException("valdiateEcompSso failed", e);
+               }
+               return userid;
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java
new file mode 100644 (file)
index 0000000..2ec5938
--- /dev/null
@@ -0,0 +1,177 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.URLEncoder;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.onap.portalsdk.core.onboarding.util.PortalApiProperties;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.model.EcompUserDetails;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+
+/**
+ * This filter checks every request for the cookie set by the ONAP Portal single
+ * sign on process. The possible paths and actions:
+ * <OL>
+ * <LI>User starts at an app page via a bookmark. No Portal cookie is set.
+ * Redirect there to get one; then continue as below.
+ * <LI>User starts at Portal and goes to app. Alternately, the user's session
+ * times out and the user hits refresh. The Portal cookie is set, but there is
+ * no valid session. Create one and publish info.
+ * <LI>User has valid Portal cookie and session. Reset the max idle in that
+ * session.
+ * </OL>
+ * <P>
+ * Notes:
+ * <UL>
+ * <LI>While redirecting, the cookie "redirectUrl" should also be set so that
+ * Portal knows where to forward the request to once the Portal Session is
+ * created and EPService cookie is set.
+ * </UL>
+ * 
+ * TODO: What about sessions? Will this be stateless?
+ * 
+ * This filter uses no annotations to avoid Spring's automatic registration,
+ * which add this filter in the chain in the wrong order.
+ */
+public class PortalAuthenticationFilter implements Filter {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       public static final String REDIRECT_URL_KEY = "redirectUrl";
+
+       private final PortalAuthManager authManager;
+
+       private final DashboardUserManager userManager;
+
+       public PortalAuthenticationFilter(PortalAuthManager authManager, DashboardUserManager userManager) {
+               this.authManager = authManager;
+               this.userManager = userManager;
+       }
+
+       @Override
+       public void init(FilterConfig filterConfig) throws ServletException {
+               // complain loudly if this key property is missing
+               String url = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
+               logger.debug("init: Portal redirect URL {}", url);
+               if (url == null)
+                       logger.error(
+                                       "init: Failed to find property in portal.properties: " + PortalApiConstants.ECOMP_REDIRECT_URL);
+       }
+
+       @Override
+       public void destroy() {
+               // No resources to release
+       }
+
+       /**
+        * Checks for valid cookies and allows request to be served if found; redirects
+        * to Portal otherwise. Requests for pages ignored in the web security config do
+        * not hit this filter.
+        */
+       @Override
+       public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+                       throws IOException, ServletException {
+               logger.debug("doFilter {}", req);
+               HttpServletRequest request = (HttpServletRequest) req;
+               HttpServletResponse response = (HttpServletResponse) res;
+               // Need to authenticate the request
+               final String userId = authManager.valdiateEcompSso(request);
+               final EcompUser ecompUser = (userId == null ? null : userManager.getUser(userId));
+               if (userId == null || ecompUser == null) {
+                       String redirectURL = buildLoginPageUrl(request);
+                       logger.trace("doFilter: unauthorized, redirecting to {}", redirectURL);
+                       response.sendRedirect(redirectURL);
+               } else {
+                       EcompUserDetails userDetails = new EcompUserDetails(ecompUser);
+                       // Using portal session as credentials is a hack
+                       PreAuthenticatedAuthenticationToken authToken = new PreAuthenticatedAuthenticationToken(userDetails,
+                                       getPortalSessionId(request), userDetails.getAuthorities());
+                       SecurityContextHolder.getContext().setAuthentication(authToken);
+                       // Pass request back down the filter chain
+                       chain.doFilter(request, response);
+               }
+       }
+
+       private String buildLoginPageUrl(HttpServletRequest request) {
+               logger.trace("buildLoginPageUrl");
+               // Why so much work to recover the original request?
+               final StringBuffer sb = request.getRequestURL();
+               sb.append(request.getQueryString() == null ? "" : "?" + request.getQueryString());
+               final String requestedUrl = sb.toString();
+               String encodedUrl = null;
+               try {
+                       encodedUrl = URLEncoder.encode(requestedUrl, "UTF-8");
+               } catch (UnsupportedEncodingException ex) {
+                       logger.error("buildLoginPageUrl: Failed to encode {}", requestedUrl);
+               }
+               return DashboardConstants.LOGIN_PAGE + "?" + REDIRECT_URL_KEY + "=" + encodedUrl;
+       }
+
+       /**
+        * Searches the request for a cookie with the specified name.
+        *
+        * @param request
+        *                       HttpServletRequest
+        * @param cookieName
+        *                       Cookie name
+        * @return Cookie, or null if not found.
+        */
+       private Cookie getCookie(HttpServletRequest request, String cookieName) {
+               Cookie[] cookies = request.getCookies();
+               if (cookies != null)
+                       for (Cookie cookie : cookies)
+                               if (cookie.getName().equals(cookieName))
+                                       return cookie;
+               return null;
+       }
+
+       /**
+        * Gets the ECOMP Portal service cookie value.
+        * 
+        * @param request
+        * @return Cookie value, or null if not found.
+        */
+       private String getPortalSessionId(HttpServletRequest request) {
+               Cookie ep = getCookie(request, PortalApiConstants.EP_SERVICE);
+               if (ep == null)
+                       return null;
+               return ep.getValue();
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java
new file mode 100644 (file)
index 0000000..f5d3759
--- /dev/null
@@ -0,0 +1,88 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.onap.portalsdk.core.onboarding.crossapi.IPortalRestCentralService;
+import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.config.SpringContextCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationContext;
+
+/**
+ * Implements the contract used by the Portal to transmit user details to this
+ * on-boarded application. The requests are intercepted first by a servlet in
+ * the EPSDK-FW library, which proxies the calls to these methods.
+ * 
+ * An instance of this class is created upon first request to the API. But this
+ * class is found and instantiated via Class.forName(), so cannot use Spring
+ * annotations.
+ */
+public class PortalRestCentralServiceImpl implements IPortalRestCentralService {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       private final PortalAuthManager authManager;
+       private final DashboardUserManager userManager;
+
+       public PortalRestCentralServiceImpl() throws IOException, PortalAPIException {
+               final ApplicationContext context = SpringContextCache.getApplicationContext();
+               authManager = (PortalAuthManager) context.getBean(PortalAuthManager.class);
+               userManager = (DashboardUserManager) context.getBean(DashboardUserManager.class);
+       }
+
+       /*
+        * Answers the Portal API credentials.
+        */
+       @Override
+       public Map<String, String> getAppCredentials() throws PortalAPIException {
+               logger.debug("getAppCredentials");
+               return authManager.getAppCredentials();
+       }
+
+       /*
+        * Extracts the user ID from a cookie in the header
+        */
+       @Override
+       public String getUserId(HttpServletRequest request) throws PortalAPIException {
+               logger.debug("getuserId");
+               return authManager.valdiateEcompSso(request);
+       }
+
+       @Override
+       public void pushUser(EcompUser user) throws PortalAPIException {
+               logger.debug("pushUser: {}", user);
+               userManager.createUser(user);
+       }
+
+       @Override
+       public void editUser(String loginId, EcompUser user) throws PortalAPIException {
+               logger.debug("editUser: {}", user);
+               userManager.updateUser(loginId, user);
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java
new file mode 100644 (file)
index 0000000..3019f52
--- /dev/null
@@ -0,0 +1,32 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import org.onap.portalsdk.core.onboarding.exception.CipherUtilException;
+import org.onap.portalsdk.core.onboarding.util.CipherUtil;
+
+public class PortalSdkDecryptorAes implements IPortalSdkDecryptor {
+
+       @SuppressWarnings("deprecation")
+       public String decrypt(String cipherText) throws CipherUtilException {
+               return CipherUtil.decrypt(cipherText);
+       }
+
+}
diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java
new file mode 100644 (file)
index 0000000..04d44cd
--- /dev/null
@@ -0,0 +1,31 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.portalapi;
+
+import org.onap.portalsdk.core.onboarding.exception.CipherUtilException;
+import org.onap.portalsdk.core.onboarding.util.CipherUtil;
+
+public class PortalSdkDecryptorPkc implements IPortalSdkDecryptor {
+
+       public String decrypt(String cipherText) throws CipherUtilException {
+               return CipherUtil.decryptPKC(cipherText);
+       }
+
+}
diff --git a/webapp-backend/src/main/resources/ESAPI.properties b/webapp-backend/src/main/resources/ESAPI.properties
new file mode 100644 (file)
index 0000000..f27b1ac
--- /dev/null
@@ -0,0 +1,385 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+#===========================================================================
+# ESAPI Configuration
+#
+# If true, then print all the ESAPI properties set here when they are loaded.
+# If false, they are not printed. Useful to reduce output when running JUnit tests.
+# If you need to troubleshoot a properties related problem, turning this on may help.
+# This is 'false' in the src/test/resources/.esapi version. It is 'true' by
+# default for reasons of backward compatibility with earlier ESAPI versions.
+ESAPI.printProperties=false
+
+# ESAPI is designed to be easily extensible. You can use the reference implementation
+# or implement your own providers to take advantage of your enterprise's security
+# infrastructure. The functions in ESAPI are referenced using the ESAPI locator, like:
+#
+#    String ciphertext =
+#              ESAPI.encryptor().encrypt("Secret message");   // Deprecated in 2.0
+#    CipherText cipherText =
+#              ESAPI.encryptor().encrypt(new PlainText("Secret message")); // Preferred
+#
+# Below you can specify the classname for the provider that you wish to use in your
+# application. The only requirement is that it implement the appropriate ESAPI interface.
+# This allows you to switch security implementations in the future without rewriting the
+# entire application.
+#
+# ExperimentalAccessController requires ESAPI-AccessControlPolicy.xml in .esapi directory
+ESAPI.AccessControl=org.owasp.esapi.reference.DefaultAccessController
+# FileBasedAuthenticator requires users.txt file in .esapi directory
+ESAPI.Authenticator=org.owasp.esapi.reference.FileBasedAuthenticator
+ESAPI.Encoder=org.owasp.esapi.reference.DefaultEncoder
+ESAPI.Encryptor=org.owasp.esapi.reference.crypto.JavaEncryptor
+
+ESAPI.Executor=org.owasp.esapi.reference.DefaultExecutor
+ESAPI.HTTPUtilities=org.owasp.esapi.reference.DefaultHTTPUtilities
+ESAPI.IntrusionDetector=org.owasp.esapi.reference.DefaultIntrusionDetector
+#ESAPI.Logger=org.owasp.esapi.reference.JavaLogFactory
+ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer
+ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator
+
+#===========================================================================
+# ESAPI Authenticator
+#
+Authenticator.AllowedLoginAttempts=3
+#Authenticator.MaxOldPasswordHashes=13
+Authenticator.UsernameParameterName=username
+#Authenticator.PasswordParameterName=password
+# RememberTokenDuration (in days)
+Authenticator.RememberTokenDuration=14
+# Session Timeouts (in minutes)
+Authenticator.IdleTimeoutDuration=20
+Authenticator.AbsoluteTimeoutDuration=120
+
+#===========================================================================
+# ESAPI Encoder
+#
+# ESAPI canonicalizes input before validation to prevent bypassing filters with encoded attacks.
+# Failure to canonicalize input is a very common mistake when implementing validation schemes.
+# Canonicalization is automatic when using the ESAPI Validator, but you can also use the
+# following code to canonicalize data.
+#
+#      ESAPI.Encoder().canonicalize( "%22hello world&#x22;" );
+#  
+# Multiple encoding is when a single encoding format is applied multiple times. Allowing
+# multiple encoding is strongly discouraged.
+Encoder.AllowMultipleEncoding=false
+
+# Mixed encoding is when multiple different encoding formats are applied, or when 
+# multiple formats are nested. Allowing multiple encoding is strongly discouraged.
+Encoder.AllowMixedEncoding=false
+
+# The default list of codecs to apply when canonicalizing untrusted data. The list should include the codecs
+# for all downstream interpreters or decoders. For example, if the data is likely to end up in a URL, HTML, or
+# inside JavaScript, then the list of codecs below is appropriate. The order of the list is not terribly important.
+Encoder.DefaultCodecList=HTMLEntityCodec,PercentCodec,JavaScriptCodec
+
+
+#===========================================================================
+# ESAPI Encryption
+#
+# The ESAPI Encryptor provides basic cryptographic functions with a simplified API.
+# To get started, generate a new key using java -classpath esapi.jar org.owasp.esapi.reference.crypto.JavaEncryptor
+# There is not currently any support for key rotation, so be careful when changing your key and salt as it
+# will invalidate all signed, encrypted, and hashed data.
+#
+# WARNING: Not all combinations of algorithms and key lengths are supported.
+# If you choose to use a key length greater than 128, you MUST download the
+# unlimited strength policy files and install in the lib directory of your JRE/JDK.
+# See http://java.sun.com/javase/downloads/index.jsp for more information.
+#
+# Backward compatibility with ESAPI Java 1.4 is supported by the two deprecated API
+# methods, Encryptor.encrypt(String) and Encryptor.decrypt(String). However, whenever
+# possible, these methods should be avoided as they use ECB cipher mode, which in almost
+# all circumstances a poor choice because of it's weakness. CBC cipher mode is the default
+# for the new Encryptor encrypt / decrypt methods for ESAPI Java 2.0.  In general, you
+# should only use this compatibility setting if you have persistent data encrypted with
+# version 1.4 and even then, you should ONLY set this compatibility mode UNTIL
+# you have decrypted all of your old encrypted data and then re-encrypted it with
+# ESAPI 2.0 using CBC mode. If you have some reason to mix the deprecated 1.4 mode
+# with the new 2.0 methods, make sure that you use the same cipher algorithm for both
+# (256-bit AES was the default for 1.4; 128-bit is the default for 2.0; see below for
+# more details.) Otherwise, you will have to use the new 2.0 encrypt / decrypt methods
+# where you can specify a SecretKey. (Note that if you are using the 256-bit AES,
+# that requires downloading the special jurisdiction policy files mentioned above.)
+#
+#              ***** IMPORTANT: Do NOT forget to replace these with your own values! *****
+# To calculate these values, you can run:
+#              java -classpath esapi.jar org.owasp.esapi.reference.crypto.JavaEncryptor
+#
+Encryptor.MasterKey=tzfztf56ftv
+Encryptor.MasterSalt=123456ztrewq
+
+# Provides the default JCE provider that ESAPI will "prefer" for its symmetric
+# encryption and hashing. (That is it will look to this provider first, but it
+# will defer to other providers if the requested algorithm is not implemented
+# by this provider.) If left unset, ESAPI will just use your Java VM's current
+# preferred JCE provider, which is generally set in the file
+# "$JAVA_HOME/jre/lib/security/java.security".
+#
+# The main intent of this is to allow ESAPI symmetric encryption to be
+# used with a FIPS 140-2 compliant crypto-module. For details, see the section
+# "Using ESAPI Symmetric Encryption with FIPS 140-2 Cryptographic Modules" in
+# the ESAPI 2.0 Symmetric Encryption User Guide, at:
+# http://owasp-esapi-java.googlecode.com/svn/trunk/documentation/esapi4java-core-2.0-symmetric-crypto-user-guide.html
+# However, this property also allows you to easily use an alternate JCE provider
+# such as "Bouncy Castle" without having to make changes to "java.security".
+# See Javadoc for SecurityProviderLoader for further details. If you wish to use
+# a provider that is not known to SecurityProviderLoader, you may specify the
+# fully-qualified class name of the JCE provider class that implements
+# java.security.Provider. If the name contains a '.', this is interpreted as
+# a fully-qualified class name that implements java.security.Provider.
+#
+# NOTE: Setting this property has the side-effect of changing it in your application
+#       as well, so if you are using JCE in your application directly rather than
+#       through ESAPI (you wouldn't do that, would you? ;-), it will change the
+#       preferred JCE provider there as well.
+#
+# Default: Keeps the JCE provider set to whatever JVM sets it to.
+Encryptor.PreferredJCEProvider=
+
+# AES is the most widely used and strongest encryption algorithm. This
+# should agree with your Encryptor.CipherTransformation property.
+# By default, ESAPI Java 1.4 uses "PBEWithMD5AndDES" and which is
+# very weak. It is essentially a password-based encryption key, hashed
+# with MD5 around 1K times and then encrypted with the weak DES algorithm
+# (56-bits) using ECB mode and an unspecified padding (it is
+# JCE provider specific, but most likely "NoPadding"). However, 2.0 uses
+# "AES/CBC/PKCSPadding". If you want to change these, change them here.
+# Warning: This property does not control the default reference implementation for
+#                 ESAPI 2.0 using JavaEncryptor. Also, this property will be dropped
+#                 in the future.
+# @deprecated
+Encryptor.EncryptionAlgorithm=AES
+#              For ESAPI Java 2.0 - New encrypt / decrypt methods use this.
+Encryptor.CipherTransformation=AES/CBC/PKCS5Padding
+
+# Applies to ESAPI 2.0 and later only!
+# Comma-separated list of cipher modes that provide *BOTH*
+# confidentiality *AND* message authenticity. (NIST refers to such cipher
+# modes as "combined modes" so that's what we shall call them.) If any of these
+# cipher modes are used then no MAC is calculated and stored
+# in the CipherText upon encryption. Likewise, if one of these
+# cipher modes is used with decryption, no attempt will be made
+# to validate the MAC contained in the CipherText object regardless
+# of whether it contains one or not. Since the expectation is that
+# these cipher modes support support message authenticity already,
+# injecting a MAC in the CipherText object would be at best redundant.
+#
+# Note that as of JDK 1.5, the SunJCE provider does not support *any*
+# of these cipher modes. Of these listed, only GCM and CCM are currently
+# NIST approved. YMMV for other JCE providers. E.g., Bouncy Castle supports
+# GCM and CCM with "NoPadding" mode, but not with "PKCS5Padding" or other
+# padding modes.
+Encryptor.cipher_modes.combined_modes=GCM,CCM,IAPM,EAX,OCB,CWC
+
+# Applies to ESAPI 2.0 and later only!
+# Additional cipher modes allowed for ESAPI 2.0 encryption. These
+# cipher modes are in _addition_ to those specified by the property
+# 'Encryptor.cipher_modes.combined_modes'.
+# Note: We will add support for streaming modes like CFB & OFB once
+# we add support for 'specified' to the property 'Encryptor.ChooseIVMethod'
+# (probably in ESAPI 2.1).
+# DISCUSS: Better name?
+Encryptor.cipher_modes.additional_allowed=CBC
+
+# 128-bit is almost always sufficient and appears to be more resistant to
+# related key attacks than is 256-bit AES. Use '_' to use default key size
+# for cipher algorithms (where it makes sense because the algorithm supports
+# a variable key size). Key length must agree to what's provided as the
+# cipher transformation, otherwise this will be ignored after logging a
+# warning.
+#
+# NOTE: This is what applies BOTH ESAPI 1.4 and 2.0. See warning above about mixing!
+Encryptor.EncryptionKeyLength=128
+
+# Because 2.0 uses CBC mode by default, it requires an initialization vector (IV).
+# (All cipher modes except ECB require an IV.) There are two choices: we can either
+# use a fixed IV known to both parties or allow ESAPI to choose a random IV. While
+# the IV does not need to be hidden from adversaries, it is important that the
+# adversary not be allowed to choose it. Also, random IVs are generally much more
+# secure than fixed IVs. (In fact, it is essential that feed-back cipher modes
+# such as CFB and OFB use a different IV for each encryption with a given key so
+# in such cases, random IVs are much preferred. By default, ESAPI 2.0 uses random
+# IVs. If you wish to use 'fixed' IVs, set 'Encryptor.ChooseIVMethod=fixed' and
+# uncomment the Encryptor.fixedIV.
+#
+# Valid values:                random|fixed|specified          'specified' not yet implemented; planned for 2.1
+Encryptor.ChooseIVMethod=random
+# If you choose to use a fixed IV, then you must place a fixed IV here that
+# is known to all others who are sharing your secret key. The format should
+# be a hex string that is the same length as the cipher block size for the
+# cipher algorithm that you are using. The following is an *example* for AES
+# from an AES test vector for AES-128/CBC as described in:
+# NIST Special Publication 800-38A (2001 Edition)
+# "Recommendation for Block Cipher Modes of Operation".
+# (Note that the block size for AES is 16 bytes == 128 bits.)
+#
+Encryptor.fixedIV=0x000102030405060708090a0b0c0d0e0f
+
+# Whether or not CipherText should use a message authentication code (MAC) with it.
+# This prevents an adversary from altering the IV as well as allowing a more
+# fool-proof way of determining the decryption failed because of an incorrect
+# key being supplied. This refers to the "separate" MAC calculated and stored
+# in CipherText, not part of any MAC that is calculated as a result of a
+# "combined mode" cipher mode.
+#
+# If you are using ESAPI with a FIPS 140-2 cryptographic module, you *must* also
+# set this property to false.
+Encryptor.CipherText.useMAC=true
+
+# Whether or not the PlainText object may be overwritten and then marked
+# eligible for garbage collection. If not set, this is still treated as 'true'.
+Encryptor.PlainText.overwrite=true
+
+# Do not use DES except in a legacy situations. 56-bit is way too small key size.
+#Encryptor.EncryptionKeyLength=56
+#Encryptor.EncryptionAlgorithm=DES
+
+# TripleDES is considered strong enough for most purposes.
+#      Note:   There is also a 112-bit version of DESede. Using the 168-bit version
+#                      requires downloading the special jurisdiction policy from Sun.
+#Encryptor.EncryptionKeyLength=168
+#Encryptor.EncryptionAlgorithm=DESede
+
+Encryptor.HashAlgorithm=SHA-512
+Encryptor.HashIterations=1024
+Encryptor.DigitalSignatureAlgorithm=SHA1withDSA
+Encryptor.DigitalSignatureKeyLength=1024
+Encryptor.RandomAlgorithm=SHA1PRNG
+Encryptor.CharacterEncoding=UTF-8
+
+# This is the Pseudo Random Function (PRF) that ESAPI's Key Derivation Function
+# (KDF) normally uses. Note this is *only* the PRF used for ESAPI's KDF and
+# *not* what is used for ESAPI's MAC. (Currently, HmacSHA1 is always used for
+# the MAC, mostly to keep the overall size at a minimum.)
+#
+# Currently supported choices for JDK 1.5 and 1.6 are:
+#      HmacSHA1 (160 bits), HmacSHA256 (256 bits), HmacSHA384 (384 bits), and
+#      HmacSHA512 (512 bits).
+# Note that HmacMD5 is *not* supported for the PRF used by the KDF even though
+# the JDKs support it.  See the ESAPI 2.0 Symmetric Encryption User Guide
+# further details.
+Encryptor.KDF.PRF=HmacSHA256
+#===========================================================================
+# ESAPI Logging
+# Set the application name if these logs are combined with other applications
+Logger.ApplicationName=portal_ric_dashboard
+# If you use an HTML log viewer that does not properly HTML escape log data, you can set LogEncodingRequired to true
+Logger.LogEncodingRequired=false
+# Determines whether ESAPI should log the application name. This might be clutter in some single-server/single-app environments.
+Logger.LogApplicationName=true
+# Determines whether ESAPI should log the server IP and port. This might be clutter in some single-server environments.
+Logger.LogServerIP=true
+# LogFileName, the name of the logging file. Provide a full directory path (e.g., C:\\ESAPI\\ESAPI_logging_file) if you
+# want to place it in a specific directory.
+Logger.LogFileName=portal_ric_dashboard_esapi_log
+# MaxLogFileSize, the max size (in bytes) of a single log file before it cuts over to a new one (default is 10,000,000)
+Logger.MaxLogFileSize=10000000
+
+
+#===========================================================================
+# ESAPI Intrusion Detection
+#
+# Each event has a base to which .count, .interval, and .action are added
+# The IntrusionException will fire if we receive "count" events within "interval" seconds
+# The IntrusionDetector is configurable to take the following actions: log, logout, and disable
+#  (multiple actions separated by commas are allowed e.g. event.test.actions=log,disable
+#
+# Custom Events
+# Names must start with "event." as the base
+# Use IntrusionDetector.addEvent( "test" ) in your code to trigger "event.test" here
+# You can also disable intrusion detection completely by changing
+# the following parameter to true
+#
+IntrusionDetector.Disable=false
+#
+IntrusionDetector.event.test.count=2
+IntrusionDetector.event.test.interval=10
+IntrusionDetector.event.test.actions=disable,log
+
+# Exception Events
+# All EnterpriseSecurityExceptions are registered automatically
+# Call IntrusionDetector.getInstance().addException(e) for Exceptions that do not extend EnterpriseSecurityException
+# Use the fully qualified classname of the exception as the base
+
+# any intrusion is an attack
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.count=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.interval=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.actions=log,disable,logout
+
+# for test purposes
+# CHECKME: Shouldn't there be something in the property name itself that designates
+#                 that these are for testing???
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.count=10
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.interval=5
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.actions=log,disable,logout
+
+# rapid validation errors indicate scans or attacks in progress
+# org.owasp.esapi.errors.ValidationException.count=10
+# org.owasp.esapi.errors.ValidationException.interval=10
+# org.owasp.esapi.errors.ValidationException.actions=log,logout
+
+# sessions jumping between hosts indicates session hijacking
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.count=2
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.interval=10
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.actions=log,logout
+
+
+#===========================================================================
+# ESAPI Validation
+#
+# The ESAPI Validator works on regular expressions with defined names. You can define names
+# either here, or you may define application specific patterns in a separate file defined below.
+# This allows enterprises to specify both organizational standards as well as application specific
+# validation rules.
+#
+Validator.ConfigurationFile=validation.properties
+Validator.ConfigurationFile.MultiValued=false
+
+# Validators used by ESAPI
+Validator.AccountName=^[a-zA-Z0-9]{3,20}$
+Validator.SystemCommand=^[a-zA-Z\\-\\/]{1,64}$
+Validator.RoleName=^[a-z]{1,20}$
+
+#the word TEST below should be changed to your application 
+#name - only relative URL's are supported
+Validator.Redirect=^\\/test.*$
+
+# Global HTTP Validation Rules
+# Values with Base64 encoded data (e.g. encrypted state) will need at least [a-zA-Z0-9\/+=]
+Validator.HTTPScheme=^(http|https)$
+Validator.HTTPServerName=^[a-zA-Z0-9_.\\-]*$
+Validator.HTTPParameterName=^[a-zA-Z0-9_]{1,32}$
+Validator.HTTPParameterValue=^[a-zA-Z0-9.\\-\\/+=@_ ]*$
+Validator.HTTPCookieName=^[a-zA-Z0-9\\-_]{1,32}$
+Validator.HTTPCookieValue=^[a-zA-Z0-9\\-\\/+=_ ]*$
+Validator.HTTPHeaderName=^[a-zA-Z0-9\\-_]{1,32}$
+Validator.HTTPHeaderValue=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPContextPath=^\\/?[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPServletPath=^[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPPath=^[a-zA-Z0-9.\\-_]*$
+Validator.HTTPQueryString=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ %]*$
+Validator.HTTPURI=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPURL=^.*$
+Validator.HTTPJSESSIONID=^[A-Z0-9]{10,30}$
+
+# Validation of file related input
+Validator.FileName=^[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
+Validator.DirectoryName=^[a-zA-Z0-9:/\\\\!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
index 382cd19..1b7bd1c 100644 (file)
@@ -1,4 +1,3 @@
-###
 # ========================LICENSE_START=================================
 # O-RAN-SC
 # %%
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ========================LICENSE_END===================================
-###
 
-# Defines property keys for the RIC Dashboard, and some defaults
+# Defines RIC Dashboard property keys and default values.
+# Create a copy in the launch directory to override values; or
+# copy to "application-abc.properties" as mentioned in the README.
 
-# Confusingly, this key has no "spring" prefix
-# The port number is chosen RANDOMLY when running a test
+# A spring property but without a "spring" prefix;
+# the port number is chosen RANDOMLY when running tests
 server.port = 8080
 
+# path to file that stores user details;
+# use a persistent volume in a K8S deployment
+userfile = users.json
+
+# class that decrypts ciphertext from Portal
+portalapi.decryptor = org.oransc.ric.portal.dashboard.portalapi.PortalSdkDecryptorAes
+# name of request cookie with user ID
+portalapi.usercookie = UserId
+
+# portal credentials must be supplied at deployment time
+portalapi.appname = RIC Dashboard
+portalapi.username =
+portalapi.password =
+
+# endpoint URLs must be supplied at deployment time
 # A1 Mediator
 a1med.url.prefix = http://jar-app-props-default-A1-URL-prefix
 a1med.url.suffix =
-
 # ANR xApp
 anrxapp.url.prefix = http://jar-app-props-default-ANR-URL-prefix
 anrxapp.url.suffix =
-
 # App Manager
 appmgr.url.prefix = http://jar-app-props-default-Xapp-Mgr-URL
 appmgr.url.suffix = /ric/v1
-
 # E2 Manager
 e2mgr.url.prefix = http://jar-app-props-default-E2-URL
 e2mgr.url.suffix = /v1
+
+# Mimic slow endpoints by defining sleep period, in milliseconds
+mock.config.delay = 0
index 26c9ffb..b17cbe2 100644 (file)
   ========================LICENSE_END===================================
   -->
 
-
 <configuration>
 
        <!-- Basic logback configuration for dev and test -->
 
        <!-- component name is log file basename -->
        <property name="componentName" value="ric-dashboard"></property>
-       <!-- gather files in a subdirectory - usually a volume in docker -->
+       <!-- gather files in a subdirectory -->
        <property name="logDirectory" value="logs" />
-       <!-- basic pattern -->
+       <!-- output pattern -->
        <property name="pattern" value="%d{&quot;yyyy-MM-dd'T'HH:mm:ss.SSSXXX&quot;, UTC} [%thread] %-5level %logger{36} - %msg%n"/>
 
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
                <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
                        <fileNamePattern>${logDirectory}/${componentName}.%i.log.zip</fileNamePattern>
                        <minIndex>1</minIndex>
-                       <maxIndex>5</maxIndex>
+                       <maxIndex>9</maxIndex>
                </rollingPolicy>
                <triggeringPolicy
                        class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-                       <maxFileSize>1MB</maxFileSize>
+                       <maxFileSize>10MB</maxFileSize>
                </triggeringPolicy>
                <!-- defaults to type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
                <encoder>
        <!-- Code under test should be chatty --> >
        <logger name="org.oransc.ric.portal.dashboard" level="DEBUG" />
 
-       <!-- Report URLs to the log -->
+       <!-- Watch authentication done by EPSDK-FW -->
+       <logger name="org.onap.portalsdk.core.onboarding.crossapi" level="DEBUG" />
+
+       <!-- Report request URLs -->
        <logger name="org.springframework.web.client.RestTemplate" level="DEBUG" />
 
 </configuration>
diff --git a/webapp-backend/src/main/resources/validation.properties b/webapp-backend/src/main/resources/validation.properties
new file mode 100644 (file)
index 0000000..0785d06
--- /dev/null
@@ -0,0 +1,19 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+# empty file to suppress OWASP complaints emitted to stdout
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.config;
+package org.oransc.ric.portal.dashboard.config;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -30,6 +30,7 @@ import org.oransc.ric.a1med.client.api.A1MediatorApi;
 import org.oransc.ric.a1med.client.invoker.ApiClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
@@ -43,8 +44,10 @@ import org.springframework.http.HttpStatus;
 public class A1MediatorMockConfiguration {
 
        private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
        // Simulate remote method delay for UI testing
-       private final int delayMs = 500;
+       @Value("${mock.config.delay:0}")
+       private int delayMs;
 
        public A1MediatorMockConfiguration() {
                logger.info("Configuring mock A1 Mediator");
@@ -63,13 +66,17 @@ public class A1MediatorMockConfiguration {
                A1MediatorApi mockApi = mock(A1MediatorApi.class);
                when(mockApi.getApiClient()).thenReturn(apiClient);
                doAnswer(inv -> {
-                       logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).a1ControllerGetHandler(any(String.class));
                doAnswer(inv -> {
-                       logger.debug("a1ControllerPutHandler sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("a1ControllerPutHandler sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).a1ControllerPutHandler(any(String.class), any(Object.class));
                return mockApi;
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.config;
+package org.oransc.ric.portal.dashboard.config;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -37,6 +37,7 @@ import org.oransc.ric.anrxapp.client.model.NeighborCellRelationMod;
 import org.oransc.ric.anrxapp.client.model.NeighborCellRelationTable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
@@ -51,17 +52,20 @@ public class AnrXappMockConfiguration {
 
        private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+       // Simulate remote method delay for UI testing
+       @Value("${mock.config.delay:0}")
+       private int delayMs;
+
        private static final String GNODEB1 = "001EF5:0045FE50";
        private static final String GNODEB2 = "001EF6:0045FE51";
        private static final String GNODEB3 = "001EF7:0045FE52";
+
        // Sonar wants separate declarations
        private final NeighborCellRelationTable ncrt;
        private final NeighborCellRelationTable ncrtNodeB1;
        private final NeighborCellRelationTable ncrtNodeB2;
        private final NeighborCellRelationTable ncrtNodeB3;
        private final GgNodeBTable gNodebTable;
-       // Simulate remote method delay for UI testing
-       private final int delayMs = 500;
 
        public AnrXappMockConfiguration() {
                logger.info("Configuring mock ANR xApp client");
@@ -118,44 +122,60 @@ public class AnrXappMockConfiguration {
                NcrtApi mockApi = mock(NcrtApi.class);
                when(mockApi.getApiClient()).thenReturn(apiClient);
                doAnswer(inv -> {
-                       logger.debug("getgNodeB sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getgNodeB sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return gNodebTable;
                }).when(mockApi).getgNodeB();
                // Swagger sends nulls; front end sends empty strings
                doAnswer(inv -> {
-                       logger.debug("getNcrt (1) sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNcrt (1) sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return ncrt;
                }).when(mockApi).getNcrt((String) isNull(), (String) isNull(), (String) isNull());
                doAnswer(inv -> {
-                       logger.debug("getNcrt (2) sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNcrt (2) sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return ncrt;
                }).when(mockApi).getNcrt(eq(""), any(String.class), any(String.class));
                doAnswer(inv -> {
-                       logger.debug("getNcrt (3) sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNcrt (3) sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return ncrtNodeB1;
                }).when(mockApi).getNcrt(eq(GNODEB1), any(String.class), any(String.class));
                doAnswer(inv -> {
-                       logger.debug("getNcrt (4) sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNcrt (4) sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return ncrtNodeB2;
                }).when(mockApi).getNcrt(eq(GNODEB2), any(String.class), any(String.class));
                doAnswer(inv -> {
-                       logger.debug("getNcrt (5) sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNcrt (5) sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return ncrtNodeB3;
                }).when(mockApi).getNcrt(eq(GNODEB3), any(String.class), any(String.class));
                doAnswer(inv -> {
-                       logger.debug("deleteNcrt sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("deleteNcrt sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).deleteNcrt(any(String.class), any(String.class));
                doAnswer(inv -> {
-                       logger.debug("modifyNcrt sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("modifyNcrt sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).modifyNcrt(any(String.class), any(String.class), any(NeighborCellRelationMod.class));
                return mockApi;
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.config;
+package org.oransc.ric.portal.dashboard.config;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -42,6 +42,7 @@ import org.oransc.ric.plt.appmgr.client.model.Xapp.StatusEnum;
 import org.oransc.ric.plt.appmgr.client.model.XappInstance;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
@@ -57,12 +58,14 @@ public class AppManagerMockConfiguration {
 
        private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+       // Simulate remote method delay for UI testing
+       @Value("${mock.config.delay:0}")
+       private int delayMs;
+
        private final AllDeployableXapps availXapps;
        private final AllDeployedXapps deployedXapps;
        private final AllXappConfig allXappConfigs;
        private final SubscriptionResponse subRes;
-       // Simulate remote method delay for UI testing
-       private final int delayMs = 500;
 
        public AppManagerMockConfiguration() {
                logger.info("Configuring mock xApp Manager");
@@ -105,58 +108,80 @@ public class AppManagerMockConfiguration {
                XappApi mockApi = mock(XappApi.class);
                when(mockApi.getApiClient()).thenReturn(mockClient);
                doAnswer(inv -> {
-                       logger.debug("getAllXappConfig sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getAllXappConfig sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return allXappConfigs;
                }).when(mockApi).getAllXappConfig();
                doAnswer(inv -> {
-                       logger.debug("createXappConfig sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("createXappConfig sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return allXappConfigs.get(0);
                }).when(mockApi).createXappConfig(any(XAppConfig.class));
                doAnswer(inv -> {
-                       logger.debug("modifyXappConfig sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("modifyXappConfig sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return allXappConfigs.get(0);
                }).when(mockApi).modifyXappConfig(any(XAppConfig.class));
                doAnswer(inv -> {
-                       logger.debug("deleteXappConfig sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("deleteXappConfig sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).deleteXappConfig(any(ConfigMetadata.class));
                doAnswer(inv -> {
-                       logger.debug("deployXapp of {} sleeping {}", inv.getArgument(0), delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("deployXapp of {} sleeping {}", inv.getArgument(0), delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return deployedXapps.get(0);
                }).when(mockApi).deployXapp(any(XAppInfo.class));
                doAnswer(inv -> {
-                       logger.debug("listAllXapps sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("listAllXapps sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return availXapps;
                }).when(mockApi).listAllXapps();
                doAnswer(inv -> {
-                       logger.debug("getAllXapps sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getAllXapps sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return deployedXapps;
                }).when(mockApi).getAllXapps();
                doAnswer(inv -> {
-                       logger.debug("getXappByName of {} sleeping {}", inv.getArgument(0), delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getXappByName of {} sleeping {}", inv.getArgument(0), delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return deployedXapps.get(0);
                }).when(mockApi).getXappByName(any(String.class));
                doAnswer(inv -> {
-                       logger.debug("undeployXapp of {} sleeping {}", inv.getArgument(0), delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("undeployXapp of {} sleeping {}", inv.getArgument(0), delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).undeployXapp(any(String.class));
                doAnswer(inv -> {
-                       logger.debug("addSubscription sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("addSubscription sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return subRes;
                }).when(mockApi).addSubscription(any(SubscriptionRequest.class));
                doAnswer(inv -> {
-                       logger.debug("deleteSubscription sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("deleteSubscription sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).deleteSubscription(any(String.class));
                return mockApi;
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.config;
+package org.oransc.ric.portal.dashboard.config;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
@@ -37,6 +37,7 @@ import org.oransc.ric.e2mgr.client.model.NodebIdentityGlobalNbId;
 import org.oransc.ric.e2mgr.client.model.SetupRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Profile;
@@ -51,10 +52,12 @@ public class E2ManagerMockConfiguration {
 
        private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
 
+       // Simulate remote method delay for UI testing
+       @Value("${mock.config.delay:0}")
+       private int delayMs;
+
        private final List<NodebIdentity> nodebIdList;
        private final GetNodebResponse nodebResponse;
-       // Simulate remote method delay for UI testing
-       private final int delayMs = 500;
 
        public E2ManagerMockConfiguration() {
                logger.info("Configuring mock E2 Manager");
@@ -89,28 +92,38 @@ public class E2ManagerMockConfiguration {
                NodebApi mockApi = mock(NodebApi.class);
                when(mockApi.getApiClient()).thenReturn(apiClient);
                doAnswer(inv -> {
-                       logger.debug("nodebDelete sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("nodebDelete sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).nodebDelete();
                doAnswer(inv -> {
-                       logger.debug("getNb sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNb sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return nodebResponse;
                }).when(mockApi).getNb(any(String.class));
                doAnswer(inv -> {
-                       logger.debug("getNodebIdList sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("getNodebIdList sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return nodebIdList;
                }).when(mockApi).getNodebIdList();
                doAnswer(inv -> {
-                       logger.debug("endcSetup sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("endcSetup sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).endcSetup(any(SetupRequest.class));
                doAnswer(inv -> {
-                       logger.debug("x2Setup sleeping {}", delayMs);
-                       Thread.sleep(delayMs);
+                       if (delayMs > 0) {
+                               logger.debug("x2Setup sleeping {}", delayMs);
+                               Thread.sleep(delayMs);
+                       }
                        return null;
                }).when(mockApi).x2Setup(any(SetupRequest.class));
                return mockApi;
diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java
new file mode 100644 (file)
index 0000000..c17baef
--- /dev/null
@@ -0,0 +1,171 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.config;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy;
+import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.onap.portalsdk.core.restful.domain.EcompRole;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.LoginServlet;
+import org.oransc.ric.portal.dashboard.portalapi.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.portalapi.PortalAuthManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.servlet.ServletRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(securedEnabled = true)
+@Profile("test")
+public class WebSecurityMockConfiguration extends WebSecurityConfigurerAdapter {
+
+       public static final String TEST_CRED_ADMIN = "admin";
+       public static final String TEST_CRED_STANDARD = "standard";
+
+       // Unfortunately EPSDK-FW does not define these as constants
+       public static final String PORTAL_USERNAME_HEADER_KEY = "username";
+       public static final String PORTAL_PASSWORD_HEADER_KEY = "password";
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       public WebSecurityMockConfiguration(@Value("${userfile}") final String userFilePath) {
+               logger.debug("ctor: user file path {}", userFilePath);
+       }
+
+       @Override
+       protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+               PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+               auth.inMemoryAuthentication() //
+                               .passwordEncoder(encoder) //
+                               // The admin user has the admin AND standard roles
+                               .withUser(TEST_CRED_ADMIN) //
+                               .password(encoder.encode(TEST_CRED_ADMIN))
+                               .roles(DashboardConstants.ROLE_NAME_ADMIN, DashboardConstants.ROLE_NAME_STANDARD)//
+                               .and()//
+                               // The standard user has only the standard role
+                               .withUser(TEST_CRED_STANDARD) //
+                               .password(encoder.encode(TEST_CRED_STANDARD)) //
+                               .roles(DashboardConstants.ROLE_NAME_STANDARD);
+       }
+
+       @Override
+       protected void configure(HttpSecurity http) throws Exception {
+               http.authorizeRequests().anyRequest().authenticated()//
+                               .and().httpBasic() //
+                               .and().csrf().disable();
+       }
+
+       @Override
+       public void configure(WebSecurity web) throws Exception {
+               // This disables Spring security, but not the app's filter.
+               web.ignoring().antMatchers(WebSecurityConfiguration.OPEN_PATHS);
+       }
+
+       @Bean
+       public ServletRegistrationBean<LoginServlet> loginServlet() {
+               LoginServlet servlet = new LoginServlet();
+               final ServletRegistrationBean<LoginServlet> servletBean = new ServletRegistrationBean<>(servlet,
+                               DashboardConstants.LOGIN_PAGE);
+               servletBean.setName("LoginServlet");
+               return servletBean;
+       }
+
+       @Bean
+       public ServletRegistrationBean<PortalRestAPIProxy> portalApiProxyServlet() {
+               PortalRestAPIProxy servlet = new PortalRestAPIProxy();
+               final ServletRegistrationBean<PortalRestAPIProxy> servletBean = new ServletRegistrationBean<>(servlet,
+                               PortalApiConstants.API_PREFIX + "/*");
+               servletBean.setName("PortalRestApiProxyServlet");
+               return servletBean;
+       }
+
+       @Bean
+       public PortalAuthManager portalAuthManager() throws Exception {
+               PortalAuthManager mockManager = mock(PortalAuthManager.class);
+               final Map<String, String> credentialsMap = new HashMap<>();
+               credentialsMap.put("appName", "appName");
+               credentialsMap.put(PORTAL_USERNAME_HEADER_KEY, PORTAL_USERNAME_HEADER_KEY);
+               credentialsMap.put(PORTAL_PASSWORD_HEADER_KEY, PORTAL_PASSWORD_HEADER_KEY);
+               doAnswer(inv -> {
+                       logger.debug("getAppCredentials");
+                       return credentialsMap;
+               }).when(mockManager).getAppCredentials();
+               doAnswer(inv -> {
+                       logger.debug("getUserId");
+                       return "userId";
+               }).when(mockManager).valdiateEcompSso(any(HttpServletRequest.class));
+               doAnswer(inv -> {
+                       logger.debug("getAppCredentials");
+                       return credentialsMap;
+               }).when(mockManager).getAppCredentials();
+               return mockManager;
+       }
+
+       // This implementation is so light it can be used during tests.
+       @Bean
+       public DashboardUserManager dashboardUserManager() throws IOException, PortalAPIException {
+               File f = new File("/tmp/users.json");
+               if (f.exists())
+                       f.delete();
+               DashboardUserManager um = new DashboardUserManager(f.getAbsolutePath());
+               // Mock user for convenience in testing
+               EcompUser demo = new EcompUser();
+               demo.setLoginId("demo");
+               demo.setFirstName("Demo");
+               demo.setLastName("User");
+               demo.setActive(true);
+               EcompRole role = new EcompRole();
+               role.setName("view");
+               Set<EcompRole> roles = new HashSet<>();
+               roles.add(role);
+               demo.setRoles(roles);
+               um.createUser(demo);
+               return um;
+       }
+
+}
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
@@ -26,6 +26,7 @@ import java.util.Map;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -98,4 +99,14 @@ public class AbstractControllerTest {
                logger.info("Context loads on mock profile");
        }
 
+       public TestRestTemplate testRestTemplateAdminRole() {
+               return restTemplate.withBasicAuth(WebSecurityMockConfiguration.TEST_CRED_ADMIN,
+                               WebSecurityMockConfiguration.TEST_CRED_ADMIN);
+       }
+
+       public TestRestTemplate testRestTemplateStandardRole() {
+               return restTemplate.withBasicAuth(WebSecurityMockConfiguration.TEST_CRED_STANDARD,
+                               WebSecurityMockConfiguration.TEST_CRED_STANDARD);
+       }
+
 }
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.io.IOException;
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
+
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-
-import org.oransc.ric.portal.dashboard.controller.AcXappController;
 import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,7 +52,8 @@ public class AcXappControllerTest extends AbstractControllerTest {
                // Always returns 501; surprised that no exception is thrown.
                URI uri = buildUri(null, AcXappController.CONTROLLER_PATH, AcXappController.ADMCTRL_METHOD);
                logger.info("Invoking {}", uri);
-               ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
+               ResponseEntity<String> response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null,
+                               String.class);
                Assertions.assertTrue(response.getStatusCode().is5xxServerError());
        }
 
@@ -64,7 +64,8 @@ public class AcXappControllerTest extends AbstractControllerTest {
                URI uri = buildUri(null, AcXappController.CONTROLLER_PATH, AcXappController.ADMCTRL_METHOD);
                HttpEntity<JsonNode> entity = new HttpEntity<>(body);
                logger.info("Invoking {}", uri);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.PUT, entity, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.PUT, entity,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
@@ -25,7 +25,6 @@ import java.util.List;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
-import org.oransc.ric.portal.dashboard.controller.AdminController;
 import org.oransc.ric.portal.dashboard.model.DashboardUser;
 import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
@@ -55,13 +54,22 @@ public class AdminControllerTest extends AbstractControllerTest {
        }
 
        @Test
-       public void usersTest() {
+       public void getUsersTest() {
                URI uri = buildUri(null, AdminController.CONTROLLER_PATH, AdminController.USER_METHOD);
                logger.info("Invoking {}", uri);
-               ResponseEntity<List<DashboardUser>> response = restTemplate.exchange(uri, HttpMethod.GET, null,
+               ResponseEntity<List<DashboardUser>> response = testRestTemplateAdminRole().exchange(uri, HttpMethod.GET, null,
                                new ParameterizedTypeReference<List<DashboardUser>>() {
                                });
                Assertions.assertFalse(response.getBody().isEmpty());
        }
 
+       @Test
+       public void getUsersTestRoleAuthFail() {
+               URI uri = buildUri(null, AdminController.CONTROLLER_PATH, AdminController.USER_METHOD);
+               logger.info("Invoking {}", uri);
+               ResponseEntity<String> response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null,
+                               String.class);
+               Assertions.assertTrue(response.getStatusCode().is4xxClientError());
+       }
+
 }
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
 import org.oransc.ric.anrxapp.client.model.GgNodeBTable;
 import org.oransc.ric.anrxapp.client.model.NeighborCellRelationMod;
 import org.oransc.ric.anrxapp.client.model.NeighborCellRelationTable;
-import org.oransc.ric.portal.dashboard.controller.AnrXappController;
 import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -67,7 +66,7 @@ public class AnrXappControllerTest extends AbstractControllerTest {
        public void gnodebsTest() {
                URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.GNODEBS_METHOD);
                logger.info("Invoking {}", uri);
-               GgNodeBTable list = restTemplate.getForObject(uri, GgNodeBTable.class);
+               GgNodeBTable list = testRestTemplateStandardRole().getForObject(uri, GgNodeBTable.class);
                Assertions.assertFalse(list.getGNodeBIds().isEmpty());
        }
 
@@ -75,7 +74,8 @@ public class AnrXappControllerTest extends AbstractControllerTest {
        public void ncrtGetTest() {
                URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.NCRT_METHOD);
                logger.info("Invoking {}", uri);
-               NeighborCellRelationTable table = restTemplate.getForObject(uri, NeighborCellRelationTable.class);
+               NeighborCellRelationTable table = testRestTemplateStandardRole().getForObject(uri,
+                               NeighborCellRelationTable.class);
                Assertions.assertFalse(table.getNcrtRelations().isEmpty());
        }
 
@@ -85,7 +85,8 @@ public class AnrXappControllerTest extends AbstractControllerTest {
                                AnrXappController.PP_SERVING, "serving", AnrXappController.PP_NEIGHBOR, "neighbor");
                logger.info("Invoking {}", uri);
                HttpEntity<NeighborCellRelationMod> entity = new HttpEntity<>(new NeighborCellRelationMod());
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.PUT, entity, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.PUT, entity,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -94,7 +95,8 @@ public class AnrXappControllerTest extends AbstractControllerTest {
                URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.NCRT_METHOD,
                                AnrXappController.PP_SERVING, "serving", AnrXappController.PP_NEIGHBOR, "neighbor");
                logger.info("Invoking {}", uri);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
@@ -30,7 +30,6 @@ import org.oransc.ric.plt.appmgr.client.model.ConfigMetadata;
 import org.oransc.ric.plt.appmgr.client.model.XAppConfig;
 import org.oransc.ric.plt.appmgr.client.model.XAppInfo;
 import org.oransc.ric.plt.appmgr.client.model.Xapp;
-import org.oransc.ric.portal.dashboard.controller.AppManagerController;
 import org.oransc.ric.portal.dashboard.model.DashboardDeployableXapps;
 import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
@@ -71,7 +70,8 @@ public class AppManagerControllerTest extends AbstractControllerTest {
        public void appListTest() {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_LIST_METHOD);
                logger.info("Invoking {}", uri);
-               DashboardDeployableXapps apps = restTemplate.getForObject(uri, DashboardDeployableXapps.class);
+               DashboardDeployableXapps apps = testRestTemplateStandardRole().getForObject(uri,
+                               DashboardDeployableXapps.class);
                Assertions.assertFalse(apps.isEmpty());
        }
 
@@ -79,7 +79,7 @@ public class AppManagerControllerTest extends AbstractControllerTest {
        public void appStatusesTest() {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD);
                logger.info("Invoking {}", uri);
-               AllDeployedXapps apps = restTemplate.getForObject(uri, AllDeployedXapps.class);
+               AllDeployedXapps apps = testRestTemplateStandardRole().getForObject(uri, AllDeployedXapps.class);
                Assertions.assertFalse(apps.isEmpty());
        }
 
@@ -87,7 +87,7 @@ public class AppManagerControllerTest extends AbstractControllerTest {
        public void appStatusTest() {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD, "app");
                logger.info("Invoking {}", uri);
-               Xapp app = restTemplate.getForObject(uri, Xapp.class);
+               Xapp app = testRestTemplateStandardRole().getForObject(uri, Xapp.class);
                Assertions.assertFalse(app.getName().isEmpty());
        }
 
@@ -96,7 +96,7 @@ public class AppManagerControllerTest extends AbstractControllerTest {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD);
                logger.info("Invoking {}", uri);
                XAppInfo info = new XAppInfo();
-               Xapp app = restTemplate.postForObject(uri, info, Xapp.class);
+               Xapp app = testRestTemplateAdminRole().postForObject(uri, info, Xapp.class);
                Assertions.assertFalse(app.getName().isEmpty());
        }
 
@@ -104,7 +104,8 @@ public class AppManagerControllerTest extends AbstractControllerTest {
        public void undeployAppTest() {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD, "app");
                logger.info("Invoking {}", uri);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -112,7 +113,7 @@ public class AppManagerControllerTest extends AbstractControllerTest {
        public void getConfigTest() {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.CONFIG_METHOD);
                logger.info("Invoking {}", uri);
-               AllXappConfig config = restTemplate.getForObject(uri, AllXappConfig.class);
+               AllXappConfig config = testRestTemplateStandardRole().getForObject(uri, AllXappConfig.class);
                Assertions.assertFalse(config.isEmpty());
        }
 
@@ -121,7 +122,7 @@ public class AppManagerControllerTest extends AbstractControllerTest {
                URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.CONFIG_METHOD);
                logger.info("Invoking {}", uri);
                XAppConfig newConfig = new XAppConfig();
-               XAppConfig response = restTemplate.postForObject(uri, newConfig, XAppConfig.class);
+               XAppConfig response = testRestTemplateAdminRole().postForObject(uri, newConfig, XAppConfig.class);
                Assertions.assertNotNull(response.getConfig());
        }
 
@@ -131,7 +132,8 @@ public class AppManagerControllerTest extends AbstractControllerTest {
                logger.info("Invoking {}", uri);
                ConfigMetadata delConfig = new ConfigMetadata();
                HttpEntity<ConfigMetadata> entity = new HttpEntity<>(delConfig);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, entity, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, entity,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -17,7 +17,7 @@
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-package org.oransc.ric.portal.dashboard.test.controller;
+package org.oransc.ric.portal.dashboard.controller;
 
 import java.lang.invoke.MethodHandles;
 import java.net.URI;
@@ -28,7 +28,6 @@ import org.junit.jupiter.api.Test;
 import org.oransc.ric.e2mgr.client.model.GetNodebResponse;
 import org.oransc.ric.e2mgr.client.model.NodebIdentity;
 import org.oransc.ric.e2mgr.client.model.SetupRequest;
-import org.oransc.ric.portal.dashboard.controller.E2ManagerController;
 import org.oransc.ric.portal.dashboard.model.RanDetailsTransport;
 import org.oransc.ric.portal.dashboard.model.SuccessTransport;
 import org.slf4j.Logger;
@@ -62,8 +61,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
        public void ranDetailsTest() {
                URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.RAN_METHOD);
                logger.info("Invoking {}", uri);
-               ResponseEntity<List<RanDetailsTransport>> response = restTemplate.exchange(uri, HttpMethod.GET, null,
-                               new ParameterizedTypeReference<List<RanDetailsTransport>>() {
+               ResponseEntity<List<RanDetailsTransport>> response = testRestTemplateStandardRole().exchange(uri,
+                               HttpMethod.GET, null, new ParameterizedTypeReference<List<RanDetailsTransport>>() {
                                });
                Assertions.assertFalse(response.getBody().isEmpty());
        }
@@ -72,8 +71,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
        public void nodebListTest() {
                URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_LIST_METHOD);
                logger.info("Invoking {}", uri);
-               ResponseEntity<List<NodebIdentity>> response = restTemplate.exchange(uri, HttpMethod.GET, null,
-                               new ParameterizedTypeReference<List<NodebIdentity>>() {
+               ResponseEntity<List<NodebIdentity>> response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET,
+                               null, new ParameterizedTypeReference<List<NodebIdentity>>() {
                                });
                Assertions.assertFalse(response.getBody().isEmpty());
        }
@@ -82,7 +81,7 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
        public void nodebStatusTest() {
                URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_METHOD, "nodeb");
                logger.info("Invoking {}", uri);
-               GetNodebResponse response = restTemplate.getForObject(uri, GetNodebResponse.class);
+               GetNodebResponse response = testRestTemplateStandardRole().getForObject(uri, GetNodebResponse.class);
                Assertions.assertNotNull(response.getRanName());
        }
 
@@ -90,7 +89,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
        public void bigRedButtonTest() {
                URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_METHOD);
                logger.info("Invoking {}", uri);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -100,7 +100,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
                logger.info("Invoking {}", uri);
                SetupRequest setup = new SetupRequest();
                HttpEntity<SetupRequest> entity = new HttpEntity<>(setup);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.POST, entity,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
@@ -110,7 +111,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest {
                logger.info("Invoking {}", uri);
                SetupRequest setup = new SetupRequest();
                HttpEntity<SetupRequest> entity = new HttpEntity<>(setup);
-               ResponseEntity<Void> voidResponse = restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class);
+               ResponseEntity<Void> voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.POST, entity,
+                               Void.class);
                Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful());
        }
 
diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java
new file mode 100644 (file)
index 0000000..509dda7
--- /dev/null
@@ -0,0 +1,103 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property and Nokia
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================LICENSE_END===================================
+ */
+package org.oransc.ric.portal.dashboard.controller;
+
+import java.lang.invoke.MethodHandles;
+import java.net.URI;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+
+public class PortalRestCentralServiceTest extends AbstractControllerTest {
+
+       private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+       // paths are hardcoded here exactly like the EPSDK-FW library :(
+
+       @Test
+       public void getAnalyticsTest() {
+               // paths are hardcoded here exactly like the EPSDK-FW library :(
+               URI uri = buildUri(null, PortalApiConstants.API_PREFIX, "/analytics");
+               logger.info("Invoking {}", uri);
+               ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
+               // No Portal is available so this always fails
+               Assertions.assertTrue(response.getStatusCode().is4xxClientError());
+       }
+
+       @Test
+       public void getLoginPageTest() {
+               URI uri = buildUri(null, DashboardConstants.LOGIN_PAGE);
+               logger.info("Invoking {}", uri);
+               ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
+               Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
+               Assertions.assertTrue(response.getBody().contains("Please log in"));
+       }
+
+       private HttpEntity<Object> getEntityWithHeaders(Object body) {
+               HttpHeaders headers = new HttpHeaders();
+               headers.set(WebSecurityMockConfiguration.PORTAL_USERNAME_HEADER_KEY,
+                               WebSecurityMockConfiguration.PORTAL_USERNAME_HEADER_KEY);
+               headers.set(WebSecurityMockConfiguration.PORTAL_PASSWORD_HEADER_KEY,
+                               WebSecurityMockConfiguration.PORTAL_PASSWORD_HEADER_KEY);
+               HttpEntity<Object> entity = new HttpEntity<>(body, headers);
+               return entity;
+       }
+
+       @Test
+       public void createUserTest() {
+               final String loginId = "login1";
+               URI create = buildUri(null, PortalApiConstants.API_PREFIX, "user");
+               logger.info("Invoking {}", create);
+               EcompUser user = new EcompUser();
+               user.setLoginId(loginId);
+               HttpEntity<Object> requestEntity = getEntityWithHeaders(user);
+               ResponseEntity<String> response = restTemplate.exchange(create, HttpMethod.POST, requestEntity, String.class);
+               Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
+       }
+
+       @Test
+       public void updateUserTest() {
+               final String loginId = "login2";
+               URI create = buildUri(null, PortalApiConstants.API_PREFIX, "user");
+               logger.info("Invoking {}", create);
+               EcompUser user = new EcompUser();
+               user.setLoginId(loginId);
+               HttpEntity<Object> requestEntity = getEntityWithHeaders(user);
+               // Create
+               ResponseEntity<String> response = restTemplate.exchange(create, HttpMethod.POST, requestEntity, String.class);
+               Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
+               URI update = buildUri(null, PortalApiConstants.API_PREFIX, "user", loginId);
+               user.setEmail("user@company.org");
+               requestEntity = getEntityWithHeaders(user);
+               response = restTemplate.exchange(update, HttpMethod.POST, requestEntity, String.class);
+               Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
+       }
+
+}
diff --git a/webapp-backend/src/test/resources/key.properties b/webapp-backend/src/test/resources/key.properties
new file mode 100644 (file)
index 0000000..ff9d220
--- /dev/null
@@ -0,0 +1,22 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+# Test properties for the EPSDK-FW library.
+# This file must be present on the Java classpath.
+
+cipher.enc.key = bogus
diff --git a/webapp-backend/src/test/resources/portal.properties b/webapp-backend/src/test/resources/portal.properties
new file mode 100644 (file)
index 0000000..94e7391
--- /dev/null
@@ -0,0 +1,26 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property and Nokia
+# %%
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# 
+#      http://www.apache.org/licenses/LICENSE-2.0
+# 
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ========================LICENSE_END===================================
+
+# Test properties for the EPSDK-FW library.
+# This file must be present on the Java classpath.
+
+portal.api.impl.class = org.oransc.ric.portal.dashboard.portalapi.PortalRestCentralServiceImpl
+role_access_centralized = remote
+ecomp_redirect_url = https://www.wikipedia.org
+ecomp_rest_url = http://localhost/portal
+ueb_app_key = abcdef1234567890
index cbf353f..5d923c3 100644 (file)
@@ -50,9 +50,9 @@ export class ANRXappDataSource extends DataSource<ANRNeighborCellRelation> {
     this.loadingSubject.next(true);
     this.anrXappService.getNcrtInfo(ggnodeb, servingCellNrcgi, neighborCellNrpci)
       .pipe(
-        catchError( (err: HttpErrorResponse) => {
-          console.log('ANRXappDataSource failed: ' + err.message);
-          this.notificationService.error('Failed to get data.');
+        catchError( (her: HttpErrorResponse) => {
+          console.log('ANRXappDataSource failed: ' + her.message);
+          this.notificationService.error('Failed to get data: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))
index 341754b..35c910d 100644 (file)
@@ -69,7 +69,7 @@ export class AppControlComponent implements OnInit {
   }
 
   onUndeployApp(app: XappControlRow): void {
-    this.confirmDialogService.openConfirmDialog('Are you sure you want to undeploy xApp ' + app.xapp + '?')
+    this.confirmDialogService.openConfirmDialog('Are you sure you want to undeploy App ' + app.xapp + '?')
       .afterClosed().subscribe( (res: boolean) => {
         if (res) {
           this.appMgrSvc.undeployXapp(app.xapp).subscribe(
@@ -77,14 +77,19 @@ export class AppControlComponent implements OnInit {
               this.dataSource.loadTable();
               switch (httpResponse.status) {
                 case 200:
-                  this.notificationService.success('xApp undeployed successfully!');
+                  this.notificationService.success('App undeployed successfully!');
                   break;
                 default:
-                  this.notificationService.warn('xApp undeploy failed.');
+                  this.notificationService.warn('App undeploy failed.');
               }
             },
-            ( (error: HttpErrorResponse) => {
-              this.notificationService.warn(error.message);
+            ( (her: HttpErrorResponse) => {
+              // the error field should have an ErrorTransport object
+              let msg = her.message;
+              if (her.error && her.error.message) {
+                msg = her.error.message;
+              }
+              this.notificationService.warn('App undeploy failed: ' + msg);
             })
           );
         }
index fa98dfa..6a19286 100644 (file)
@@ -59,9 +59,9 @@ export class AppControlDataSource extends DataSource<XappControlRow> {
     this.loadingSubject.next(true);
     this.appMgrSvc.getDeployed()
       .pipe(
-        catchError( (err: HttpErrorResponse) => {
-          console.log('AppControlDataSource failed: ' + err.message);
-          this.notificationService.error('Failed to get applications.');
+        catchError( (her: HttpErrorResponse) => {
+          console.log('AppControlDataSource failed: ' + her.message);
+          this.notificationService.error('Failed to get applications: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))
index 4eab408..21860ac 100644 (file)
@@ -60,11 +60,16 @@ export class CatalogComponent implements OnInit {
         if (res) {
           this.appMgrService.deployXapp(app.name).subscribe(
             (response: HttpResponse<Object>) => {
-              this.notificationService.success('Deploy succeeded!');
+              this.notificationService.success('App deploy succeeded!');
             },
-            (error: HttpErrorResponse) => {
-              this.notificationService.warn('Deploy failed: ' + error.message);
-            }
+            ( (her: HttpErrorResponse) => {
+              // the error field should have an ErrorTransport object
+              let msg = her.message;
+              if (her.error && her.error.message) {
+                msg = her.error.message;
+              }
+              this.notificationService.warn('App deploy failed: ' + msg);
+            })
           );
         }
       }
index 5c9ac94..fb54b84 100644 (file)
@@ -50,9 +50,9 @@ export class CatalogDataSource extends DataSource<XMDeployableApp> {
     this.loadingSubject.next(true);
     this.appMgrSvc.getDeployable()
       .pipe(
-        catchError( (err: HttpErrorResponse) => {
-          console.log('CatalogDataSource failed: ' + err.message);
-          this.notificationService.error('Failed to get applications.');
+        catchError( (her: HttpErrorResponse) => {
+          console.log('CatalogDataSource failed: ' + her.message);
+          this.notificationService.error('Failed to get applications: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))
index 2b586ff..2ab5f7c 100644 (file)
@@ -20,7 +20,7 @@
 
 <!-- browse icons at https://material.io/tools/icons/?style=baseline -->
 <mat-nav-list>
-  <a mat-list-item routerLink="/login" (click)="onSidenavClose()">
+  <a mat-list-item routerLink="/" (click)="onSidenavClose()">
       <mat-icon>home</mat-icon> <span class="nav-caption">Home</span>
   </a>
   <a mat-list-item routerLink="/catalog" (click)="onSidenavClose()">
index 25442f7..7c63f13 100644 (file)
@@ -86,9 +86,14 @@ export class RanControlConnectDialogComponent implements OnInit {
                 this.notifService.success('Connect succeeded!');
                 this.dialogRef.close(true);
             },
-            ( (error: HttpErrorResponse) => {
+            ( (her: HttpErrorResponse) => {
                 this.processing = false;
-                this.errorService.displayError('RAN Connection Failed: ' + error.message);
+                // the error field carries the server's response
+                let msg = her.message;
+                if (her.error && her.error.message) {
+                  msg = her.error.message;
+                }
+                this.errorService.displayError('Connect failed: ' + msg);
                 // keep the dialog open
             })
         );
index 26665ca..f5ef36c 100644 (file)
@@ -66,12 +66,17 @@ export class RanControlComponent implements OnInit {
           this.e2MgrSvc.nodebDelete().subscribe(
             ( response: HttpResponse<Object>) => {
               if (response.status === 200) {
-                this.notificationService.success('Disconnect all RAN Connections Succeeded!');
+                this.notificationService.success('Disconnect succeeded!');
                 this.dataSource.loadTable();
               }
             },
-            ( (error: HttpErrorResponse) => {
-              this.errorDialogService.displayError(aboutError + error.message);
+            ( (her: HttpErrorResponse) => {
+              // the error field should have an ErrorTransport object
+              let msg = her.message;
+              if (her.error && her.error.message) {
+                msg = her.error.message;
+              }
+              this.errorDialogService.displayError('Disconnect failed: ' + msg);
             })
           );
         }
index d919f4e..50626b5 100644 (file)
@@ -47,9 +47,9 @@ export class RANControlDataSource extends DataSource<E2RanDetails> {
     this.loadingSubject.next(true);
     this.e2MgrSvcservice.getRan()
       .pipe(
-        catchError( (err: HttpErrorResponse) => {
-          console.log('RANControlDataSource failed: ' + err.message);
-          this.notificationService.error('Failed to get RAN details.');
+        catchError( (her: HttpErrorResponse) => {
+          console.log('RANControlDataSource failed: ' + her.message);
+          this.notificationService.error('Failed to get RAN details: ' + her.message);
           return of([]);
         }),
         finalize( () =>  this.loadingSubject.next(false) )
index 255dbca..e4e667a 100644 (file)
@@ -134,8 +134,8 @@ export class StatsService {
             this.delayMax = res[5].value;
             this.loadMax = res[6].value;
         },
-        (err: HttpErrorResponse) => {
-            console.log (err.message);
+        (her: HttpErrorResponse) => {
+            console.log ('loadConfig failed: ' + her.message);
           });
     }
 }
index dcc0369..f53a2d3 100644 (file)
@@ -50,9 +50,9 @@ export class UserDataSource extends DataSource<DashboardUser> {
     this.loadingSubject.next(true);
     this.dashboardSvc.getUsers()
       .pipe(
-        catchError( (err: HttpErrorResponse) => {
-          console.log('UserDataSource failed: ' + err.message);
-          this.notificationService.error('Failed to get users.');
+        catchError( (her: HttpErrorResponse) => {
+          console.log('UserDataSource failed: ' + her.message);
+          this.notificationService.error('Failed to get users: ' + her.message);
           return of([]);
         }),
         finalize(() => this.loadingSubject.next(false))