Non-RT RIC Dashboard

First commit

Change-Id: I9e140d31d65d13df3ce07f6b87eac250ee952eab
Issue-ID: NONRTRIC-61
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
diff --git a/dashboard/webapp-backend/.gitignore b/dashboard/webapp-backend/.gitignore
new file mode 100644
index 0000000..02dbd44
--- /dev/null
+++ b/dashboard/webapp-backend/.gitignore
@@ -0,0 +1,34 @@
+/.classpath
+/.project
+/.settings
+/target/
+/logs/
+/.mvn/wrapper/maven-wrapper.jar
+/logs/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+/build/
+
+/application-tlab2.properties
+/application.properties
+/dashboard-users.json
diff --git a/dashboard/webapp-backend/README.md b/dashboard/webapp-backend/README.md
new file mode 100644
index 0000000..5dd1b1e
--- /dev/null
+++ b/dashboard/webapp-backend/README.md
@@ -0,0 +1,25 @@
+# RIC Dashboard Web Application Backend
+
+The RIC Dashboard back-end provides REST services to the Dashboard
+front-end Typescript features running in the user's browser.  For
+production use, it also serves the Angular application files.
+
+Please see the documentation in the docs/ folder.
+
+The backend server publishes live API documentation at the
+URL `http://your-host-name-here:8080/swagger-ui.html`
+
+## License
+
+Copyright (C) 2019 AT&T Intellectual Property. All rights reserved.
+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.
diff --git a/dashboard/webapp-backend/config/.gitignore b/dashboard/webapp-backend/config/.gitignore
new file mode 100644
index 0000000..edd66f1
--- /dev/null
+++ b/dashboard/webapp-backend/config/.gitignore
@@ -0,0 +1,2 @@
+/key.properties
+/portal.properties
diff --git a/dashboard/webapp-backend/config/key.properties.template b/dashboard/webapp-backend/config/key.properties.template
new file mode 100644
index 0000000..a8d44e3
--- /dev/null
+++ b/dashboard/webapp-backend/config/key.properties.template
@@ -0,0 +1,21 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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/dashboard/webapp-backend/config/portal.properties.template b/dashboard/webapp-backend/config/portal.properties.template
new file mode 100644
index 0000000..8c7fec7
--- /dev/null
+++ b/dashboard/webapp-backend/config/portal.properties.template
@@ -0,0 +1,34 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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
diff --git a/dashboard/webapp-backend/pom.xml b/dashboard/webapp-backend/pom.xml
new file mode 100644
index 0000000..ec4fdea
--- /dev/null
+++ b/dashboard/webapp-backend/pom.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--<![CDATA[
+========================LICENSE_START=================================
+O-RAN-SC
+%%
+Copyright (C) 2019 AT&T Intellectual Property
+%%
+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===================================
+]]>-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
+		<artifactId>ric-dash-parent</artifactId>
+		<version>1.2.5-SNAPSHOT</version>
+	</parent>
+	<artifactId>ric-dash-be</artifactId>
+	<name>RIC Dashboard Webapp backend</name>
+	<properties>
+		<springfox.version>2.9.2</springfox.version>
+		<!-- 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>
+		<!-- Platform components -->
+		<dependency>
+			<groupId>org.o-ran-sc.ric.plt.a1controller.client</groupId>
+			<artifactId>a1-controller-client</artifactId>
+			<version>0.1.0-SNAPSHOT</version>
+		</dependency>
+		<dependency>
+			<groupId>org.onap.portal.sdk</groupId>
+			<artifactId>epsdk-fw</artifactId>
+			<version>2.6.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>
+		</dependency>
+		<dependency>
+			<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>
+		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-core</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger2</artifactId>
+			<version>${springfox.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>io.springfox</groupId>
+			<artifactId>springfox-swagger-ui</artifactId>
+			<version>${springfox.version}</version>
+		</dependency>
+		<!-- Test dependencies -->
+		<!-- Mockito supports development, not just testing -->
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-api</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-engine</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.junit.platform</groupId>
+			<artifactId>junit-platform-launcher</artifactId>
+			<!-- Override Spring-Boot choice for Eclipse -->
+			<version>1.4.2</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+			<plugin>
+				<!-- Most configuration and all execution is inherited -->
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>license-maven-plugin</artifactId>
+				<configuration>
+					<roots>
+						<root>src</root>
+					</roots>
+					<excludes>
+						<exclude>**/*.json</exclude>
+					</excludes>
+				</configuration>
+			</plugin>
+			<!-- Add the build number to the jar manifest. Spring-Boot uses a complex 
+				packaging process that makes access to the original Manifest.MF very difficult. 
+				However, Java provides access to the implementation version for a package, 
+				so cram the build number into there. -->
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-jar-plugin</artifactId>
+				<configuration>
+					<archive>
+						<manifest>
+							<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+						</manifest>
+						<manifestEntries>
+							<Implementation-Version>${project.version}-b${build.number}</Implementation-Version>
+						</manifestEntries>
+					</archive>
+				</configuration>
+			</plugin>
+			<plugin>
+				<artifactId>maven-resources-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>copy-resources</id>
+						<phase>validate</phase>
+						<goals>
+							<goal>copy-resources</goal>
+						</goals>
+						<configuration>
+							<outputDirectory>${project.build.directory}/classes/resources/</outputDirectory>
+							<resources>
+								<resource>
+									<directory>${project.parent.basedir}/webapp-frontend/dist/dashApp/</directory>
+								</resource>
+							</resources>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<!-- do not deploy a jar or pom file -->
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-deploy-plugin</artifactId>
+				<configuration>
+					<skip>true</skip>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.8.4</version>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<!-- https://stackoverflow.com/questions/39126226/fabric8-springboot-full-example -->
+			<plugin>
+				<groupId>io.fabric8</groupId>
+				<artifactId>docker-maven-plugin</artifactId>
+				<version>0.30.0</version>
+				<configuration>
+					<verbose>true</verbose>
+					<!-- environment variables supplied by Jenkins -->
+					<pullRegistry>${env.CONTAINER_PULL_REGISTRY}</pullRegistry>
+					<pushRegistry>${env.CONTAINER_PUSH_REGISTRY}</pushRegistry>
+					<images>
+						<image>
+							<!-- Specify a tag to avoid default tag "latest" -->
+							<!-- Avoid maven artifact name here -->
+							<name>ric-dashboard:${project.version}</name>
+							<build>
+								<from>openjdk:11-jre-slim</from>
+								<tags>
+									<!-- Add tag with build number -->
+									<tag>${project.version}</tag>
+								</tags>
+								<assembly>
+									<descriptorRef>artifact</descriptorRef>
+								</assembly>
+								<runCmds>
+									<!-- Ensure logs dir exists and is world writable -->
+									<runCmd>mkdir /logs</runCmd>
+									<runCmd>chmod -R 777 /logs</runCmd>
+								</runCmds>
+								<cmd>
+									<!-- Include maven dir on classpath for prop files -->
+									<exec>
+										<arg>java</arg>
+										<arg>-Xms128m</arg>
+										<arg>-Xmx256m</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>-Djava.security.egd=file:/dev/./urandom</arg>
+										<arg>org.springframework.boot.loader.PropertiesLauncher</arg>
+									</exec>
+								</cmd>
+							</build>
+						</image>
+					</images>
+				</configuration>
+				<!-- build Docker images in install phase, push in deploy phase -->
+				<executions>
+					<execution>
+						<goals>
+							<goal>build</goal>
+							<goal>push</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java
new file mode 100644
index 0000000..333c532
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java
@@ -0,0 +1,60 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication
+// Limit scan to dashboard classes; exclude generated API classes
+@ComponentScan("org.oransc.ric.portal.dashboard")
+public class DashboardApplication {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	public static void main(String[] args) throws IOException {
+		SpringApplication.run(DashboardApplication.class, args);
+		// Ensure this appears on the console by using level WARN
+		logger.warn("main: version '{}' successful start",
+				getImplementationVersion(MethodHandles.lookup().lookupClass()));
+	}
+
+	/**
+	 * Gets version details for the specified class.
+	 * 
+	 * @param clazz
+	 *                  Class to get the version
+	 * 
+	 * @return the value of the MANIFEST.MF property Implementation-Version as
+	 *         written by maven when packaged in a jar; 'unknown' otherwise.
+	 */
+	public static String getImplementationVersion(Class<?> clazz) {
+		String classPath = clazz.getResource(clazz.getSimpleName() + ".class").toString();
+		return classPath.startsWith("jar") ? clazz.getPackage().getImplementationVersion() : "unknown-not-jar";
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java
new file mode 100644
index 0000000..d61ce1d
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java
@@ -0,0 +1,44 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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;
+
+public abstract class DashboardConstants {
+
+	private DashboardConstants() {
+		// Sonar insists on hiding the constructor
+	}
+
+	public static final String ENDPOINT_PREFIX = "/api";
+	// Factor out method names used in multiple controllers
+	public static final String VERSION_METHOD = "version";
+	public static final String APP_NAME_AC = "AC";
+	public static final String APP_NAME_MC = "MC";
+	// 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;
+	public static final String A1_CONTROLLER_USERNAME = "admin";
+	public static final String A1_CONTROLLER_PASSWORD = "Kp8bJ4SXszM0WXlhak3eHlcse2gAw84vaoGGmJvUy2U";
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardUserManager.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardUserManager.java
new file mode 100644
index 0000000..c5fd101
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardUserManager.java
@@ -0,0 +1,184 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.File;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.restful.domain.EcompRole;
+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 simple 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());
+
+	// This default value is only useful for development and testing.
+	public static final String USER_FILE_PATH = "dashboard-users.json";
+
+	private final File userFile;
+	private final List<EcompUser> users;
+
+	/**
+	 * Development/test-only constructor that uses default file path.
+	 * 
+	 * @param clear
+	 *                  If true, start empty and remove any existing file.
+	 * 
+	 * @throws IOException
+	 *                         On file error
+	 */
+	public DashboardUserManager(boolean clear) throws IOException {
+		this(USER_FILE_PATH);
+		if (clear) {
+			logger.debug("ctor: removing file {}", userFile.getAbsolutePath());
+			File f = new File(DashboardUserManager.USER_FILE_PATH);
+			if (f.exists())
+				f.delete();
+			users.clear();
+		}
+	}
+
+	/**
+	 * Constructur that accepts a file path
+	 * 
+	 * @param userFilePath
+	 *                         File path
+	 * @throws IOException
+	 *                         If file cannot be read
+	 */
+	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 current users.
+	 * 
+	 * @return List of EcompUser objects, possibly empty
+	 */
+	public List<EcompUser> getUsers() {
+		return this.users;
+	}
+
+	/**
+	 * 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);
+		}
+	}
+
+	// Test infrastructure
+	public static void main(String[] args) throws Exception {
+		DashboardUserManager dum = new DashboardUserManager(false);
+		EcompUser user = new EcompUser();
+		user.setActive(true);
+		user.setLoginId("demo");
+		user.setFirstName("First");
+		user.setLastName("Last");
+		EcompRole role = new EcompRole();
+		role.setId(1L);
+		role.setName(DashboardConstants.ROLE_NAME_ADMIN);
+		Set<EcompRole> roles = new HashSet<>();
+		roles.add(role);
+		user.setRoles(roles);
+		dum.createUser(user);
+		logger.debug("Created user {}", user);
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/A1ControllerConfiguration.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/A1ControllerConfiguration.java
new file mode 100644
index 0000000..ffdcacd
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/A1ControllerConfiguration.java
@@ -0,0 +1,74 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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.lang.invoke.MethodHandles;
+import org.oransc.ric.a1controller.client.api.A1ControllerApi;
+import org.oransc.ric.a1controller.client.invoker.ApiClient;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+
+/**
+ * Creates an A1 controller client as a bean to be managed by the Spring
+ * container.
+ */
+@Configuration
+@Profile("!test")
+public class A1ControllerConfiguration {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+	public static final String A1_CONTROLLER_USERNAME = DashboardConstants.A1_CONTROLLER_USERNAME;
+	public static final String A1_CONTROLLER_PASSWORD = DashboardConstants.A1_CONTROLLER_PASSWORD;
+
+	// Populated by the autowired constructor
+	private final String a1ControllerUrl;
+
+	@Autowired
+	public A1ControllerConfiguration(@Value("${a1controller.url.prefix}") final String urlPrefix, //
+			@Value("${a1controller.url.suffix}") final String urlSuffix) {
+		logger.debug("ctor prefix '{}' suffix '{}'", urlPrefix, urlSuffix);
+		a1ControllerUrl = new DefaultUriBuilderFactory(urlPrefix.trim()).builder().path(urlSuffix.trim()).build().normalize()
+				.toString();
+		logger.info("Configuring A1 Controller at URL {}", a1ControllerUrl);
+	}
+
+	private ApiClient apiClient() {
+		ApiClient apiClient = new ApiClient(new RestTemplate());
+		apiClient.setBasePath(a1ControllerUrl);
+		apiClient.setUsername(A1_CONTROLLER_USERNAME);
+		apiClient.setPassword(A1_CONTROLLER_PASSWORD);
+		return apiClient;
+	}
+
+	@Bean
+	// The bean (method) name must be globally unique
+	public A1ControllerApi a1ControllerApi() {
+		return new A1ControllerApi(apiClient());
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/AdminConfiguration.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/AdminConfiguration.java
new file mode 100644
index 0000000..696d74f
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/AdminConfiguration.java
@@ -0,0 +1,58 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.oransc.ric.portal.dashboard.DashboardUserManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+/**
+ * Creates an instance of the user manager.
+ */
+@Configuration
+@Profile("!test")
+public class AdminConfiguration {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	// Populated by the autowired constructor
+	private final String userfile;
+
+	@Autowired
+	public AdminConfiguration(@Value("${userfile}") final String userfile) {
+		logger.debug("ctor userfile '{}'", userfile);
+		this.userfile = userfile;
+	}
+
+	@Bean
+	// The bean (method) name must be globally unique
+	public DashboardUserManager userManager() throws IOException {
+		return new DashboardUserManager(userfile);
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/PortalApiConfiguration.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/PortalApiConfiguration.java
new file mode 100644
index 0000000..f318cad
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/PortalApiConfiguration.java
@@ -0,0 +1,57 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.lang.invoke.MethodHandles;
+
+import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+@Configuration
+@Profile("!test")
+public class PortalApiConfiguration {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	/**
+	 * Instantiates the EPSDK-FW servlet that implements the API called by Portal.
+	 * 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() {
+		logger.debug("portalApiProxyServletBean");
+		PortalRestAPIProxy servlet = new PortalRestAPIProxy();
+		final ServletRegistrationBean<PortalRestAPIProxy> servletBean = new ServletRegistrationBean<>(servlet,
+				PortalApiConstants.API_PREFIX + "/*");
+		servletBean.setName("PortalRestApiProxyServlet");
+		return servletBean;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java
new file mode 100644
index 0000000..3a87782
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java
@@ -0,0 +1,44 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SwaggerConfiguration.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SwaggerConfiguration.java
new file mode 100644
index 0000000..b4bb0a3
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SwaggerConfiguration.java
@@ -0,0 +1,68 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.oransc.ric.portal.dashboard.DashboardApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api
+ */
+@Configuration
+@EnableSwagger2
+public class SwaggerConfiguration {
+
+	/**
+	 * @return new Docket
+	 */
+	@Bean
+	public Docket api() {
+		return new Docket(DocumentationType.SWAGGER_2).select() //
+				.apis(RequestHandlerSelectors.basePackage(DashboardApplication.class.getPackage().getName())) //
+				.paths(PathSelectors.any()) //
+				.build() //
+				.apiInfo(apiInfo());
+	}
+
+	private ApiInfo apiInfo() {
+		final String version = DashboardApplication.class.getPackage().getImplementationVersion();
+		return new ApiInfoBuilder() //
+				.title("RIC Dashboard backend") //
+				.description("Proxies access to RIC services.")//
+				.termsOfServiceUrl("Terms of service") //
+				.contact(new Contact("RIC Dashboard Dev Team", //
+						"http://no-docs-yet.org/", //
+						"noreply@O-RAN-SC.org")) //
+				.license("Apache 2.0 License").licenseUrl("http://www.apache.org/licenses/LICENSE-2.0") //
+				.version(version == null ? "version not available" : version) //
+				.build();
+	}
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java
new file mode 100644
index 0000000..b912500
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java
@@ -0,0 +1,125 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 java.lang.reflect.InvocationTargetException;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.oransc.ric.portal.dashboard.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.controller.A1Controller;
+import org.oransc.ric.portal.dashboard.controller.SimpleErrorController;
+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.Autowired;
+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;
+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;
+import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
+
+@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("${portalapi.security}")
+	private Boolean portalapiSecurity;
+	@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;
+
+	@Autowired
+	DashboardUserManager userManager;
+
+	@Override
+    protected void configure(HttpSecurity http) throws Exception {
+		logger.debug("configure: portalapi.username {}", userName);
+		// A chain of ".and()" always baffles me
+		http.authorizeRequests().anyRequest().authenticated();
+		http.headers().frameOptions().disable();
+		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 + "/**", //
+			A1Controller.CONTROLLER_PATH + "/" + A1Controller.VERSION_METHOD, //					
+			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,
+			IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+		return new PortalAuthManager(appName, userName, password, decryptor, userCookie);
+	}
+
+	/*
+	 * 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,
+			IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+		PortalAuthenticationFilter portalAuthenticationFilter = new PortalAuthenticationFilter(portalapiSecurity,
+				portalAuthManagerBean(), this.userManager);
+		return portalAuthenticationFilter;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/A1Controller.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/A1Controller.java
new file mode 100644
index 0000000..386d43c
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/A1Controller.java
@@ -0,0 +1,220 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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.util.List;
+import javax.servlet.http.HttpServletResponse;
+import org.oransc.ric.a1controller.client.api.A1ControllerApi;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidPISchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidPISchemaInput;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidSchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidSchemaInput;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidSchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidSchemaInput;
+import org.oransc.ric.a1controller.client.model.InputNRRidSchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidSchemaInput;
+import org.oransc.ric.a1controller.client.model.OutputDescNamePTSchema;
+import org.oransc.ric.a1controller.client.model.OutputDescNamePTSchemaOutput;
+import org.oransc.ric.a1controller.client.model.OutputPISchema;
+import org.oransc.ric.a1controller.client.model.OutputPIidsListSchema;
+import org.oransc.ric.a1controller.client.model.OutputPTidsListSchema;
+import org.oransc.ric.portal.dashboard.DashboardApplication;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.model.PolicyInstance;
+import org.oransc.ric.portal.dashboard.model.PolicyInstances;
+import org.oransc.ric.portal.dashboard.model.PolicyType;
+import org.oransc.ric.portal.dashboard.model.PolicyTypes;
+import org.oransc.ric.portal.dashboard.model.SuccessTransport;
+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.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import io.swagger.annotations.ApiOperation;
+
+/**
+ * Proxies calls from the front end to the A1 Controller via the A1 Mediator
+ * API.
+ *
+ * If a method throws RestClientResponseException, it is handled by
+ * {@link CustomResponseEntityExceptionHandler#handleProxyMethodException(Exception,
+ * org.springframework.web.context.request.WebRequest)}
+ * which returns status 502. All other exceptions are handled by Spring which
+ * returns status 500.
+ */
+@RestController
+@RequestMapping(value = A1Controller.CONTROLLER_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
+public class A1Controller {
+
+    private static final String NEAR_RT_RIC_ID = "NearRtRic1";
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	// Publish paths in constants so tests are easy to write
+	public static final String CONTROLLER_PATH = DashboardConstants.ENDPOINT_PREFIX + "/policy";
+	// Endpoints
+	public static final String VERSION_METHOD = DashboardConstants.VERSION_METHOD;
+	public static final String POLICY_TYPES_METHOD = "policytypes";
+	public static final String POLICY_TYPE_ID_NAME = "policy_type_id";
+	public static final String POLICIES_NAME = "policies";
+	public static final String POLICY_INSTANCE_ID_NAME = "policy_instance_id";
+
+	// Populated by the autowired constructor
+	private final A1ControllerApi a1ControllerApi;
+
+	@Autowired
+	public A1Controller(final A1ControllerApi A1ControllerApi) {
+		Assert.notNull(A1ControllerApi, "API must not be null");
+		this.a1ControllerApi = A1ControllerApi;
+		if (logger.isDebugEnabled())
+			logger.debug("ctor: configured with client type {}", A1ControllerApi.getClass().getName());
+	}
+
+	@ApiOperation(value = "Gets the A1 client library MANIFEST.MF property Implementation-Version.",
+	        response = SuccessTransport.class)
+	@GetMapping(VERSION_METHOD)
+	// No role required
+	public SuccessTransport getA1ControllerClientVersion() {
+		return new SuccessTransport(200, DashboardApplication.getImplementationVersion(A1ControllerApi.class));
+	}
+
+	/*
+	 * The fields are defined in the A1Control Typescript interface.
+	 */
+	@ApiOperation(value = "Gets the policy types from Near Realtime-RIC via the A1 Controller API")
+	@GetMapping(POLICY_TYPES_METHOD)
+	@Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
+	public Object getAllPolicyTypes(HttpServletResponse response) {
+		logger.debug("getAllPolicyTypes");
+		InputNRRidSchemaInput nrrid = new InputNRRidSchemaInput();
+		nrrid.setNearRtRicId(NEAR_RT_RIC_ID);
+		InputNRRidSchema inputSchema = new InputNRRidSchema();
+		inputSchema.setInput(nrrid);
+		OutputPTidsListSchema outputPTidsListSchema =
+		        a1ControllerApi.a1ControllerGetAllPolicyTypes(inputSchema);
+		List<Integer> policyTypeIds = outputPTidsListSchema.getOutput().getPolicyTypeIdList();
+		PolicyTypes policyTypes = new PolicyTypes();
+		InputNRRidPTidSchema typeSchema = new InputNRRidPTidSchema();
+		InputNRRidPTidSchemaInput typeId = new InputNRRidPTidSchemaInput();
+		typeId.setNearRtRicId(NEAR_RT_RIC_ID);
+		for (Integer policyTypeId : policyTypeIds) {
+			typeId.setPolicyTypeId(policyTypeId);
+			typeSchema.setInput(typeId);
+			OutputDescNamePTSchema controllerGetPolicyType =
+			        a1ControllerApi.a1ControllerGetPolicyType(typeSchema);
+			OutputDescNamePTSchemaOutput policyTypeSchema = controllerGetPolicyType.getOutput();
+			PolicyType type = new PolicyType(policyTypeId, policyTypeSchema.getName(),
+					policyTypeSchema.getDescription(), policyTypeSchema.getPolicyType().toString());
+			policyTypes.add(type);
+		}
+		return policyTypes;
+	}
+
+	@ApiOperation(value = "Returns the policy instances for the given policy type.")
+	@GetMapping(POLICY_TYPES_METHOD + "/{" + POLICY_TYPE_ID_NAME + "}/" + POLICIES_NAME)
+	@Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
+	public Object getPolicyInstances(@PathVariable(POLICY_TYPE_ID_NAME) String policyTypeIdString) {
+		logger.debug("getPolicyInstances {}", policyTypeIdString);
+		InputNRRidPTidSchemaInput typeIdInput = new InputNRRidPTidSchemaInput();
+		typeIdInput.setNearRtRicId(NEAR_RT_RIC_ID);
+		Integer policyTypeId = Integer.decode(policyTypeIdString);
+		typeIdInput.setPolicyTypeId(policyTypeId);
+		InputNRRidPTidSchema inputSchema = new InputNRRidPTidSchema();
+		inputSchema.setInput(typeIdInput);
+		OutputPIidsListSchema controllerGetAllInstancesForType =
+		        a1ControllerApi.a1ControllerGetAllInstancesForType(inputSchema);
+		List<String> instancesForType = controllerGetAllInstancesForType.getOutput().getPolicyInstanceIdList();
+		PolicyInstances instances = new PolicyInstances();
+		InputNRRidPTidPIidSchemaInput instanceIdInput = new InputNRRidPTidPIidSchemaInput();
+		instanceIdInput.setNearRtRicId(NEAR_RT_RIC_ID);
+		instanceIdInput.setPolicyTypeId(policyTypeId);
+		InputNRRidPTidPIidSchema instanceInputSchema = new InputNRRidPTidPIidSchema();
+		for (String instanceId : instancesForType) {
+			instanceIdInput.setPolicyInstanceId(instanceId);
+			instanceInputSchema.setInput(instanceIdInput);
+			OutputPISchema policyInstance =
+			        a1ControllerApi.a1ControllerGetPolicyInstance(instanceInputSchema);
+			PolicyInstance instance =
+			        new PolicyInstance(instanceId, policyInstance.getOutput().getPolicyInstance());
+			instances.add(instance);
+		}
+		return instances;
+	}
+
+	@ApiOperation(value = "Returns a policy instance of a type")
+	@GetMapping(POLICY_TYPES_METHOD + "/{" + POLICY_TYPE_ID_NAME + "}/" + POLICIES_NAME + "/{"
+	        + POLICY_INSTANCE_ID_NAME + "}")
+	@Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD })
+	public Object getPolicyInstance(@PathVariable(POLICY_TYPE_ID_NAME) String policyTypeIdString,
+			@PathVariable(POLICY_INSTANCE_ID_NAME) String policyInstanceId) {
+		logger.debug("getPolicyInstance {}:{}", policyTypeIdString, policyInstanceId);
+		InputNRRidPTidPIidSchemaInput instanceIdInput = new InputNRRidPTidPIidSchemaInput();
+		instanceIdInput.setNearRtRicId(NEAR_RT_RIC_ID);
+		instanceIdInput.setPolicyTypeId(Integer.decode(policyTypeIdString));
+		instanceIdInput.setPolicyInstanceId(policyInstanceId);
+		InputNRRidPTidPIidSchema inputSchema = new InputNRRidPTidPIidSchema();
+		inputSchema.setInput(instanceIdInput);
+		OutputPISchema policyInstance = a1ControllerApi.a1ControllerGetPolicyInstance(inputSchema);
+		return policyInstance.getOutput().getPolicyInstance();
+	}
+
+	@ApiOperation(value = "Creates the policy instances for the given policy type.")
+	@PutMapping(POLICY_TYPES_METHOD + "/{" + POLICY_TYPE_ID_NAME + "}/" + POLICIES_NAME + "/{"
+	        + POLICY_INSTANCE_ID_NAME + "}")
+	@Secured({ DashboardConstants.ROLE_ADMIN })
+	public void putPolicyInstance(@PathVariable(POLICY_TYPE_ID_NAME) String policyTypeIdString,
+			@PathVariable(POLICY_INSTANCE_ID_NAME) String policyInstanceId, @RequestBody String instance) {
+		logger.debug("putPolicyInstance typeId: {}, instanceId: {}, instance: {}", policyTypeIdString,
+		        policyInstanceId, instance);
+		InputNRRidPTidPIidPISchemaInput createInstanceInput = new InputNRRidPTidPIidPISchemaInput();
+		createInstanceInput.setNearRtRicId(NEAR_RT_RIC_ID);
+		createInstanceInput.setPolicyTypeId(Integer.decode(policyTypeIdString));
+		createInstanceInput.setPolicyInstanceId(policyInstanceId);
+		createInstanceInput.setPolicyInstance(instance);
+		InputNRRidPTidPIidPISchema inputSchema = new InputNRRidPTidPIidPISchema();
+		inputSchema.setInput(createInstanceInput);
+		a1ControllerApi.a1ControllerCreatePolicyInstance(inputSchema);
+	}
+
+	@ApiOperation(value = "Deletes the policy instances for the given policy type.")
+	@DeleteMapping(POLICY_TYPES_METHOD + "/{" + POLICY_TYPE_ID_NAME + "}/" + POLICIES_NAME + "/{"
+			+ POLICY_INSTANCE_ID_NAME + "}")
+	@Secured({ DashboardConstants.ROLE_ADMIN })
+	public void deletePolicyInstance(@PathVariable(POLICY_TYPE_ID_NAME) String policyTypeIdString,
+			@PathVariable(POLICY_INSTANCE_ID_NAME) String policyInstanceId) {
+		logger.debug("deletePolicyInstance typeId: {}, instanceId: {}", policyTypeIdString, policyInstanceId);
+		InputNRRidPTidPIidSchemaInput instanceIdInput = new InputNRRidPTidPIidSchemaInput();
+		instanceIdInput.setNearRtRicId(NEAR_RT_RIC_ID);
+		instanceIdInput.setPolicyTypeId(Integer.decode(policyTypeIdString));
+		instanceIdInput.setPolicyInstanceId(policyInstanceId);
+		InputNRRidPTidPIidSchema inputSchema = new InputNRRidPTidPIidSchema();
+		inputSchema.setInput(instanceIdInput);
+		a1ControllerApi.a1ControllerDeletePolicyInstance(inputSchema);
+	}
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java
new file mode 100644
index 0000000..f5ecd10
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java
@@ -0,0 +1,82 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 org.oransc.ric.portal.dashboard.model.ErrorTransport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.client.HttpStatusCodeException;
+import org.springframework.web.client.RestClientResponseException;
+import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+/**
+ * Catches certain exceptions. This controller advice factors out try-catch
+ * blocks in many controller methods.
+ * 
+ * Also see:<br>
+ * https://www.baeldung.com/exception-handling-for-rest-with-spring
+ * https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services
+ */
+@ControllerAdvice
+public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
+
+	// Superclass has "logger" that is exposed here, so use a different name
+	private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	/**
+	 * Logs the error and generates a JSON response when a REST controller method
+	 * takes a RestClientResponseException. This is thrown by the Http client when a
+	 * remote method returns a non-2xx code. All the controller methods are proxies
+	 * in that they just forward the request along to a remote system, so if that
+	 * remote system fails, return 502 plus some details about the failure, rather
+	 * than the generic 500 that Spring-Boot will return on an uncaught exception.
+	 * 
+	 * Why 502? I quote: <blockquote>HTTP server received an invalid response from a
+	 * server it consulted when acting as a proxy or gateway.</blockquote>
+	 * 
+	 * @param ex
+	 *                    The exception
+	 * 
+	 * @param request
+	 *                    The original request
+	 * 
+	 * @return A response entity with status code 502 plus some details in the body.
+	 */
+	@ExceptionHandler({ RestClientResponseException.class })
+	public final ResponseEntity<ErrorTransport> handleProxyMethodException(Exception ex, WebRequest request) {
+		// Capture the full stack trace in the log.
+		log.error("handleProxyMethodException: request {}, exception {}", request.getDescription(false), ex);
+		if (ex instanceof HttpStatusCodeException) {
+			HttpStatusCodeException hsce = (HttpStatusCodeException) ex;
+			return new ResponseEntity<>(new ErrorTransport(hsce.getRawStatusCode(), hsce.getResponseBodyAsString(),
+					ex.toString(), request.getDescription(false)), HttpStatus.BAD_GATEWAY);
+		} else {
+			return new ResponseEntity<>(new ErrorTransport(500, ex), HttpStatus.BAD_GATEWAY);
+		}
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/Html5PathsController.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/Html5PathsController.java
new file mode 100644
index 0000000..ed2e905
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/Html5PathsController.java
@@ -0,0 +1,67 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.net.URL;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * Listens for requests to known Angular routes.
+ */
+@Controller
+public class Html5PathsController {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	/**
+	 * Forwards the browser to the index (main) page upon request of a known route.
+	 * This unfortunately requires duplication of the Angular route strings in the
+	 * path mappings on this method. Could switch to a regex pattern instead someday
+	 * if the routes change too often.
+	 * 
+	 * https://stackoverflow.com/questions/44692781/configure-spring-boot-to-redirect-404-to-a-single-page-app
+	 * 
+	 * @param request
+	 *                     HttpServletRequest
+	 * @param response
+	 *                     HttpServletResponse
+	 * @throws IOException
+	 *                         On error
+	 */
+	@RequestMapping(method = { RequestMethod.OPTIONS, RequestMethod.GET }, //
+			path = { "/catalog", "/control", "/stats", "/user" })
+	public void forwardAngularRoutes(HttpServletRequest request, HttpServletResponse response) throws IOException {
+		URL url = new URL(request.getScheme(), request.getServerName(), request.getServerPort(), "/index.html");
+		if (logger.isDebugEnabled())
+			logger.debug("forwardAngularRoutes: {} redirected to {}", request.getRequestURI(), url);
+		response.sendRedirect(url.toString());
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java
new file mode 100644
index 0000000..e2248e6
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/SimpleErrorController.java
@@ -0,0 +1,93 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.servlet.error.ErrorAttributes;
+import org.springframework.boot.web.servlet.error.ErrorController;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.context.request.ServletWebRequest;
+
+import springfox.documentation.annotations.ApiIgnore;
+
+/**
+ * Provides a controller which is invoked on any error within the Spring-managed
+ * context, including page not found, and redirects the caller to a custom error
+ * page. The caller is also redirected to this page if a REST controller takes
+ * an uncaught exception.
+ * 
+ * If trace is requested via request parameter ("?trace=true") and available,
+ * adds stack trace information to the standard JSON error response.
+ * 
+ * Excluded from Swagger API documentation.
+ * 
+ * https://stackoverflow.com/questions/25356781/spring-boot-remove-whitelabel-error-page
+ * https://www.baeldung.com/spring-boot-custom-error-page
+ */
+
+@ApiIgnore
+@Controller
+@RequestMapping(value = SimpleErrorController.ERROR_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
+public class SimpleErrorController implements ErrorController {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	public static final String ERROR_PATH = "/error";
+
+	private final ErrorAttributes errorAttributes;
+
+	@Autowired
+	public SimpleErrorController(ErrorAttributes errorAttributes) {
+		this.errorAttributes = errorAttributes;
+	}
+
+	@Override
+	public String getErrorPath() {
+		logger.warn("getErrorPath");
+		return ERROR_PATH;
+	}
+
+	@GetMapping
+	public String handleError(HttpServletRequest request) {
+		ServletWebRequest servletWebRequest = new ServletWebRequest(request);
+		Throwable t = errorAttributes.getError(servletWebRequest);
+		if (t != null)
+			logger.warn("handleError", t);
+		Map<String, Object> attributes = errorAttributes.getErrorAttributes(servletWebRequest, true);
+		attributes.forEach((attribute, value) -> {
+			logger.warn("handleError: {} -> {}", attribute, value);
+		});
+		// Return the name of the page INCLUDING suffix, which I guess is a "view" name.
+		// Just "error" is not enough, but don't seem to need a ModelAndView object.
+		return "error.html";
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/CaasIngressDemo.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/CaasIngressDemo.java
new file mode 100644
index 0000000..3a75dfa
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/CaasIngressDemo.java
@@ -0,0 +1,44 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.k8sapi;
+
+import java.lang.invoke.MethodHandles;
+
+import org.oransc.ric.portal.dashboard.util.HttpsURLConnectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+public class CaasIngressDemo {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	public static void main(String[] args) throws Exception {
+		HttpsURLConnectionUtils.turnOffSslChecking();
+		// Get IP address from REC deployment team for testing
+		final String podsUrl = "https://10.0.0.1:16443/api/v1/namespaces/ricaux/pods";
+		RestTemplate rt = new RestTemplate();
+		ResponseEntity<String> podsResponse = rt.getForEntity(podsUrl, String.class);
+		logger.info(podsResponse.getBody());
+		HttpsURLConnectionUtils.turnOnSslChecking();
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/SimpleKubernetesClient.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/SimpleKubernetesClient.java
new file mode 100644
index 0000000..bcfae62
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/k8sapi/SimpleKubernetesClient.java
@@ -0,0 +1,56 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.k8sapi;
+
+import java.lang.invoke.MethodHandles;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+
+/**
+ * Provides a method to get a list of pods using RestTemplate. As currently
+ * configured this uses the default JVM HTTPS support which can be configured to
+ * ignore errors as a workaround for self-signed SSL certificates.
+ */
+public class SimpleKubernetesClient {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	private final String k8sUrl;
+
+	public SimpleKubernetesClient(String baseUrl) {
+		logger.debug("ctor: baseUrl {}", baseUrl);
+		k8sUrl = baseUrl;
+	}
+
+	public String listPods(String namespace) {
+		logger.debug("listPods for namespace {}", namespace);
+		String podsUrl = new DefaultUriBuilderFactory(k8sUrl.trim()).builder().pathSegment("v1")
+				.pathSegment("namespaces").pathSegment(namespace.trim()).pathSegment("pods").build().normalize()
+				.toString();
+		RestTemplate rt = new RestTemplate();
+		ResponseEntity<String> podsResponse = rt.getForEntity(podsUrl, String.class);
+		return podsResponse.getBody();
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java
new file mode 100644
index 0000000..7bc9f00
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java
@@ -0,0 +1,85 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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();
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java
new file mode 100644
index 0000000..516e3c8
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java
@@ -0,0 +1,133 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.time.Instant;
+
+/**
+ * This mimics the model Spring-Boot uses for a message returned on failure, to
+ * be serialized as JSON.
+ */
+public class ErrorTransport implements IDashboardResponse {
+
+	private Instant timestamp;
+	private Integer status;
+	private String error;
+	private String message;
+	private String path;
+
+	/**
+	 * Builds an empty object.
+	 */
+	public ErrorTransport() {
+		// no-arg constructor
+	}
+
+	/**
+	 * Convenience constructor for minimal value set.
+	 * 
+	 * @param status
+	 *                   Integer value like 400
+	 * @param error
+	 *                   Error message
+	 */
+	public ErrorTransport(int status, String error) {
+		this(status, error, null, null);
+	}
+
+	/**
+	 * Convenience constructor for populating an error from an exception
+	 * 
+	 * @param status
+	 *                      Integer value like 400
+	 * @param throwable
+	 *                      The caught exception/throwable to convert to String with
+	 *                      an upper bound on characters
+	 */
+	public ErrorTransport(int status, Throwable throwable) {
+		this.timestamp = Instant.now();
+		this.status = status;
+		final int enough = 256;
+		String exString = throwable.toString();
+		this.error = exString.length() > enough ? exString.substring(0, enough) : exString;
+	}
+
+	/**
+	 * Builds an object with all fields
+	 * 
+	 * @param status
+	 *                    Integer value like 500
+	 * @param error
+	 *                    Explanation
+	 * @param message
+	 *                    Additional explanation
+	 * @param path
+	 *                    Requested path
+	 */
+	public ErrorTransport(int status, String error, String message, String path) {
+		this.timestamp = Instant.now();
+		this.status = status;
+		this.error = error;
+		this.message = message;
+		this.path = path;
+	}
+
+	public Integer getStatus() {
+		return status;
+	}
+
+	public void setStatus(Integer status) {
+		this.status = status;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public void setMessage(String error) {
+		this.message = error;
+	}
+
+	public Instant getTimestamp() {
+		return timestamp;
+	}
+
+	public void setTimestamp(Instant timestamp) {
+		this.timestamp = timestamp;
+	}
+
+	public String getError() {
+		return error;
+	}
+
+	public void setError(String error) {
+		this.error = error;
+	}
+
+	public String getPath() {
+		return path;
+	}
+
+	public void setPath(String path) {
+		this.path = path;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/IDashboardResponse.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/IDashboardResponse.java
new file mode 100644
index 0000000..a298fa6
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/IDashboardResponse.java
@@ -0,0 +1,27 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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;
+
+/**
+ * Marker interface used by all transport models.
+ */
+public interface IDashboardResponse {
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstance.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstance.java
new file mode 100644
index 0000000..b699b3c
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstance.java
@@ -0,0 +1,48 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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;
+
+public class PolicyInstance implements IDashboardResponse {
+	private String instanceId;
+	private Object instance;
+
+	public PolicyInstance(String id, Object instance) {
+		this.instanceId = id;
+		this.instance = instance;
+	}
+
+	public String getInstanceId() {
+		return instanceId;
+	}
+	public void setInstanceId(String instanceId) {
+		this.instanceId = instanceId;
+	}
+	public Object getInstance() {
+		return instance;
+	}
+	public void setInstance(Object instance) {
+		this.instance = instance;
+	}
+
+	@Override
+	public String toString() {
+		return PolicyInstance.class.getName() + ": [id:" + instanceId + ", instance: " + instance + "]";
+	}
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstances.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstances.java
new file mode 100644
index 0000000..c750487
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyInstances.java
@@ -0,0 +1,28 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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;
+
+public class PolicyInstances extends ArrayList<PolicyInstance> {
+
+	private static final long serialVersionUID = -928428052502491021L;
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyType.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyType.java
new file mode 100644
index 0000000..d24667a
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyType.java
@@ -0,0 +1,81 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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 com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PolicyType {
+
+    @JsonProperty("policy_type_id")
+	Integer policyTypeId;
+
+	@JsonProperty("name")
+	String name;
+
+	@JsonProperty("description")
+	String description;
+
+	@JsonProperty("create_schema")
+	String createSchema;
+
+	public PolicyType(Integer policyId, String name, String description, String createSchema) {
+		this.policyTypeId = policyId;
+		this.name = name;
+		this.description = description;
+		this.createSchema = createSchema;
+	}
+
+	public Integer getPolicyTypeId() {
+		return policyTypeId;
+	}
+
+	public void setPolicyTypeId(Integer policyTypeId) {
+		this.policyTypeId = policyTypeId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getDescription() {
+		return description;
+	}
+
+	public void setDescription(String description) {
+		this.description = description;
+	}
+
+	public String getCreateSchema() {
+		return createSchema;
+	}
+
+	public void setCreateSchema(String createSchema) {
+		this.createSchema = createSchema;
+	}
+
+	@Override
+	public String toString() {
+		return "[policy_type_id:" + policyTypeId +  ", name:" + name + ", description:" + description + ", create_schema:" + createSchema + "]";
+	}
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyTypes.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyTypes.java
new file mode 100644
index 0000000..3165d1b
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/PolicyTypes.java
@@ -0,0 +1,28 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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;
+
+public class PolicyTypes extends ArrayList<PolicyType> {
+
+	private static final long serialVersionUID = -928428052502491021L;
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/SuccessTransport.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/SuccessTransport.java
new file mode 100644
index 0000000..9e13789
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/SuccessTransport.java
@@ -0,0 +1,63 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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;
+
+public class SuccessTransport implements IDashboardResponse {
+
+	private int status;
+	private Object data;
+
+	/**
+	 * Builds an empty object
+	 */
+	public SuccessTransport() {
+		// no-arg constructor
+	}
+
+	/**
+	 * Builds an object with the specified values.
+	 * 
+	 * @param status
+	 *                   Status code
+	 * @param data
+	 *                   Data to transport
+	 */
+	public SuccessTransport(int status, Object data) {
+		this.status = status;
+		this.data = data;
+	}
+
+	public int getStatus() {
+		return status;
+	}
+
+	public void setStatus(int status) {
+		this.status = status;
+	}
+
+	public Object getData() {
+		return data;
+	}
+
+	public void setData(Object data) {
+		this.data = data;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java
new file mode 100644
index 0000000..34d80c9
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java
@@ -0,0 +1,41 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java
new file mode 100644
index 0000000..dc70f7e
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java
@@ -0,0 +1,120 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import org.onap.portalsdk.core.onboarding.crossapi.IPortalRestCentralService;
+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, IllegalArgumentException,
+			InvocationTargetException, NoSuchMethodException, SecurityException {
+		credentialsMap = new HashMap<>();
+		credentialsMap.put(IPortalRestCentralService.CREDENTIALS_APP, appName);
+		credentialsMap.put(IPortalRestCentralService.CREDENTIALS_USER, username);
+		credentialsMap.put(IPortalRestCentralService.CREDENTIALS_PASS, 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.getDeclaredConstructor().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 validateEcompSso(HttpServletRequest request) {
+		// Check ECOMP Portal cookie
+		Cookie ep = getCookie(request, PortalApiConstants.EP_SERVICE);
+		if (ep == null) {
+			logger.debug("validateEcompSso: 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("validateEcompSso: 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("validateEcompSso failed", e);
+		}
+		return userid;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java
new file mode 100644
index 0000000..4b6de91
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java
@@ -0,0 +1,281 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.lang.invoke.MethodHandles;
+import java.net.URLEncoder;
+import java.util.HashSet;
+
+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.KeyProperties;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.onap.portalsdk.core.onboarding.util.PortalApiProperties;
+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.DashboardUserManager;
+import org.oransc.ric.portal.dashboard.model.EcompUserDetails;
+import org.owasp.esapi.reference.DefaultSecurityConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.Authentication;
+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());
+
+	// Unfortunately not all file names are defined as constants
+	private static final String[] securityPropertyFiles = { KeyProperties.PROPERTY_FILE_NAME,
+			PortalApiProperties.PROPERTY_FILE_NAME, DefaultSecurityConfiguration.DEFAULT_RESOURCE_FILE,
+			"validation.properties" };
+
+	public static final String REDIRECT_URL_KEY = "redirectUrl";
+
+	private final boolean enforcePortalSecurity;
+	private final PortalAuthManager authManager;
+
+	private final DashboardUserManager userManager;
+
+	public PortalAuthenticationFilter(boolean portalSecurity, PortalAuthManager authManager,
+			DashboardUserManager userManager) {
+		this.enforcePortalSecurity = portalSecurity;
+		this.authManager = authManager;
+		this.userManager = userManager;
+		if (portalSecurity) {
+			// Throw if security is requested and prerequisites are not met
+			for (String pf : securityPropertyFiles) {
+				InputStream in = MethodHandles.lookup().lookupClass().getClassLoader().getResourceAsStream(pf);
+				if (in == null) {
+					String msg = "Failed to find property file on classpath: " + pf;
+					logger.error(msg);
+					throw new RuntimeException(msg);
+				} else {
+					try {
+						in.close();
+					} catch (IOException ex) {
+						logger.warn("Failed to close stream", ex);
+					}
+				}
+			}
+		}
+	}
+
+	@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
+	}
+
+	/**
+	 * 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 {
+		if (enforcePortalSecurity)
+			doFilterEPSDKFW(req, res, chain);
+		else
+			doFilterMockUserAdminRole(req, res, chain);
+	}
+
+	/*
+	 * Populates security context with a mock user in the admin role.
+	 * 
+	 */
+	private void doFilterMockUserAdminRole(ServletRequest req, ServletResponse res, FilterChain chain)
+			throws IOException, ServletException {
+		Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+		if (auth == null || auth.getAuthorities().isEmpty()) {
+			if (logger.isDebugEnabled()) {
+				logger.debug("doFilter adding auth to request URI {}",
+						(req instanceof HttpServletRequest) ? ((HttpServletRequest) req).getRequestURL() : req);
+			}
+			EcompRole admin = new EcompRole();
+			admin.setId(1L);
+			admin.setName(DashboardConstants.ROLE_ADMIN);
+			HashSet<EcompRole> roles = new HashSet<>();
+			roles.add(admin);
+			EcompUser user = new EcompUser();
+			user.setLoginId("fakeLoginId");
+			user.setRoles(roles);
+			user.setActive(true);
+			EcompUserDetails userDetails = new EcompUserDetails(user);
+			PreAuthenticatedAuthenticationToken authToken = new PreAuthenticatedAuthenticationToken(userDetails,
+					"fakeCredentials", userDetails.getAuthorities());
+			SecurityContextHolder.getContext().setAuthentication(authToken);
+		} else {
+			logger.debug("doFilter: authorities {}", auth.getAuthorities());
+		}
+		chain.doFilter(req, res);
+	}
+
+	/*
+	 * Checks for valid cookies and allows request to be served if found; redirects
+	 * to Portal otherwise.
+	 */
+	private void doFilterEPSDKFW(ServletRequest req, ServletResponse res, FilterChain chain)
+			throws IOException, ServletException {
+		HttpServletRequest request = (HttpServletRequest) req;
+		HttpServletResponse response = (HttpServletResponse) res;
+		if (logger.isTraceEnabled())
+			logger.trace("doFilter: req {}", request.getRequestURI());
+		// Need to authenticate the request
+		final String userId = authManager.validateEcompSso(request);
+		final EcompUser ecompUser = (userId == null ? null : userManager.getUser(userId));
+		if (userId == null || ecompUser == null) {
+			logger.debug("doFilter: unauthorized user requests URI {}, serving login page", request.getRequestURI());
+			StringBuffer sb = request.getRequestURL();
+			sb.append(request.getQueryString() == null ? "" : "?" + request.getQueryString());
+			String body = generateLoginRedirectPage(sb.toString());
+			response.setContentType(MediaType.TEXT_HTML_VALUE);
+			response.getWriter().print(body);
+			response.getWriter().flush();
+		} 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);
+		}
+	}
+
+	/**
+	 * Generates a page with text only, absolutely no references to any webapp
+	 * resources, so this can be served to an unauthenticated user without
+	 * triggering a new authentication attempt. The page has a link to the Portal
+	 * URL from configuration, with a return URL that is the original request.
+	 * 
+	 * @param appUrl
+	 *                   Original requested URL
+	 * @return HTML
+	 * @throws UnsupportedEncodingException
+	 *                                          On error
+	 */
+	private static String generateLoginRedirectPage(String appUrl) throws UnsupportedEncodingException {
+		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>");
+		return body;
+	}
+
+	/**
+	 * 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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java
new file mode 100644
index 0000000..581ca25
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java
@@ -0,0 +1,89 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.DashboardUserManager;
+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.validateEcompSso(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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java
new file mode 100644
index 0000000..279f1dd
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java
@@ -0,0 +1,32 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java
new file mode 100644
index 0000000..0127267
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java
@@ -0,0 +1,31 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/util/HttpsURLConnectionUtils.java b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/util/HttpsURLConnectionUtils.java
new file mode 100644
index 0000000..a97ed7b
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/util/HttpsURLConnectionUtils.java
@@ -0,0 +1,82 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.util;
+
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Disables and enables certificate and host-name checking in
+ * HttpsURLConnection, the default JVM implementation of the HTTPS/TLS protocol.
+ * Has no effect on implementations such as Apache Http Client, Ok Http.
+ * 
+ * https://stackoverflow.com/questions/23504819/how-to-disable-ssl-certificate-checking-with-spring-resttemplate/58291331#58291331
+ */
+public final class HttpsURLConnectionUtils {
+
+	private static final HostnameVerifier jvmHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+
+	private static final HostnameVerifier trivialHostnameVerifier = new HostnameVerifier() {
+		public boolean verify(String hostname, SSLSession sslSession) {
+			return true;
+		}
+	};
+
+	private static final TrustManager[] UNQUESTIONING_TRUST_MANAGER = new TrustManager[] { new X509TrustManager() {
+		public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+			return null;
+		}
+
+		public void checkClientTrusted(X509Certificate[] certs, String authType) {
+		}
+
+		public void checkServerTrusted(X509Certificate[] certs, String authType) {
+		}
+	} };
+
+	public static void turnOffSslChecking() throws NoSuchAlgorithmException, KeyManagementException {
+		HttpsURLConnection.setDefaultHostnameVerifier(trivialHostnameVerifier);
+		// Install the all-trusting trust manager
+		SSLContext sc = SSLContext.getInstance("SSL");
+		sc.init(null, UNQUESTIONING_TRUST_MANAGER, null);
+		HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+	}
+
+	public static void turnOnSslChecking() throws KeyManagementException, NoSuchAlgorithmException {
+		HttpsURLConnection.setDefaultHostnameVerifier(jvmHostnameVerifier);
+		// Return it to the initial state (discovered by reflection, now hardcoded)
+		SSLContext sc = SSLContext.getInstance("SSL");
+		sc.init(null, null, null);
+		HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+	}
+
+	private HttpsURLConnectionUtils() {
+		throw new UnsupportedOperationException("Do not instantiate libraries.");
+	}
+}
diff --git a/dashboard/webapp-backend/src/main/resources/ESAPI.properties b/dashboard/webapp-backend/src/main/resources/ESAPI.properties
new file mode 100644
index 0000000..9a26119
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/resources/ESAPI.properties
@@ -0,0 +1,385 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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}$
diff --git a/dashboard/webapp-backend/src/main/resources/application.properties b/dashboard/webapp-backend/src/main/resources/application.properties
new file mode 100644
index 0000000..e969968
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/resources/application.properties
@@ -0,0 +1,73 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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===================================
+
+# 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.
+
+# 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
+
+# boolean flag whether to enforce Portal user and roles on requests
+portalapi.security = true
+# 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 =
+# endpoint URLs must be supplied at deployment time
+# A1 Controller
+a1controller.url.prefix = http://localhost:8282
+a1controller.url.suffix = /restconf/operations
+# 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
+# Kubernetes API via https://github.com/nokia/caas-ingress
+# Set insecure=true to disable SSL certificate and hostname checking
+caasingress.insecure = true
+caasingress.aux.url.prefix = https://jar-app-props-default-caas-ingress-aux-URL
+caasingress.aux.url.suffix = /api
+caasingress.plt.url.prefix = https://jar-app-props-default-caas-ingress-plt-URL
+caasingress.plt.url.suffix = /api
+
+# Mimic slow endpoints by defining sleep period, in milliseconds
+mock.config.delay = 0
+
+# Kibana report on metrics
+metrics.url.ac = http://jar-app-props-kibana-url-ac
+metrics.url.mc = http://jar-app-props-kibana-url-mc
diff --git a/dashboard/webapp-backend/src/main/resources/logback.xml b/dashboard/webapp-backend/src/main/resources/logback.xml
new file mode 100644
index 0000000..213d105
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/resources/logback.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ========================LICENSE_START=================================
+  O-RAN-SC
+  %%
+  Copyright (C) 2019 AT&T Intellectual Property
+  %%
+  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===================================
+  -->
+
+<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 -->
+	<property name="logDirectory" value="logs" />
+	<!-- 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">
+		<!-- defaults to type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+		<encoder>
+			<pattern>${pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${logDirectory}/${componentName}.log</file>
+		<append>true</append>
+		<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
+			<fileNamePattern>${logDirectory}/${componentName}.%i.log.zip</fileNamePattern>
+			<minIndex>1</minIndex>
+			<maxIndex>9</maxIndex>
+		</rollingPolicy>
+		<triggeringPolicy
+			class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
+			<maxFileSize>10MB</maxFileSize>
+		</triggeringPolicy>
+		<!-- defaults to type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+		<encoder>
+			<pattern>${pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- Set default level for all loggers -->
+	<root level="INFO">
+		<appender-ref ref="CONSOLE" />
+		<appender-ref ref="FILE" />
+	</root>
+
+	<!-- Code under test should be chatty --> >
+	<logger name="org.oransc.ric.portal.dashboard" level="DEBUG" />
+
+	<!-- 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" />
+
+	<!-- for debugging security
+	<logger name="org.springframework.security" level="DEBUG" />
+	-->
+
+</configuration>
diff --git a/dashboard/webapp-backend/src/main/resources/static/error.html b/dashboard/webapp-backend/src/main/resources/static/error.html
new file mode 100644
index 0000000..1b7633f
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/resources/static/error.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+  ========================LICENSE_START=================================
+  O-RAN-SC
+  %%
+  Copyright (C) 2019 AT&T Intellectual Property
+  %%
+  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===================================
+  -->
+
+<html>
+<head>
+<title>Static error page</title>
+<style>
+html, body {
+  font-family: Helvetica, Arial, sans-serif;
+}
+</style>
+</head>
+<body>
+<h2>Non RT RIC Dashboard Error</h2>
+<h4>The previous request could not be processed.</h4>
+<a href="/">Click here to reload the application</a>
+</body>
+</html>
diff --git a/dashboard/webapp-backend/src/main/resources/validation.properties b/dashboard/webapp-backend/src/main/resources/validation.properties
new file mode 100644
index 0000000..c7a5c79
--- /dev/null
+++ b/dashboard/webapp-backend/src/main/resources/validation.properties
@@ -0,0 +1,19 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/DashboardTestServer.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/DashboardTestServer.java
new file mode 100644
index 0000000..df1a51c
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/DashboardTestServer.java
@@ -0,0 +1,69 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.lang.invoke.MethodHandles;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+/**
+ * This class supports front-end web development. Placing this class in the test
+ * area allows excluding the mock configuration classes and the Mockito
+ * dependencies from the packaged version of the app.
+ *
+ * To launch a development server set the environment variable as listed below.
+ * This runs a Spring-Boot server with mock back-end beans, and keeps the server
+ * alive for manual testing. Supply this JVM argument:
+ *
+ * <pre>
+ * -Dorg.oransc.ric.portal.dashboard=mock
+ * </pre>
+ */
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
+@ActiveProfiles("test")
+public class DashboardTestServer {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	/*
+	 * Keeps the test server alive forever. Use a guard so this test is never run by
+	 * Jenkins.
+	 */
+	@EnabledIfSystemProperty(named = "org.oransc.ric.portal.dashboard", matches = "mock")
+	@Test
+	public void keepServerAlive() {
+		logger.warn("Keeping server alive!");
+		try {
+			synchronized (this) {
+				this.wait();
+			}
+		} catch (Exception ex) {
+			logger.warn(ex.toString());
+		}
+	}
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1ControllerMockConfiguration.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1ControllerMockConfiguration.java
new file mode 100644
index 0000000..b056cb5
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1ControllerMockConfiguration.java
@@ -0,0 +1,424 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 Nordix Foundation
+ * %%
+ * 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 static org.mockito.Mockito.when;
+
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.oransc.ric.a1controller.client.api.A1ControllerApi;
+import org.oransc.ric.a1controller.client.invoker.ApiClient;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidPISchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidPIidSchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidPTidSchema;
+import org.oransc.ric.a1controller.client.model.InputNRRidSchema;
+import org.oransc.ric.a1controller.client.model.OutputDescNamePTSchema;
+import org.oransc.ric.a1controller.client.model.OutputDescNamePTSchemaOutput;
+import org.oransc.ric.a1controller.client.model.OutputPISchema;
+import org.oransc.ric.a1controller.client.model.OutputPISchemaOutput;
+import org.oransc.ric.a1controller.client.model.OutputPIidsListSchema;
+import org.oransc.ric.a1controller.client.model.OutputPIidsListSchemaOutput;
+import org.oransc.ric.a1controller.client.model.OutputPTidsListSchema;
+import org.oransc.ric.a1controller.client.model.OutputPTidsListSchemaOutput;
+import org.oransc.ric.portal.dashboard.model.PolicyType;
+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;
+import org.springframework.http.HttpStatus;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+/**
+ * Creates a mock implementation of the A1 controller client API.
+ */
+@Profile("test")
+@Configuration
+public class A1ControllerMockConfiguration {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	// A "control" is an element in the XApp descriptor
+	public static final String AC_CONTROL_NAME = "admission_control_policy";
+
+	// Simulate remote method delay for UI testing
+	@Value("${mock.config.delay:0}")
+	private int delayMs;
+
+	public A1ControllerMockConfiguration() {
+		logger.info("Configuring mock A1 Mediator");
+	}
+
+	private ApiClient apiClient() {
+		ApiClient mockClient = mock(ApiClient.class);
+		when(mockClient.getStatusCode()).thenReturn(HttpStatus.OK);
+		return mockClient;
+	}
+
+	@Bean
+	// Use the same name as regular configuration
+	public A1ControllerApi a1ControllerApi() {
+		ApiClient apiClient = apiClient();
+		A1ControllerApi mockApi = mock(A1ControllerApi.class);
+
+		when(mockApi.getApiClient()).thenReturn(apiClient);
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			List<Integer> types = database.getTypes();
+			OutputPTidsListSchemaOutput output = new OutputPTidsListSchemaOutput();
+			output.setPolicyTypeIdList(types);
+			OutputPTidsListSchema outputSchema = new OutputPTidsListSchema();
+			outputSchema.setOutput(output);
+			return outputSchema;
+		}).when(mockApi).a1ControllerGetAllPolicyTypes(any(InputNRRidSchema.class));
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetPolicyType sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			InputNRRidPTidSchema input = inv.<InputNRRidPTidSchema>getArgument(0);
+			PolicyType policyType = database.getPolicyType(input.getInput().getPolicyTypeId());
+			OutputDescNamePTSchemaOutput type = new OutputDescNamePTSchemaOutput();
+			type.setName(policyType.getName());
+			type.setDescription(policyType.getDescription());
+			type.setPolicyType(database.normalize(policyType.getCreateSchema()));
+			OutputDescNamePTSchema outputSchema = new OutputDescNamePTSchema();
+			outputSchema.setOutput(type);
+			return outputSchema;
+		}).when(mockApi).a1ControllerGetPolicyType(any(InputNRRidPTidSchema.class));
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			InputNRRidPTidSchema input = inv.<InputNRRidPTidSchema>getArgument(0);
+			List<String> instances = database.getInstances(Optional.of(input.getInput().getPolicyTypeId()));
+			OutputPIidsListSchemaOutput instancesOutput = new OutputPIidsListSchemaOutput();
+			instancesOutput.setPolicyInstanceIdList(instances);
+			OutputPIidsListSchema outputSchema = new OutputPIidsListSchema();
+			outputSchema.setOutput(instancesOutput);
+			return outputSchema;
+		}).when(mockApi).a1ControllerGetAllInstancesForType(any(InputNRRidPTidSchema.class));
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			InputNRRidPTidPIidSchema input = inv.<InputNRRidPTidPIidSchema>getArgument(0);
+			Integer polcyTypeId = input.getInput().getPolicyTypeId();
+			String instanceId = input.getInput().getPolicyInstanceId();
+			String instance = database.normalize(database.getInstance(polcyTypeId, instanceId));
+			OutputPISchemaOutput instanceOutput = new OutputPISchemaOutput();
+			instanceOutput.setPolicyInstance(instance);
+			OutputPISchema outputSchema = new OutputPISchema();
+			outputSchema.setOutput(instanceOutput);
+			return outputSchema;
+		}).when(mockApi).a1ControllerGetPolicyInstance(any(InputNRRidPTidPIidSchema.class));
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			InputNRRidPTidPIidPISchema input = inv.<InputNRRidPTidPIidPISchema>getArgument(0);
+			Integer polcyTypeId = input.getInput().getPolicyTypeId();
+			String instanceId = input.getInput().getPolicyInstanceId();
+			String instance = input.getInput().getPolicyInstance();
+			database.putInstance(polcyTypeId, instanceId, instance);
+			return null;
+		}).when(mockApi).a1ControllerCreatePolicyInstance(any(InputNRRidPTidPIidPISchema.class));
+
+		doAnswer(inv -> {
+			if (delayMs > 0) {
+				logger.debug("a1ControllerGetHandler sleeping {}", delayMs);
+				Thread.sleep(delayMs);
+			}
+			InputNRRidPTidPIidSchema input = inv.<InputNRRidPTidPIidSchema>getArgument(0);
+			Integer polcyTypeId = input.getInput().getPolicyTypeId();
+			String instanceId = input.getInput().getPolicyInstanceId();
+			database.deleteInstance(polcyTypeId, instanceId);
+			return null;
+		}).when(mockApi).a1ControllerDeletePolicyInstance(any(InputNRRidPTidPIidSchema.class));
+
+		return mockApi;
+	}
+
+	class Database {
+
+		private String schema1 = "{\"$schema\": " //
+				+ "\"http://json-schema.org/draft-07/schema#\"," //
+				+ "\"title\": \"ANR\"," //
+				+ "\"description\": \"ANR Neighbour Cell Relation Policy\"," //
+				+ "\"type\": \"object\"," //
+				+ "\"properties\": " //
+				+ "{ \"servingCellNrcgi\": {" //
+				+ "\"type\": \"string\"," //
+				+ "\"description\" : \"Serving Cell Identifier (NR CGI)\"}," //
+				+ "\"neighborCellNrpci\": {" //
+				+ "\"type\": \"string\"," //
+				+ "\"description\": \"Neighbor Cell Identifier (NR PCI)\"}," //
+				+ "\"neighborCellNrcgi\": {" //
+				+ "\"type\": \"string\"," //
+				+ "\"description\": \"Neighbor Cell Identifier (NR CGI)\"}," //
+				+ "\"flagNoHo\": {" //
+				+ "\"type\": \"boolean\"," //
+				+ "\"description\": \"Flag for HANDOVER NOT ALLOWED\"}," //
+				+ "\"flagNoXn\": {" //
+				+ "\"type\": \"boolean\"," //
+				+ "\"description\": \"Flag for Xn CONNECTION NOT ALLOWED\"}," //
+				+ "\"flagNoRemove\": {" //
+				+ "\"type\": \"boolean\"," //
+				+ "\"description\": \"Flag for DELETION NOT ALLOWED\"}}, " //
+				+ "\"required\": [ \"servingCellNrcgi\",\"neighborCellNrpci\",\"neighborCellNrcgi\",\"flagNoHo\",\"flagNoXn\",\"flagNoRemove\" ]}";
+		private PolicyType policy1 = new PolicyType(1, "ANR", "ANR Neighbour Cell Relation Policy", schema1);
+
+		private String policyInstance1 = "{\"servingCellNrcgi\": \"Cell1\",\r\n" + //
+				"\"neighborCellNrpci\": \"NCell1\",\r\n" + //
+				"\"neighborCellNrcgi\": \"Ncell1\",\r\n" + //
+				"\"flagNoHo\": true,\r\n" + //
+				"\"flagNoXn\": true,\r\n" + //
+				"\"flagNoRemove\": true}";
+
+		private String schema2 = "{\n" + "          \"type\": \"object\",\n" + //
+				"          \"title\": \"Car\",\n" + //
+				"          \"properties\": {\n" + //
+				"            \"make\": {\n" + //
+				"              \"type\": \"string\",\n" + //
+				"              \"enum\": [\n" + //
+				"                \"Toyota\",\n" + //
+				"                \"BMW\",\n" + //
+				"                \"Honda\",\n" + //
+				"                \"Ford\",\n" + //
+				"                \"Chevy\",\n" + //
+				"                \"VW\"\n" + //
+				"              ]\n" + //
+				"            },\n" + //
+				"            \"model\": {\n" + //
+				"              \"type\": \"string\"\n" + //
+				"            },\n" + //
+				"            \"year\": {\n" + //
+				"              \"type\": \"integer\",\n" + //
+				"              \"enum\": [\n" + //
+				"                1995,1996,1997,1998,1999,\n" + //
+				"                2000,2001,2002,2003,2004,\n" + //
+				"                2005,2006,2007,2008,2009,\n" + //
+				"                2010,2011,2012,2013,2014\n" + //
+				"              ],\n" + //
+				"              \"default\": 2008\n" + //
+				"            },\n" + //
+				"            \"safety\": {\n" + //
+				"              \"type\": \"integer\",\n" + //
+				"              \"format\": \"rating\",\n" + //
+				"              \"maximum\": 5,\n" + //
+				"              \"exclusiveMaximum\": false,\n" + //
+				"              \"readonly\": false\n" + //
+				"            }\n" + //
+				"          }\n" + //
+				"        }\n";
+		private PolicyType policy2 = new PolicyType(2, "type2", "Type2 description", schema2);
+
+		private String schema3 = "{\n" + //
+				"  \"$id\": \"https://example.com/person.schema.json\",\n" + //
+				"  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n" + //
+				"  \"title\": \"Person\",\n" + //
+				"  \"type\": \"object\",\n" + //
+				"  \"properties\": {\n" + //
+				"    \"firstName\": {\n" + //
+				"      \"type\": \"string\",\n" + //
+				"      \"description\": \"The person's first name.\"\n" + //
+				"    },\n" + //
+				"    \"lastName\": {\n" + //
+				"      \"type\": \"string\",\n" + //
+				"      \"description\": \"The person's last name.\"\n" + //
+				"    },\n" + //
+				"    \"age\": {\n" + //
+				"      \"description\": \"Age in years which must be equal to or greater than zero.\",\n" + //
+				"      \"type\": \"integer\",\n" + //
+				"      \"minimum\": 0\n" + //
+				"    }\n" + //
+				"  }\n" + //
+				"}";
+		private PolicyType policy3 = new PolicyType(3, "type3", "Type3 description", schema3);
+
+		private String schema4 = "{" + //
+				"		  \"$id\": \"https://example.com/arrays.schema.json\"," + //
+				"		  \"$schema\": \"http://json-schema.org/draft-07/schema#\"," + //
+				"		  \"description\": \"A representation of a person, company, organization, or place\"," + //
+				"		  \"type\": \"object\"," + //
+				"		  \"properties\": {" + //
+				"		    \"fruits\": {" + //
+				"		      \"type\": \"array\"," + //
+				"		      \"items\": {" + //
+				"		        \"type\": \"string\"" + //
+				"		      }" + //
+				"		    }," + //
+				"		    \"vegetables\": {" + //
+				"		      \"type\": \"array\"," + //
+				"		      \"items\": { \"$ref\": \"#/definitions/veggie\" }" + //
+				"		    }" + //
+				"		  }," + //
+				"		  \"definitions\": {" + //
+				"		    \"veggie\": {" + //
+				"		      \"type\": \"object\"," + //
+				"		      \"required\": [ \"veggieName\", \"veggieLike\" ]," + //
+				"		      \"properties\": {" + //
+				"		        \"veggieName\": {" + //
+				"		          \"type\": \"string\"," + //
+				"		          \"description\": \"The name of the vegetable.\"" + //
+				"		        }," + //
+				"		        \"veggieLike\": {" + //
+				"		          \"type\": \"boolean\"," + //
+				"		          \"description\": \"Do I like this vegetable?\"" + //
+				"		        }" + //
+				"		      }" + //
+				"		    }" + //
+				"		  }" + //
+				"		}";
+		private PolicyType policy4 = new PolicyType(4, "type4", "Type4 description", schema4);
+
+		public class PolicyException extends Exception {
+
+			private static final long serialVersionUID = 1L;
+
+			public PolicyException(String message) {
+				super(message);
+				System.out.println("**** Exception " + message);
+			}
+		}
+
+		private class PolicyTypeHolder {
+			PolicyTypeHolder(PolicyType pt) {
+				this.policyType = pt;
+			}
+
+			String getInstance(String instanceId) throws PolicyException {
+				String instance = instances.get(instanceId);
+				if (instance == null) {
+					throw new PolicyException("Instance not found: " + instanceId);
+				}
+				return instance;
+			}
+
+			PolicyType getPolicyType() {
+				return policyType;
+			}
+
+			void putInstance(String id, String data) {
+				instances.put(id, data);
+			}
+
+			void deleteInstance(String id) {
+				instances.remove(id);
+			}
+
+			List<String> getInstances() {
+				return new ArrayList<>(instances.keySet());
+			}
+
+			private final PolicyType policyType;
+			private Map<String, String> instances = new HashMap<>();
+		}
+
+		Database() {
+			types.put(1, new PolicyTypeHolder(policy1));
+			types.put(2, new PolicyTypeHolder(policy2));
+			types.put(3, new PolicyTypeHolder(policy3));
+			types.put(4, new PolicyTypeHolder(policy4));
+			try {
+				putInstance(1, "ANR-1", policyInstance1);
+			} catch (JsonProcessingException | PolicyException e) {
+				// Nothing
+			}
+		}
+
+		String normalize(String str) {
+			return str.replace('\n', ' ');
+		}
+
+		void putInstance(Integer typeId, String instanceId, String instanceData)
+				throws JsonProcessingException, PolicyException {
+			PolicyTypeHolder type = getTypeHolder(typeId);
+			type.putInstance(instanceId, instanceData);
+		}
+
+		void deleteInstance(Integer typeId, String instanceId) throws JsonProcessingException, PolicyException {
+			PolicyTypeHolder type = getTypeHolder(typeId);
+			type.deleteInstance(instanceId);
+		}
+
+		String getInstance(Integer typeId, String instanceId) throws JsonProcessingException, PolicyException {
+			return getTypeHolder(typeId).getInstance(instanceId);
+		}
+
+		List<Integer> getTypes() {
+			return new ArrayList<>(types.keySet());
+		}
+
+		List<String> getInstances(Optional<Integer> typeId) throws PolicyException {
+			if (typeId.isPresent()) {
+				return getTypeHolder(typeId.get()).getInstances();
+			} else {
+				Set<String> res = new HashSet<String>();
+				for (Iterator<PolicyTypeHolder> i = types.values().iterator(); i.hasNext();) {
+					res.addAll(i.next().getInstances());
+				}
+				return new ArrayList<>(res);
+			}
+		}
+
+		private PolicyTypeHolder getTypeHolder(Integer typeId) throws PolicyException {
+			PolicyTypeHolder typeHolder = types.get(typeId);
+			if (typeHolder == null) {
+				throw new PolicyException("Type not found: " + typeId);
+			}
+			return typeHolder;
+		}
+
+		private PolicyType getPolicyType(Integer typeId) throws PolicyException {
+			PolicyTypeHolder typeHolder = getTypeHolder(typeId);
+			return typeHolder.getPolicyType();
+		}
+
+		private Map<Integer, PolicyTypeHolder> types = new HashMap<>();
+
+	}
+
+	private final Database database = new Database();
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/PortalApIMockConfiguration.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/PortalApIMockConfiguration.java
new file mode 100644
index 0000000..a3d05be
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/PortalApIMockConfiguration.java
@@ -0,0 +1,83 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy;
+import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
+import org.oransc.ric.portal.dashboard.portalapi.PortalAuthManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+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;
+
+@Configuration
+@Profile("test")
+public class PortalApIMockConfiguration {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	// 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";
+
+	@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).validateEcompSso(any(HttpServletRequest.class));
+		doAnswer(inv -> {
+			logger.debug("getAppCredentials");
+			return credentialsMap;
+		}).when(mockManager).getAppCredentials();
+		return mockManager;
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java
new file mode 100644
index 0000000..80cde66
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java
@@ -0,0 +1,84 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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.lang.invoke.MethodHandles;
+
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+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";
+
+	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);
+		web.ignoring().antMatchers("/", "/csrf"); // allow swagger-ui to load
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java
new file mode 100644
index 0000000..d4163f0
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java
@@ -0,0 +1,112 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 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;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+// Need the fake answers from the backend
+@ActiveProfiles("test")
+public class AbstractControllerTest {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	// Created by Spring black magic
+	// https://spring.io/guides/gs/testing-web/
+	@LocalServerPort
+	private int localServerPort;
+
+	@Autowired
+	protected TestRestTemplate restTemplate;
+
+	/**
+	 * Flexible URI builder.
+	 * 
+	 * @param queryParams
+	 *                        Map of string-string query parameters
+	 * @param path
+	 *                        Array of path components. If a component has an
+	 *                        embedded slash, the string is split and each
+	 *                        subcomponent is added individually.
+	 * @return URI
+	 */
+	protected URI buildUri(final Map<String, String> queryParams, final String... path) {
+		UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:" + localServerPort + "/");
+		for (int p = 0; p < path.length; ++p) {
+			if (path[p] == null || path[p].isEmpty()) {
+				throw new IllegalArgumentException("Unexpected null or empty at path index " + Integer.toString(p));
+			} else if (path[p].contains("/")) {
+				String[] subpaths = path[p].split("/");
+				for (String s : subpaths)
+					if (!s.isEmpty())
+						builder.pathSegment(s);
+			} else {
+				builder.pathSegment(path[p]);
+			}
+		}
+		if (queryParams != null && queryParams.size() > 0) {
+			for (Map.Entry<String, String> entry : queryParams.entrySet()) {
+				if (entry.getKey() == null || entry.getValue() == null)
+					throw new IllegalArgumentException("Unexpected null key or value");
+				else
+					builder.queryParam(entry.getKey(), entry.getValue());
+			}
+		}
+		return builder.build().encode().toUri();
+	}
+
+	// Because I put the annotations on this parent class,
+	// must define at least one test here.
+	@Test
+	public void contextLoads() {
+		// Silence Sonar warning about missing assertion.
+		Assertions.assertTrue(logger.isWarnEnabled());
+		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);
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java
new file mode 100644
index 0000000..48c5931
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java
@@ -0,0 +1,48 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+/**
+ * Tests whether the default (not mock) configuration classes run to completion.
+ */
+@ExtendWith(SpringExtension.class)
+@SpringBootTest
+public class DefaultContextTest {
+
+	private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+	@Test
+	public void contextLoads() {
+		// Silence Sonar warning about missing assertion.
+		Assertions.assertTrue(logger.isWarnEnabled());
+		logger.info("Context loads on default profile");
+	}
+
+}
diff --git a/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java
new file mode 100644
index 0000000..dc80968
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java
@@ -0,0 +1,117 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 AT&T Intellectual Property
+ * %%
+ * 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 java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+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.EcompRole;
+import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
+import org.oransc.ric.portal.dashboard.config.PortalApIMockConfiguration;
+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());
+
+	@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 getErrorPageTest() {
+		// Send unauthorized request
+		URI uri = buildUri(null, "/favicon.ico");
+		logger.info("Invoking {}", uri);
+		ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
+		Assertions.assertTrue(response.getStatusCode().is4xxClientError());
+		Assertions.assertTrue(response.getBody().contains("Static error page"));
+	}
+
+	private HttpEntity<Object> getEntityWithHeaders(Object body) {
+		HttpHeaders headers = new HttpHeaders();
+		headers.set(PortalApIMockConfiguration.PORTAL_USERNAME_HEADER_KEY,
+				PortalApIMockConfiguration.PORTAL_USERNAME_HEADER_KEY);
+		headers.set(PortalApIMockConfiguration.PORTAL_PASSWORD_HEADER_KEY,
+				PortalApIMockConfiguration.PORTAL_PASSWORD_HEADER_KEY);
+		HttpEntity<Object> entity = new HttpEntity<>(body, headers);
+		return entity;
+	}
+
+	private EcompUser createEcompUser(String loginId) {
+		EcompUser user = new EcompUser();
+		user.setLoginId(loginId);
+		EcompRole role = new EcompRole();
+		role.setRoleFunctions(Collections.EMPTY_SET);
+		role.setId(1L);
+		role.setName(DashboardConstants.ROLE_NAME_ADMIN);
+		Set<EcompRole> roles = new HashSet<>();
+		roles.add(role);
+		user.setRoles(roles);
+		return user;
+	}
+
+/*	@Test
+	public void createUserTest() {
+		final String loginId = "login1";
+		URI create = buildUri(null, PortalApiConstants.API_PREFIX, "user");
+		logger.info("Invoking {}", create);
+		HttpEntity<Object> requestEntity = getEntityWithHeaders(createEcompUser(loginId));
+		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");
+		EcompUser user = createEcompUser(loginId);
+		logger.info("Invoking {}", create);
+		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/dashboard/webapp-backend/src/test/resources/anr-policy-instance.json b/dashboard/webapp-backend/src/test/resources/anr-policy-instance.json
new file mode 100644
index 0000000..0d7315e
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/anr-policy-instance.json
@@ -0,0 +1,8 @@
+{
+  "servingCellNrcgi": "Cell1",
+  "neighborCellNrpci": "NCell1",
+  "neighborCellNrcgi": "Ncell1",
+  "flagNoHo": true,
+  "flagNoXn": true,
+  "flagNoRemove": true
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/anr-policy-schema.json b/dashboard/webapp-backend/src/test/resources/anr-policy-schema.json
new file mode 100644
index 0000000..6e0263d
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/anr-policy-schema.json
@@ -0,0 +1,40 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ANR",
+  "description": "ANR Neighbour Cell Relation Policy",
+  "type": "object",
+  "properties": {
+    "servingCellNrcgi": {
+      "type": "string",
+      "description": "Serving Cell Identifier (NR CGI)"
+    },
+    "neighborCellNrpci": {
+      "type": "string",
+      "description": "Neighbor Cell Identifier (NR PCI)"
+    },
+    "neighborCellNrcgi": {
+      "type": "string",
+      "description": "Neighbor Cell Identifier (NR CGI)"
+    },
+    "flagNoHo": {
+      "type": "boolean",
+      "description": "Flag for HANDOVER NOT ALLOWED"
+    },
+    "flagNoXn": {
+      "type": "boolean",
+      "description": "Flag for Xn CONNECTION NOT ALLOWED"
+    },
+    "flagNoRemove": {
+      "type": "boolean",
+      "description": "Flag for DELETION NOT ALLOWED"
+    }
+  },
+  "required": [
+    "servingCellNrcgi",
+    "neighborCellNrpci",
+    "neighborCellNrcgi",
+    "flagNoHo",
+    "flagNoXn",
+    "flagNoRemove"
+  ]
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/caas-ingress-ricaux-pods.json b/dashboard/webapp-backend/src/test/resources/caas-ingress-ricaux-pods.json
new file mode 100644
index 0000000..216b7db
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/caas-ingress-ricaux-pods.json
@@ -0,0 +1,9 @@
+{
+	"kind": "PodList",
+	"apiVersion": "v1",
+	"metadata": {
+		"selfLink": "/api/v1/namespaces/ricaux/pods",
+		"resourceVersion": "1594484"
+	},
+	"items": []
+}
diff --git a/dashboard/webapp-backend/src/test/resources/caas-ingress-ricplt-pods.json b/dashboard/webapp-backend/src/test/resources/caas-ingress-ricplt-pods.json
new file mode 100644
index 0000000..cf9c1a0
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/caas-ingress-ricplt-pods.json
@@ -0,0 +1,1847 @@
+{
+	"kind": "PodList",
+	"apiVersion": "v1",
+	"metadata": {
+		"selfLink": "/api/v1/namespaces/ricplt/pods",
+		"resourceVersion": "3189380"
+	},
+	"items": [
+		{
+			"metadata": {
+				"name": "deployment-ricplt-a1mediator-74d9fc4c8c-77vtp",
+				"generateName": "deployment-ricplt-a1mediator-74d9fc4c8c-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-a1mediator-74d9fc4c8c-77vtp",
+				"uid": "487c582d-36d6-406a-92a3-bfbce04b83de",
+				"resourceVersion": "2945631",
+				"creationTimestamp": "2019-10-18T16:15:04Z",
+				"labels": {
+					"app": "ricplt-a1mediator",
+					"group": "nagios",
+					"pod-template-hash": "74d9fc4c8c",
+					"release": "r1-a1mediator"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-a1mediator-74d9fc4c8c",
+						"uid": "84ef8695-3eb2-4dcd-b214-9d00ab5fb6b3",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "a1conf",
+						"configMap": {
+							"name": "configmap-ricplt-a1mediator-a1conf",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-a1mediator",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-a1:0.10.3",
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 10000,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 4562,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-a1mediator-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "a1conf",
+								"mountPath": "/opt/ricmanifest.json",
+								"subPath": "ricmanifest.json"
+							},
+							{
+								"name": "a1conf",
+								"mountPath": "/opt/rmr_string_int_mapping.txt",
+								"subPath": "rmr_string_int_mapping.txt"
+							},
+							{
+								"name": "a1conf",
+								"mountPath": "/opt/route/local.rt",
+								"subPath": "local.rt"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"livenessProbe": {
+							"httpGet": {
+								"path": "/a1-p/healthcheck",
+								"port": "http",
+								"scheme": "HTTP"
+							},
+							"timeoutSeconds": 1,
+							"periodSeconds": 10,
+							"successThreshold": 1,
+							"failureThreshold": 3
+						},
+						"readinessProbe": {
+							"httpGet": {
+								"path": "/a1-p/healthcheck",
+								"port": "http",
+								"scheme": "HTTP"
+							},
+							"timeoutSeconds": 1,
+							"periodSeconds": 10,
+							"successThreshold": 1,
+							"failureThreshold": 3
+						},
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.202",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "a1mediator",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:04Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:09Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:09Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:04Z"
+					}
+				],
+				"hostIP": "172.29.16.202",
+				"podIP": "10.244.4.71",
+				"podIPs": [
+					{
+						"ip": "10.244.4.71"
+					}
+				],
+				"startTime": "2019-10-18T16:15:04Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-a1mediator",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:05Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-a1:0.10.3",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-a1@sha256:64a61ed84d4d05dfa1690bb45949da333d7b088e2e12dbba0ce60c06445f52cb",
+						"containerID": "docker://07ceed10ee2a4413c167951e458b59a505a073cce82ac543146e58b698489d59",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-appmgr-6ccf55b98b-kbkt4",
+				"generateName": "deployment-ricplt-appmgr-6ccf55b98b-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-appmgr-6ccf55b98b-kbkt4",
+				"uid": "4e084e8a-eb0a-4ea2-9cc1-7f812cd6bb28",
+				"resourceVersion": "2945633",
+				"creationTimestamp": "2019-10-18T16:15:01Z",
+				"labels": {
+					"app": "ricplt-appmgr",
+					"group": "nagios",
+					"pod-template-hash": "6ccf55b98b",
+					"release": "r1-appmgr"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-appmgr-6ccf55b98b",
+						"uid": "a169ebc4-9b7c-4b8d-81ed-6364e07df24e",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "config-volume",
+						"configMap": {
+							"name": "configmap-ricplt-appmgr-appconfig",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "cert-volume",
+						"configMap": {
+							"name": "xapp-mgr-certs",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "secret-volume",
+						"secret": {
+							"secretName": "xapp-mgr-creds",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "helm-secret-volume",
+						"emptyDir": {
+							
+						}
+					},
+					{
+						"name": "appmgr-bin-volume",
+						"configMap": {
+							"name": "configmap-ricplt-appmgr-bin",
+							"defaultMode": 493
+						}
+					},
+					{
+						"name": "svcacct-ricplt-appmgr-token-kclzw",
+						"secret": {
+							"secretName": "svcacct-ricplt-appmgr-token-kclzw",
+							"defaultMode": 420
+						}
+					}
+				],
+				"initContainers": [
+					{
+						"name": "container-ricplt-appmgr-copy-tiller-secret",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/it-dep-init:0.0.1",
+						"command": [
+							"/appmgr-tiller-secret-copier.sh"
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-appmgr-env"
+								}
+							}
+						],
+						"env": [
+							{
+								"name": "SVCACCT_NAME",
+								"value": "svcacct-ricplt-appmgr"
+							},
+							{
+								"name": "CLUSTER_NAME",
+								"value": "kubernetes"
+							},
+							{
+								"name": "KUBECONFIG",
+								"value": "/tmp/kubeconfig"
+							},
+							{
+								"name": "K8S_API_HOST",
+								"value": "https://10.254.0.1:443"
+							},
+							{
+								"name": "SECRET_NAMESPACE",
+								"value": "ricinfra"
+							},
+							{
+								"name": "SECRET_NAME",
+								"value": "secret-helm-client-ricxapp"
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "helm-secret-volume",
+								"mountPath": "/opt/ric/secret"
+							},
+							{
+								"name": "appmgr-bin-volume",
+								"mountPath": "/svcacct-to-kubeconfig.sh",
+								"subPath": "svcacct-to-kubeconfig.sh"
+							},
+							{
+								"name": "appmgr-bin-volume",
+								"mountPath": "/appmgr-tiller-secret-copier.sh",
+								"subPath": "appmgr-tiller-secret-copier.sh"
+							},
+							{
+								"name": "svcacct-ricplt-appmgr-token-kclzw",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "IfNotPresent"
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-appmgr",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-appmgr:0.1.9",
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 8080,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 4560,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-appmgr-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "config-volume",
+								"mountPath": "/opt/ric/config/appmgr.yaml",
+								"subPath": "appmgr.yaml"
+							},
+							{
+								"name": "cert-volume",
+								"mountPath": "/opt/ric/certificates"
+							},
+							{
+								"name": "helm-secret-volume",
+								"mountPath": "/opt/ric/secret"
+							},
+							{
+								"name": "secret-volume",
+								"mountPath": "/opt/ric/secret/helm_repo_username",
+								"subPath": "helm_repo_username"
+							},
+							{
+								"name": "secret-volume",
+								"mountPath": "/opt/ric/secret/helm_repo_password",
+								"subPath": "helm_repo_password"
+							},
+							{
+								"name": "svcacct-ricplt-appmgr-token-kclzw",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "svcacct-ricplt-appmgr",
+				"serviceAccount": "svcacct-ricplt-appmgr",
+				"nodeName": "172.29.16.203",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "appmgr",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:03Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:04Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:04Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:01Z"
+					}
+				],
+				"hostIP": "172.29.16.203",
+				"podIP": "10.244.3.252",
+				"podIPs": [
+					{
+						"ip": "10.244.3.252"
+					}
+				],
+				"startTime": "2019-10-18T16:15:01Z",
+				"initContainerStatuses": [
+					{
+						"name": "container-ricplt-appmgr-copy-tiller-secret",
+						"state": {
+							"terminated": {
+								"exitCode": 0,
+								"reason": "Completed",
+								"startedAt": "2019-10-18T16:15:02Z",
+								"finishedAt": "2019-10-18T16:15:02Z",
+								"containerID": "docker://130db0adfad526204726bf11fe24741d94f11f39f97f0d826b066ec852e5a452"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/it-dep-init:0.0.1",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/it-dep-init@sha256:d3a2c02660a0b5da5a7e38626c49018ca7f5e3bc39106b0728ff72245cd20be5",
+						"containerID": "docker://130db0adfad526204726bf11fe24741d94f11f39f97f0d826b066ec852e5a452"
+					}
+				],
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-appmgr",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:03Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-appmgr:0.1.9",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-appmgr@sha256:5c076f702d570b385d10200cda8d504475ce44eb1bcbb131b1d50e00eabae4d7",
+						"containerID": "docker://791f455c1974a100aaa09ab0a290e438d75aa1c3aadcb717c42d53e02cdedb83",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-dbaas-d4c9f7b88-7wgb9",
+				"generateName": "deployment-ricplt-dbaas-d4c9f7b88-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-dbaas-d4c9f7b88-7wgb9",
+				"uid": "e54e51fc-c4bd-4308-805e-16c2d588dacd",
+				"resourceVersion": "2945634",
+				"creationTimestamp": "2019-10-18T16:15:01Z",
+				"labels": {
+					"app": "ricplt-dbaas",
+					"group": "nagios",
+					"pod-template-hash": "d4c9f7b88",
+					"release": "r1-dbaas"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-dbaas-d4c9f7b88",
+						"uid": "7e8d5d34-efa9-41fe-b92f-d9b71bc40360",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-dbaas",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-dbaas:0.1.0",
+						"ports": [
+							{
+								"name": "sql",
+								"containerPort": 6379,
+								"protocol": "TCP"
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 0,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.202",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:01Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:03Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:03Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:01Z"
+					}
+				],
+				"hostIP": "172.29.16.202",
+				"podIP": "10.244.4.70",
+				"podIPs": [
+					{
+						"ip": "10.244.4.70"
+					}
+				],
+				"startTime": "2019-10-18T16:15:01Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-dbaas",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:02Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-dbaas:0.1.0",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-dbaas@sha256:f63cfa353f355155ec6455a68d18c631900a2602bf7cc2ba35d6210971736b76",
+						"containerID": "docker://8972d8b61d5c3ff56b50814575647d70fb3307602506cda3e34b6734c28a3f36",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-e2mgr-86c76477c5-mf5t5",
+				"generateName": "deployment-ricplt-e2mgr-86c76477c5-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-e2mgr-86c76477c5-mf5t5",
+				"uid": "5f2231ea-bec0-46fc-a8f8-09b0b80e982f",
+				"resourceVersion": "2945636",
+				"creationTimestamp": "2019-10-18T16:15:02Z",
+				"labels": {
+					"app": "ricplt-e2mgr",
+					"group": "nagios",
+					"pod-template-hash": "86c76477c5",
+					"release": "r1-e2mgr"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-e2mgr-86c76477c5",
+						"uid": "f7dfd4a3-4eb3-4c46-a6c8-adc4ae37ef57",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "local-router-file",
+						"configMap": {
+							"name": "configmap-ricplt-e2mgr-router-configmap",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "local-configuration-file",
+						"configMap": {
+							"name": "configmap-ricplt-e2mgr-configuration-configmap",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-e2mgr",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-e2mgr:2.0.7",
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 3800,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 3801,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-e2mgr-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "local-router-file",
+								"mountPath": "/opt/E2Manager/router.txt",
+								"subPath": "router.txt"
+							},
+							{
+								"name": "local-configuration-file",
+								"mountPath": "/opt/E2Manager/resources/configuration.yaml",
+								"subPath": "configuration.yaml"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always",
+						"securityContext": {
+							"privileged": false
+						},
+						"stdin": true,
+						"tty": true
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.204",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "e2mgr",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:02Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:05Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:05Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:02Z"
+					}
+				],
+				"hostIP": "172.29.16.204",
+				"podIP": "10.244.2.100",
+				"podIPs": [
+					{
+						"ip": "10.244.2.100"
+					}
+				],
+				"startTime": "2019-10-18T16:15:02Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-e2mgr",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:04Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-e2mgr:2.0.7",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-e2mgr@sha256:87fa19a934215bdec71a355ef08eec9e273c992bab80af727f4f1b7a74ebacfa",
+						"containerID": "docker://ff3a2fcfc72b00e3c317899f2b620da2f65e3de260623daed7825f6a74dbcb5c",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-e2term-5dcf8b54b-5mkxl",
+				"generateName": "deployment-ricplt-e2term-5dcf8b54b-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-e2term-5dcf8b54b-5mkxl",
+				"uid": "8fae1dcd-5e42-4a66-be6f-e893d5563689",
+				"resourceVersion": "2945639",
+				"creationTimestamp": "2019-10-18T16:15:03Z",
+				"labels": {
+					"app": "ricplt-e2term",
+					"group": "nagios",
+					"pod-template-hash": "5dcf8b54b",
+					"release": "r1-e2term"
+				},
+				"annotations": {
+					"danm.k8s.io/interfaces": "[\n  {\"clusterNetwork\":\"default\", \"ip\":\"dynamic\"},\n  {\"clusterNetwork\":\"ran\", \"ip\":\"dynamic\", \"ip6\":\"dynamic\"}\n]\n",
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-e2term-5dcf8b54b",
+						"uid": "cc20b8a0-6d74-4fb9-b384-bdce9c9ae184",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "local-router-file",
+						"configMap": {
+							"name": "configmap-ricplt-e2term-router-configmap",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "localtime",
+						"hostPath": {
+							"path": "/etc/localtime",
+							"type": ""
+						}
+					},
+					{
+						"name": "pizpub-config",
+						"configMap": {
+							"name": "configmap-ricplt-e2term-pizpub",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "vol-shared",
+						"persistentVolumeClaim": {
+							"claimName": "pvc-ricplt-e2term"
+						}
+					},
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-e2term",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-e2:2.0.7.5",
+						"ports": [
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 38000,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-e2term-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "local-router-file",
+								"mountPath": "/opt/e2/router.txt",
+								"subPath": "router.txt"
+							},
+							{
+								"name": "local-router-file",
+								"mountPath": "/tmp/rmr_verbose",
+								"subPath": "rmr_verbose"
+							},
+							{
+								"name": "vol-shared",
+								"mountPath": "/data/outgoing/",
+								"subPath": "outgoing"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always",
+						"securityContext": {
+							"privileged": false
+						},
+						"stdin": true,
+						"tty": true
+					},
+					{
+						"name": "container-ricplt-e2term-pizpub",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/pizpub:0.0.5155",
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "localtime",
+								"readOnly": true,
+								"mountPath": "/etc/localtime"
+							},
+							{
+								"name": "vol-shared",
+								"mountPath": "/data"
+							},
+							{
+								"name": "pizpub-config",
+								"mountPath": "/opt/app/config/conf/"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"lifecycle": {
+							"postStart": {
+								"exec": {
+									"command": [
+										"/bin/sh",
+										"/opt/app/config/conf/cleaner.sh",
+										"/data/sent",
+										"3"
+									]
+								}
+							}
+						},
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirstWithHostNet",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.201",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "e2term",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:22Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:33Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:33Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:22Z"
+					}
+				],
+				"hostIP": "172.29.16.201",
+				"podIP": "10.244.1.90",
+				"podIPs": [
+					{
+						"ip": "10.244.1.90"
+					}
+				],
+				"startTime": "2019-10-18T16:15:22Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-e2term",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:31Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "ranco-dev-tools.eastus.cloudapp.azure.com:10001/ric-plt-e2:2.0.7.5",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-e2@sha256:0ea1a356d018495a93e124ddd793e09626bf6e4d9b96355e731673ef7fab5a1f",
+						"containerID": "docker://bf9ca87dbad9436b0ed99ffe38036fb49033a9bc2cf2eb548397fbc9c48f1c3d",
+						"started": true
+					},
+					{
+						"name": "container-ricplt-e2term-pizpub",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:32Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "ranco-dev-tools.eastus.cloudapp.azure.com:10001/pizpub:0.0.5155",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/pizpub@sha256:138c2d2d25e6528c4a5a8a402c277722d1c1fd4d6792b644967acd538affb1ed",
+						"containerID": "docker://93e39661623b7afc8156008bb6fbc206458964a6eb0633f80164e4c7ef59fe76",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-jaegeradapter-6ff8676c7-m4qkf",
+				"generateName": "deployment-ricplt-jaegeradapter-6ff8676c7-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-jaegeradapter-6ff8676c7-m4qkf",
+				"uid": "d6a5e9e9-87d0-4d1e-b1b7-cc1a4f20dc2e",
+				"resourceVersion": "2945640",
+				"creationTimestamp": "2019-10-18T16:15:08Z",
+				"labels": {
+					"app": "ricplt-jaegeradapter",
+					"group": "nagios",
+					"pod-template-hash": "6ff8676c7",
+					"release": "r1-jaegeradapter"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-jaegeradapter-6ff8676c7",
+						"uid": "98bc03d7-a082-4ac1-9b89-064022a37dff",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-jaegeradapter",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/all-in-one:1.12",
+						"ports": [
+							{
+								"name": "zipkincompact",
+								"containerPort": 5775,
+								"protocol": "UDP"
+							},
+							{
+								"name": "jaegercompact",
+								"containerPort": 6831,
+								"protocol": "UDP"
+							},
+							{
+								"name": "jaegerbinary",
+								"containerPort": 6832,
+								"protocol": "UDP"
+							},
+							{
+								"name": "httpquery",
+								"containerPort": 16686,
+								"protocol": "TCP"
+							},
+							{
+								"name": "httpconfig",
+								"containerPort": 5778,
+								"protocol": "TCP"
+							},
+							{
+								"name": "zipkinhttp",
+								"containerPort": 9411,
+								"protocol": "TCP"
+							},
+							{
+								"name": "jaegerhttp",
+								"containerPort": 14268,
+								"protocol": "TCP"
+							},
+							{
+								"name": "jaegerhttpt",
+								"containerPort": 14267,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-jaegeradapter"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"livenessProbe": {
+							"httpGet": {
+								"path": "/",
+								"port": 16686,
+								"scheme": "HTTP"
+							},
+							"timeoutSeconds": 1,
+							"periodSeconds": 10,
+							"successThreshold": 1,
+							"failureThreshold": 3
+						},
+						"readinessProbe": {
+							"httpGet": {
+								"path": "/",
+								"port": 16686,
+								"scheme": "HTTP"
+							},
+							"timeoutSeconds": 1,
+							"periodSeconds": 10,
+							"successThreshold": 1,
+							"failureThreshold": 3
+						},
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.203",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "jaegeradapter",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:08Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:15Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:15Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:08Z"
+					}
+				],
+				"hostIP": "172.29.16.203",
+				"podIP": "10.244.3.254",
+				"podIPs": [
+					{
+						"ip": "10.244.3.254"
+					}
+				],
+				"startTime": "2019-10-18T16:15:08Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-jaegeradapter",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:09Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/all-in-one:1.12",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/all-in-one@sha256:51b901b653f4a4ca5dd50be9133c08dacf2d3eb092e507c213e7955a0e132297",
+						"containerID": "docker://95013a49a1705a503f5f7dde7a38fa7277523a73cdef96d264fcefe170e8a921",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-rsm-88477585f-qkkj7",
+				"generateName": "deployment-ricplt-rsm-88477585f-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-rsm-88477585f-qkkj7",
+				"uid": "d4c58ff4-743e-4ed6-bd36-aeb02daa1ca6",
+				"resourceVersion": "2945642",
+				"creationTimestamp": "2019-10-18T16:15:07Z",
+				"labels": {
+					"app": "ricplt-rsm",
+					"group": "nagios",
+					"pod-template-hash": "88477585f",
+					"release": "r1-rsm"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-rsm-88477585f",
+						"uid": "1fe7de57-90d9-4898-9b71-1ae9c4a6f014",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "local-router-file",
+						"configMap": {
+							"name": "configmap-ricplt-rsm-router-configmap",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "local-configuration-file",
+						"configMap": {
+							"name": "configmap-ricplt-rsm",
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-rsm",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-rsm:2.0.7",
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 4800,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 4801,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-rsm-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "local-router-file",
+								"mountPath": "/opt/RSM/router.txt",
+								"subPath": "router.txt"
+							},
+							{
+								"name": "local-configuration-file",
+								"mountPath": "/opt/RSM/resources/configuration.yaml",
+								"subPath": "configuration.yaml"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always",
+						"securityContext": {
+							"privileged": false
+						},
+						"stdin": true,
+						"tty": true
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.203",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "rsm",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:07Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:09Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:09Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:07Z"
+					}
+				],
+				"hostIP": "172.29.16.203",
+				"podIP": "10.244.3.253",
+				"podIPs": [
+					{
+						"ip": "10.244.3.253"
+					}
+				],
+				"startTime": "2019-10-18T16:15:07Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-rsm",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:08Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-rsm:2.0.7",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-rsm@sha256:e6fb3bc17fcd5a2fbc7d34eeb744fbfed4eaaaf6c669e084b379ee05368820d3",
+						"containerID": "docker://5e90673a6b2c292f2ce7c731bf8747c8a63f429eca08d08a993130001c7d6f5e",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-submgr-7549b87fb8-4t6mx",
+				"generateName": "deployment-ricplt-submgr-7549b87fb8-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-submgr-7549b87fb8-4t6mx",
+				"uid": "c6fbd48b-2757-421c-a534-f1931b04312b",
+				"resourceVersion": "2945646",
+				"creationTimestamp": "2019-10-18T16:15:05Z",
+				"labels": {
+					"app": "ricplt-submgr",
+					"group": "nagios",
+					"pod-template-hash": "7549b87fb8",
+					"release": "r1-submgr"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-submgr-7549b87fb8",
+						"uid": "e2b9dd9f-cca4-4f64-9e11-b6ee174c4f6f",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "config-volume",
+						"configMap": {
+							"name": "submgrcfg",
+							"items": [
+								{
+									"key": "submgrcfg",
+									"path": "submgr-config.yaml",
+									"mode": 420
+								}
+							],
+							"defaultMode": 420
+						}
+					},
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-submgr",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-submgr:0.10.5",
+						"command": [
+							"/run_submgr.sh"
+						],
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 3800,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrroute",
+								"containerPort": 4561,
+								"protocol": "TCP"
+							},
+							{
+								"name": "rmrdata",
+								"containerPort": 4560,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-submgr-env"
+								}
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "config-volume",
+								"mountPath": "/cfg"
+							},
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.205",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "submgr",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:05Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:07Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:07Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:05Z"
+					}
+				],
+				"hostIP": "172.29.16.205",
+				"podIP": "10.244.0.168",
+				"podIPs": [
+					{
+						"ip": "10.244.0.168"
+					}
+				],
+				"startTime": "2019-10-18T16:15:05Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-submgr",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:07Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-submgr:0.10.5",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-submgr@sha256:aa8ada253d0800a849b6124fc54793815caaf93ad46b8d47cdd1f590ef69f813",
+						"containerID": "docker://724ba7834ef80d1f3c85ae7990ead480ed5226f0275816bae358edc9ddf54da6",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		},
+		{
+			"metadata": {
+				"name": "deployment-ricplt-vespamgr-55f6484b7-g5zfw",
+				"generateName": "deployment-ricplt-vespamgr-55f6484b7-",
+				"namespace": "ricplt",
+				"selfLink": "/api/v1/namespaces/ricplt/pods/deployment-ricplt-vespamgr-55f6484b7-g5zfw",
+				"uid": "a7ae0f4a-adfa-48c6-8c41-725ba1c84b11",
+				"resourceVersion": "2945649",
+				"creationTimestamp": "2019-10-18T16:15:06Z",
+				"labels": {
+					"app": "ricplt-vespamgr",
+					"group": "nagios",
+					"pod-template-hash": "55f6484b7",
+					"release": "r1-vespamgr"
+				},
+				"annotations": {
+					"kubernetes.io/psp": "caas-default"
+				},
+				"ownerReferences": [
+					{
+						"apiVersion": "apps/v1",
+						"kind": "ReplicaSet",
+						"name": "deployment-ricplt-vespamgr-55f6484b7",
+						"uid": "dc5e0e81-23da-4fed-99da-14cb7a8fe06c",
+						"controller": true,
+						"blockOwnerDeletion": true
+					}
+				]
+			},
+			"spec": {
+				"volumes": [
+					{
+						"name": "default-token-5j24g",
+						"secret": {
+							"secretName": "default-token-5j24g",
+							"defaultMode": 420
+						}
+					}
+				],
+				"containers": [
+					{
+						"name": "container-ricplt-vespamgr",
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-vespamgr:0.0.5",
+						"ports": [
+							{
+								"name": "http",
+								"containerPort": 8080,
+								"protocol": "TCP"
+							}
+						],
+						"envFrom": [
+							{
+								"configMapRef": {
+									"name": "configmap-ricplt-vespamgr"
+								}
+							},
+							{
+								"secretRef": {
+									"name": "vespa-secrets"
+								}
+							}
+						],
+						"env": [
+							{
+								"name": "VESMGR_APPMGRDOMAIN",
+								"value": "appmgr-service"
+							}
+						],
+						"resources": {
+							
+						},
+						"volumeMounts": [
+							{
+								"name": "default-token-5j24g",
+								"readOnly": true,
+								"mountPath": "/var/run/secrets/kubernetes.io/serviceaccount"
+							}
+						],
+						"livenessProbe": {
+							"httpGet": {
+								"path": "/supervision",
+								"port": 8080,
+								"scheme": "HTTP"
+							},
+							"initialDelaySeconds": 30,
+							"timeoutSeconds": 20,
+							"periodSeconds": 60,
+							"successThreshold": 1,
+							"failureThreshold": 3
+						},
+						"terminationMessagePath": "/dev/termination-log",
+						"terminationMessagePolicy": "File",
+						"imagePullPolicy": "Always"
+					}
+				],
+				"restartPolicy": "Always",
+				"terminationGracePeriodSeconds": 30,
+				"dnsPolicy": "ClusterFirst",
+				"serviceAccountName": "default",
+				"serviceAccount": "default",
+				"nodeName": "172.29.16.204",
+				"securityContext": {
+					
+				},
+				"imagePullSecrets": [
+					{
+						"name": "docker-reg-cred"
+					}
+				],
+				"hostname": "vespamgr",
+				"schedulerName": "default-scheduler",
+				"enableServiceLinks": true
+			},
+			"status": {
+				"phase": "Running",
+				"conditions": [
+					{
+						"type": "Initialized",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:06Z"
+					},
+					{
+						"type": "Ready",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:08Z"
+					},
+					{
+						"type": "ContainersReady",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:08Z"
+					},
+					{
+						"type": "PodScheduled",
+						"status": "True",
+						"lastProbeTime": null,
+						"lastTransitionTime": "2019-10-18T16:15:06Z"
+					}
+				],
+				"hostIP": "172.29.16.204",
+				"podIP": "10.244.2.101",
+				"podIPs": [
+					{
+						"ip": "10.244.2.101"
+					}
+				],
+				"startTime": "2019-10-18T16:15:06Z",
+				"containerStatuses": [
+					{
+						"name": "container-ricplt-vespamgr",
+						"state": {
+							"running": {
+								"startedAt": "2019-10-18T16:15:08Z"
+							}
+						},
+						"lastState": {
+							
+						},
+						"ready": true,
+						"restartCount": 0,
+						"image": "registry.kube-system.svc.rec.io:5555/ric/ric-plt-vespamgr:0.0.5",
+						"imageID": "docker-pullable://registry.kube-system.svc.rec.io:5555/ric/ric-plt-vespamgr@sha256:97753ef72471a5fddd59ff35a2fe763b041848f6e83214acb78ad73c7316b371",
+						"containerID": "docker://80884c969cbf802945c075afc47d747b5e747e4645c691d376820c9d61094e7c",
+						"started": true
+					}
+				],
+				"qosClass": "BestEffort"
+			}
+		}
+	]
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/demo-policy-schema-1.json b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-1.json
new file mode 100644
index 0000000..09999ef
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-1.json
@@ -0,0 +1,53 @@
+{
+  "type": "object",
+  "title": "Car",
+  "properties": {
+    "make": {
+      "type": "string",
+      "enum": [
+        "Toyota",
+        "BMW",
+        "Honda",
+        "Ford",
+        "Chevy",
+        "VW"
+      ]
+    },
+    "model": {
+      "type": "string"
+    },
+    "year": {
+      "type": "integer",
+      "enum": [
+        1995,
+        1996,
+        1997,
+        1998,
+        1999,
+        2000,
+        2001,
+        2002,
+        2003,
+        2004,
+        2005,
+        2006,
+        2007,
+        2008,
+        2009,
+        2010,
+        2011,
+        2012,
+        2013,
+        2014
+      ],
+      "default": 2008
+    },
+    "safety": {
+      "type": "integer",
+      "format": "rating",
+      "maximum": 5,
+      "exclusiveMaximum": false,
+      "readonly": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/demo-policy-schema-2.json b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-2.json
new file mode 100644
index 0000000..69ec678
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-2.json
@@ -0,0 +1,21 @@
+{
+  "$id": "https://example.com/person.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Person",
+  "type": "object",
+  "properties": {
+    "firstName": {
+      "type": "string",
+      "description": "The person's first name."
+    },
+    "lastName": {
+      "type": "string",
+      "description": "The person's last name."
+    },
+    "age": {
+      "description": "Age in years which must be equal to or greater than zero.",
+      "type": "integer",
+      "minimum": 0
+    }
+  }
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/demo-policy-schema-3.json b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-3.json
new file mode 100644
index 0000000..3be9959
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/demo-policy-schema-3.json
@@ -0,0 +1,39 @@
+{
+  "$id": "https://example.com/arrays.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "description": "A representation of a person, company, organization, or place",
+  "type": "object",
+  "properties": {
+    "fruits": {
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "vegetables": {
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/veggie"
+      }
+    }
+  },
+  "definitions": {
+    "veggie": {
+      "type": "object",
+      "required": [
+        "veggieName",
+        "veggieLike"
+      ],
+      "properties": {
+        "veggieName": {
+          "type": "string",
+          "description": "The name of the vegetable."
+        },
+        "veggieLike": {
+          "type": "boolean",
+          "description": "Do I like this vegetable?"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dashboard/webapp-backend/src/test/resources/key.properties b/dashboard/webapp-backend/src/test/resources/key.properties
new file mode 100644
index 0000000..85a5689
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/key.properties
@@ -0,0 +1,22 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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/dashboard/webapp-backend/src/test/resources/portal.properties b/dashboard/webapp-backend/src/test/resources/portal.properties
new file mode 100644
index 0000000..326539e
--- /dev/null
+++ b/dashboard/webapp-backend/src/test/resources/portal.properties
@@ -0,0 +1,26 @@
+# ========================LICENSE_START=================================
+# O-RAN-SC
+# %%
+# Copyright (C) 2019 AT&T Intellectual Property
+# %%
+# 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