From: Lott, Christopher (cl778h) Date: Wed, 17 Jul 2019 18:28:11 +0000 (-0400) Subject: Integrate EPSDK-FW library for auth and users X-Git-Tag: R2~52 X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=commitdiff_plain;h=3f812ea25d352ec33d07f5ffa4c2aa2a77e8e793;p=portal%2Fric-dashboard.git Integrate EPSDK-FW library for auth and users Change-Id: I8cba9e80e50b0e890783610d769e275091f942a7 Signed-off-by: Lott, Christopher (cl778h) --- diff --git a/README.md b/README.md index 3cec2d60..e70617f5 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,39 @@ # O-RAN-SC RIC Dashboard Web Application -This webapp is built with Angular 7 and Spring-Boot 2. +The O-RAN SC RIC Dashboard provides administrative and operator functions +for a disaggregated radio access network (RAN) controller. +The web app is built as a single-page app using an Angular 8 front end +and a Spring-Boot 2 back end. ## Deployment configuration -The application expects an application.properties file to be provided, -probably mounted as a file from a Kubernetes configuration map, with -the following content: +The application expects the following configuration files, +usually mounted as files from Kubernetes configuration maps: - # A1 Mediator - a1med.url = http://A1-URL - # ANR xApp - anrxapp.url = http://ANR-URL - # E2 Manager - e2mgr.url = http://E2-URL - # Xapp Manager - xappmgr.url = http://MGR-URL + application.properties (in launch directory) + key.properties (on classpath) + portal.properties (on classpath) + +Sample files are in directory src/main/resources and src/test/resources. ## Development guide This section gives a quickstart guide for developers. -### Check prerequisites +### Prerequisites -1. Java development kit (JDK), version 1.8 or later +1. Java development kit (JDK), version 11 or later 2. Maven dependency-management tool, version 3.4 or later ### Build and launch the web app - mvn -Ddocker.skip=true clean install - cd webapp-backend - mvn spring-boot:run - -Then open a browser on http://localhost:8080 - -In addition to the above, you can run the Angular server -for debugging the frontend and backend separately: - - cd webapp-frontend - ./ng serve --proxy-config proxy.conf.json +Instructions for launching a backend Sprint-Boot server +are available in the webapp-backend README file. +After launching, open a browser on http://localhost:8080 -Then open a browser on http://localhost:4200 +Instructions for launching a frontend Angular server (only for development) +are available in the webapp-frontend README file. +After launching, open a browser on http://localhost:4200 ## License diff --git a/docs/release-notes.rst b/docs/release-notes.rst index b6eb3f20..10560391 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -20,7 +20,7 @@ RIC Dashboard Release Notes =========================== -Version 1.2.0, 17 July 2019 +Version 1.2.0, 26 July 2019 --------------------------- * Split URL properties into prefix/suffix parts * Add jacoco plugin to back-end for code coverage @@ -29,6 +29,7 @@ Version 1.2.0, 17 July 2019 * Drop mock RAN names feature that supported R1 testing * Extend mock endpoints to simulate delay seen in tests * Move mock configuration classes into test area +* Add EPSDK-FW user management and Portal security Version 1.0.5, 5 July 2019 -------------------------- diff --git a/webapp-backend/.gitignore b/webapp-backend/.gitignore index 0367d238..891095d0 100644 --- a/webapp-backend/.gitignore +++ b/webapp-backend/.gitignore @@ -30,3 +30,5 @@ /build/ /application-tlab2.properties +/application.properties +/users.json diff --git a/webapp-backend/README.md b/webapp-backend/README.md index 8a142d78..6751ef99 100644 --- a/webapp-backend/README.md +++ b/webapp-backend/README.md @@ -1,19 +1,123 @@ # RIC Dashboard Web Application Backend -## Launch server +The RIC Dashboard back-end provides REST services to the Dashboard +front-end Typescript features running in the user's browser. For +production use, this server also offers the Angular application files. -Run `mvn -Dspring.config.name=application-abc spring-boot:run` to run a server configured -by the file 'application-abc.properties' in the local directory. +The server uses the ONAP Portal's "EPSDK-FW" library to support +single-sign-on (SSO) feature, which requires users to authenticate +at the ONAP Portal UI. Authentication features including SSO are +excluded by Spring profiles when running the back-end as a development +server, see below. -## Development server +## Launch production server -Set an environment variable via JVM argument "-Dorg.oransc.ric.portal.dashboard=mock" -and run the JUnit test case DashboardServerTest for a development server to run standalone -with mock configuration and data that simulates the behavior of remote endpoints. +This server requires several configuration files: + + application.properties (in launch directory) + key.properties (on Java classpath) + portal.properties (on Java classpath) + +These steps are required: + +1. Check the set of properties files in the config folder, and create + files from templates as needed. E.g., copy + "key.properties.template" to "key.properties". +2. Add the config folder to the Java classpath +3a. Launch the server with this command-line invocation: + + java -cp config:target/ric-dash-be-1.2.0-SNAPSHOT.jar \ + -Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication \ + org.springframework.boot.loader.PropertiesLauncher + +3b. To use the configuration in the "application-abc.properties" file, addd a +key-value pair for "spring.config.name" and launch with an invocation like this: + + java -cp config:target/ric-dash-be-1.2.0-SNAPSHOT.jar \ + -Dspring.config.name=application-abc \ + -Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication \ + org.springframework.boot.loader.PropertiesLauncher + +### Production user authentication + +The regular server authenticates requests using cookies that are set +by the ONAP Portal: + + EPService + UserId + +The EPService value is not checked. The UserId value is decrypted +using a secret key shared with the ONAP Portal to yield a user ID. +That ID must match a user's loginId defined in the user manager. + +The regular server checks requests for the following granted +authorities (role names), as defined in the DashboardConstants class. +A standard user can read (GET) all methods but not make changes. +An administrator can read (GET) and write (POST PUT DELETE) all data. + + Standard_User + System_Administrator + +Use the following structure in a JSON file to publish a user for the +user manager: + + [ + { + "orgId":null, + "managerId":null, + "firstName":"Demo", + "middleInitial":null, + "lastName":"User", + "phone":null, + "email":null, + "hrid":null, + "orgUserId":null, + "orgCode":null, + "orgManagerUserId":null, + "jobTitle":null, + "loginId":"demo", + "active":true, + "roles":[ + { + "id":null, + "name":"Standard_User", + "roleFunctions":null + } + ] + } + ] + + +## Launch development server + +The development server uses local configuration and serves mock data +that simulates the behavior of remote endpoints. The directory +src/main/resources contains usable versions of the required property +files. These steps are required to launch: + +1. Set an environment variable via JVM argument: "-Dorg.oransc.ric.portal.dashboard=mock" +2. Run the JUnit test case DashboardServerTest -- not exactly a "test" because it never finishes. + +Both steps can be done with this command-line invocation: + + mvn -Dorg.oransc.ric.portal.dashboard=mock -Dtest=DashboardTestServer test + +### Development user authentication + +The development server requires basic HTTP user authentication for all requests. Like +the production server, it requires HTTP headers with authentication for Portal API +requests. The credentials are in constants in this Java class in the src/test/java +folder: + + org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration + +Like the production server, the development server also performs role-based +authentication on requests. The user name-role name associations are defined +in the class shown above. ## Swagger API documentation -View the server's API documentation at URL `http://localhost:8080/swagger-ui.html`. +Both a regular and a development server publish API documentation at URL `http://localhost:8080/swagger-ui.html`. ## License diff --git a/webapp-backend/config/.gitignore b/webapp-backend/config/.gitignore new file mode 100644 index 00000000..edd66f16 --- /dev/null +++ b/webapp-backend/config/.gitignore @@ -0,0 +1,2 @@ +/key.properties +/portal.properties diff --git a/webapp-backend/config/key.properties.template b/webapp-backend/config/key.properties.template new file mode 100644 index 00000000..6ba89b1e --- /dev/null +++ b/webapp-backend/config/key.properties.template @@ -0,0 +1,21 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +# Template for the file that provides a secret key for the RIC Dashboard. + +cipher.enc.key = diff --git a/webapp-backend/config/portal.properties.template b/webapp-backend/config/portal.properties.template new file mode 100644 index 00000000..601793c2 --- /dev/null +++ b/webapp-backend/config/portal.properties.template @@ -0,0 +1,34 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +# Template for the file that provides properties for the EPSDK-FW library. +# This file must be present on the Java classpath. + +# The following properties are the same in every deployment + +portal.api.impl.class = org.oransc.ric.portal.dashboard.portalapi.PortalRestCentralServiceImpl +role_access_centralized = remote + +# The following properties are DIFFERENT in every deployment + +# URL of portal login screen +ecomp_redirect_url = http://localhost/portal +# URL of portal API +ecomp_rest_url = http://localhost/portal +# Value assigned by portal instance +ueb_app_key = abcdef1234567890 diff --git a/webapp-backend/pom.xml b/webapp-backend/pom.xml index e96777fe..40db7710 100644 --- a/webapp-backend/pom.xml +++ b/webapp-backend/pom.xml @@ -34,6 +34,13 @@ limitations under the License. 0 + + + onap-releases + ONAP - Release Repository + https://nexus.onap.org/content/repositories/releases + + @@ -57,6 +64,55 @@ limitations under the License. e2-mgr-client 20190703-SNAPSHOT + + org.onap.portal.sdk + epsdk-fw + 2.4.0 + + + commons-logging + commons-logging + + + log4j + log4j + + + log4j + apache-log4j-extras + + + org.slf4j + slf4j-log4j12 + + + junit + junit + + + commons-fileupload + commons-fileupload + + + commons-beanutils + commons-beanutils + + + + org.powermock + powermock-module-junit4 + + + + org.powermock + powermock-api-mockito + + + + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-web @@ -65,6 +121,11 @@ limitations under the License. org.slf4j slf4j-api + + + org.slf4j + jcl-over-slf4j + ch.qos.logback logback-classic @@ -230,12 +291,21 @@ limitations under the License. - mkdir /maven/logs - chmod -R 777 /maven + mkdir /logs + chmod -R 777 /logs - - + + + java + -cp + maven:maven/${project.artifactId}-${project.version}.${project.packaging} + -Dloader.main=org.oransc.ric.portal.dashboard.DashboardApplication + -Xms128m + -Xmx256m + -Djava.security.egd=file:/dev/./urandom + org.springframework.boot.loader.PropertiesLauncher + diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java index 05778dc5..4819e345 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardApplication.java @@ -19,6 +19,8 @@ */ package org.oransc.ric.portal.dashboard; +import java.io.IOException; +import java.io.InputStream; import java.lang.invoke.MethodHandles; import org.slf4j.Logger; @@ -34,8 +36,19 @@ public class DashboardApplication { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - public static void main(String[] args) { + // Unfortunately these names are not available as constants + private static final String[] propertyFiles = { "ESAPI.properties", "key.properties", "portal.properties", + "validation.properties" }; + + public static void main(String[] args) throws IOException { SpringApplication.run(DashboardApplication.class, args); + for (String pf : propertyFiles) { + InputStream in = MethodHandles.lookup().lookupClass().getClassLoader().getResourceAsStream(pf); + if (in == null) + logger.warn("Failed to find property file on classpath: {}", pf); + else + in.close(); + } // Force this onto the console by using level WARN logger.warn("main: version '{}' successful start", getImplementationVersion(MethodHandles.lookup().lookupClass())); diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java index 9b80864f..bb093cda 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/DashboardConstants.java @@ -27,7 +27,18 @@ public abstract class DashboardConstants { public static final String ENDPOINT_PREFIX = "/api"; + public static final String LOGIN_PAGE = "/login.html"; + // Factor out method names used in multiple controllers public static final String VERSION_METHOD = "version"; + // The role names are defined by ONAP Portal. + // The prefix "ROLE_" is required by Spring. + // These are used in Java code annotations that require constants. + public static final String ROLE_NAME_STANDARD = "Standard_User"; + public static final String ROLE_NAME_ADMIN = "System_Administrator"; + private static final String ROLE_PREFIX = "ROLE_"; + public static final String ROLE_ADMIN = ROLE_PREFIX + ROLE_NAME_ADMIN; + public static final String ROLE_STANDARD = ROLE_PREFIX + ROLE_NAME_STANDARD; + } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java new file mode 100644 index 00000000..fe58e933 --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/LoginServlet.java @@ -0,0 +1,116 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URLEncoder; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.onap.portalsdk.core.onboarding.util.PortalApiProperties; +import org.oransc.ric.portal.dashboard.portalapi.PortalAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; + +/** + * Serves a login page that contains a link from configuration to ONAP Portal. + * This avoids the immediate redirect to Portal that is confusing to users and + * infuriating to developers. + * + * Basically this is do-it-yourself JSP :) + */ +public class LoginServlet extends HttpServlet { + + private static final long serialVersionUID = 1191385178190976568L; + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + logger.debug("init"); + super.init(servletConfig); + final String portalURL = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL); + if (portalURL == null || portalURL.length() == 0) + throw new ServletException("Failed to get property " + PortalApiConstants.ECOMP_REDIRECT_URL); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + logger.debug("doGet {}", request.getRequestURI()); + // The original page URL should arrive as a query parameter + String appUrl = request.getParameter(PortalAuthenticationFilter.REDIRECT_URL_KEY); + // If a user bookmarks the login page, then nothing arrives; + // use the original URL without the login suffix. + if (appUrl == null || appUrl.isEmpty()) { + String loginUrl = request.getRequestURL().toString(); + int indexOfLogin = loginUrl.indexOf(DashboardConstants.LOGIN_PAGE); + appUrl = loginUrl.substring(0, indexOfLogin); + } + String encodedAppUrl = URLEncoder.encode(appUrl, "UTF-8"); + String portalBaseUrl = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL); + String redirectUrl = portalBaseUrl + "?" + PortalAuthenticationFilter.REDIRECT_URL_KEY + "=" + encodedAppUrl; + String aHref = ""; + // If only Java had "here" documents. + String body = String.join(// + System.getProperty("line.separator"), // + "", // + "", // + "RIC Dashboard", // + "", // + "", // + "", // + "

RIC Dashboard

", // + "

Please log in.

", // + "

", // + aHref, "Click here to authenticate at the ONAP Portal", // + "

", // + "", // + ""); + writeAndFlush(response, MediaType.TEXT_HTML_VALUE, body); + } + + /** + * Sets the content type and writes the response. + * + * @param response + * @param contentType + * @param responseBody + * @throws IOException + */ + private void writeAndFlush(HttpServletResponse response, String contentType, String responseBody) + throws IOException { + response.setContentType(contentType); + response.getWriter().print(responseBody); + response.getWriter().flush(); + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/SpringContextCache.java new file mode 100644 index 00000000..2f1d1f69 --- /dev/null +++ b/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 and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.config; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * Allows non-Spring-managed classes to obtain the Spring application context. + */ +@Component +public class SpringContextCache implements ApplicationContextAware { + + private static ApplicationContext applicationContext = null; + + @Override + public void setApplicationContext(final ApplicationContext appContext) throws BeansException { + applicationContext = appContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java new file mode 100644 index 00000000..9357a1cf --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/config/WebSecurityConfiguration.java @@ -0,0 +1,172 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.config; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; + +import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy; +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.oransc.ric.portal.dashboard.DashboardConstants; +import org.oransc.ric.portal.dashboard.LoginServlet; +import org.oransc.ric.portal.dashboard.controller.AcXappController; +import org.oransc.ric.portal.dashboard.controller.AdminController; +import org.oransc.ric.portal.dashboard.controller.AnrXappController; +import org.oransc.ric.portal.dashboard.controller.AppManagerController; +import org.oransc.ric.portal.dashboard.controller.E2ManagerController; +import org.oransc.ric.portal.dashboard.controller.SimpleErrorController; +import org.oransc.ric.portal.dashboard.portalapi.DashboardUserManager; +import org.oransc.ric.portal.dashboard.portalapi.PortalAuthManager; +import org.oransc.ric.portal.dashboard.portalapi.PortalAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +@Profile("!test") +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // Although constructor arguments are recommended over field injection, + // this results in fewer lines of code. + @Value("${userfile}") + private String userFilePath; + @Value("${portalapi.appname}") + private String appName; + @Value("${portalapi.username}") + private String userName; + @Value("${portalapi.password}") + private String password; + @Value("${portalapi.decryptor}") + private String decryptor; + @Value("${portalapi.usercookie}") + private String userCookie; + + protected void configure(HttpSecurity http) throws Exception { + logger.debug("configure"); + // A chain of ".and()" always baffles me + http.authorizeRequests().anyRequest().authenticated(); + // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); + http.addFilterBefore(portalAuthenticationFilterBean(), BasicAuthenticationFilter.class); + } + + /** + * Resource paths that do not require authentication, especially including + * Swagger-generated documentation. + */ + public static final String[] OPEN_PATHS = { // + "/v2/api-docs", // + "/swagger-resources/**", // + "/swagger-ui.html", // + "/webjars/**", // + PortalApiConstants.API_PREFIX + "/**", // + AcXappController.CONTROLLER_PATH + "/" + AcXappController.VERSION_METHOD, // + AdminController.CONTROLLER_PATH + "/" + AdminController.HEALTH_METHOD, // + AdminController.CONTROLLER_PATH + "/" + AdminController.VERSION_METHOD, // + AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.HEALTH_ALIVE_METHOD, // + AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.HEALTH_READY_METHOD, // + AnrXappController.CONTROLLER_PATH + "/" + AnrXappController.VERSION_METHOD, // + AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.HEALTH_ALIVE_METHOD, // + AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.HEALTH_READY_METHOD, // + AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.VERSION_METHOD, // + E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.HEALTH_METHOD, // + E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.VERSION_METHOD, // + DashboardConstants.LOGIN_PAGE, // + SimpleErrorController.ERROR_PATH }; + + @Override + public void configure(WebSecurity web) throws Exception { + // This disables Spring security, but not the app's filter. + web.ignoring().antMatchers(OPEN_PATHS); + } + + @Bean + public PortalAuthManager portalAuthManagerBean() + throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException { + return new PortalAuthManager(appName, userName, password, decryptor, userCookie); + } + + @Bean + public DashboardUserManager dashboardUserManagerBean() throws IOException { + return new DashboardUserManager(userFilePath); + } + + /* + * If this is annotated with @Bean, it is created automatically AND REGISTERED, + * and Spring processes annotations in the source of the class. However, the + * filter is added in the chain apparently in the wrong order. Alternately, with + * no @Bean and added to the chain up in the configure() method in the desired + * order, the ignoring() matcher pattern configured above causes Spring to + * bypass this filter, which seems to me means the filter participates + * correctly. + */ + public PortalAuthenticationFilter portalAuthenticationFilterBean() + throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException { + PortalAuthenticationFilter portalAuthenticationFilter = new PortalAuthenticationFilter(portalAuthManagerBean(), + dashboardUserManagerBean()); + return portalAuthenticationFilter; + } + + /** + * Instantiates the EPSDK-FW servlet. Needed because this app is not configured + * to scan the EPSDK-FW packages; there's also a chance that Spring-Boot does + * not automatically process @WebServlet annotations. + * + * @return Servlet registration bean for the Portal Rest API proxy servlet. + */ + @Bean + public ServletRegistrationBean portalApiProxyServletBean() { + PortalRestAPIProxy servlet = new PortalRestAPIProxy(); + final ServletRegistrationBean servletBean = new ServletRegistrationBean<>(servlet, + PortalApiConstants.API_PREFIX + "/*"); + servletBean.setName("PortalRestApiProxyServlet"); + return servletBean; + } + + /** + * Instantiates a trivial login servlet that serves a basic page with a link to + * authenticate at Portal. The login filter redirects to this page instead of + * Portal. + * + * @return Servlet registration bean for the Dashboard login servlet. + */ + @Bean + public ServletRegistrationBean loginServletBean() { + LoginServlet servlet = new LoginServlet(); + final ServletRegistrationBean servletBean = new ServletRegistrationBean<>(servlet, + DashboardConstants.LOGIN_PAGE); + servletBean.setName("LoginServlet"); + return servletBean; + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java index cdb99b0b..655b47aa 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AcXappController.java @@ -31,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; +import org.springframework.security.access.annotation.Secured; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -77,6 +78,7 @@ public class AcXappController { @ApiOperation(value = "Gets the A1 client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class) @GetMapping(VERSION_METHOD) + // No role required public SuccessTransport getA1MediatorClientVersion() { return new SuccessTransport(200, DashboardApplication.getImplementationVersion(A1MediatorApi.class)); } @@ -86,6 +88,7 @@ public class AcXappController { */ @ApiOperation(value = "Gets the admission control policy for AC xApp via the A1 Mediator") @GetMapping(ADMCTRL_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public Object getAdmissionControlPolicy(HttpServletResponse response) { logger.debug("getAdmissionControlPolicy"); response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); @@ -98,6 +101,7 @@ public class AcXappController { */ @ApiOperation(value = "Sets the admission control policy for AC xApp via the A1 Mediator") @PutMapping(ADMCTRL_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public void setAdmissionControlPolicy(@ApiParam(value = "Admission control policy") @RequestBody JsonNode acPolicy, // HttpServletResponse response) { logger.debug("setAdmissionControlPolicy {}", acPolicy); diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AdminController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AdminController.java index 86a77009..6f282543 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AdminController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AdminController.java @@ -28,6 +28,7 @@ import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; +import org.springframework.security.access.annotation.Secured; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -66,6 +67,7 @@ public class AdminController { @ApiOperation(value = "Gets the Dashboard MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class) @GetMapping(VERSION_METHOD) + // No role required public SuccessTransport getVersion() { logger.debug("getVersion"); return new SuccessTransport(200, @@ -74,6 +76,7 @@ public class AdminController { @ApiOperation(value = "Checks the health of the application.", response = SuccessTransport.class) @GetMapping(HEALTH_METHOD) + // No role required public SuccessTransport getHealth() { logger.debug("getHealth"); return new SuccessTransport(200, "Dashboard is healthy!"); @@ -81,6 +84,7 @@ public class AdminController { @ApiOperation(value = "Gets the list of application users.", response = DashboardUser.class, responseContainer = "List") @GetMapping(USER_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public DashboardUser[] getUsers() { logger.debug("getUsers"); return users; diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java index c6a6b90f..55b42124 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AnrXappController.java @@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; +import org.springframework.security.access.annotation.Secured; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -93,12 +94,14 @@ public class AnrXappController { @ApiOperation(value = "Gets the ANR client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class) @GetMapping(VERSION_METHOD) + // No role required public SuccessTransport getClientVersion() { return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthApi.class)); } @ApiOperation(value = "Performs a liveness probe on the ANR xApp, result expressed as the response code.") @GetMapping(HEALTH_ALIVE_METHOD) + // No role required public void getHealthAlive(HttpServletResponse response) { logger.debug("getHealthAlive"); healthApi.getHealthAlive(); @@ -107,6 +110,7 @@ public class AnrXappController { @ApiOperation(value = "Performs a readiness probe on the ANR xApp, result expressed as the response code.") @GetMapping(HEALTH_READY_METHOD) + // No role required public void getHealthReady(HttpServletResponse response) { logger.debug("getHealthReady"); healthApi.getHealthReady(); @@ -115,6 +119,7 @@ public class AnrXappController { @ApiOperation(value = "Returns list of gNodeB IDs based on NCRT in ANR", response = GgNodeBTable.class) @GetMapping(GNODEBS_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public GgNodeBTable getGnodebs() { logger.debug("getGnodebs"); return ncrtApi.getgNodeB(); @@ -122,6 +127,7 @@ public class AnrXappController { @ApiOperation(value = "Returns neighbor cell relation table for all gNodeBs or based on query parameters", response = NeighborCellRelationTable.class) @GetMapping(NCRT_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public NeighborCellRelationTable getNcrt( // @RequestParam(name = QP_NODEB, required = false) String ggnbId, // @RequestParam(name = QP_SERVING, required = false) String servingCellNrcgi, // @@ -134,6 +140,7 @@ public class AnrXappController { // /ncrt/servingcells/{servCellNrcgi}/neighborcells/{neighCellNrpci} : @ApiOperation(value = "Modify neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI") @PutMapping(NCRT_METHOD + "/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}") + @Secured({ DashboardConstants.ROLE_ADMIN }) public void modifyNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, // @RequestBody NeighborCellRelationMod ncrMod, HttpServletResponse response) { @@ -145,6 +152,7 @@ public class AnrXappController { @ApiOperation(value = "Delete neighbor cell relation based on Serving Cell NRCGI and Neighbor Cell NRPCI") @DeleteMapping(NCRT_METHOD + "/" + PP_SERVING + "/{" + PP_SERVING + "}/" + PP_NEIGHBOR + "/{" + PP_NEIGHBOR + "}") + @Secured({ DashboardConstants.ROLE_ADMIN }) public void deleteNcrt(@PathVariable(PP_SERVING) String servingCellNrcgi, // @PathVariable(PP_NEIGHBOR) String neighborCellNrpci, // HttpServletResponse response) { diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java index 40e03dbf..f71c6bd9 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/AppManagerController.java @@ -42,6 +42,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; +import org.springframework.security.access.annotation.Secured; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -95,12 +96,14 @@ public class AppManagerController { @ApiOperation(value = "Gets the XApp manager client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class) @GetMapping(VERSION_METHOD) + // No role required public SuccessTransport getClientVersion() { return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthApi.class)); } @ApiOperation(value = "Health check of xApp Manager - Liveness probe.") @GetMapping(HEALTH_ALIVE_METHOD) + // No role required public void getHealth(HttpServletResponse response) { logger.debug("getHealthAlive"); healthApi.getHealthAlive(); @@ -109,6 +112,7 @@ public class AppManagerController { @ApiOperation(value = "Readiness check of xApp Manager - Readiness probe.") @GetMapping(HEALTH_READY_METHOD) + // No role required public void getHealthReady(HttpServletResponse response) { logger.debug("getHealthReady"); healthApi.getHealthReady(); @@ -117,6 +121,7 @@ public class AppManagerController { @ApiOperation(value = "Returns the configuration of all xapps.", response = AllXappConfig.class) @GetMapping(CONFIG_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public AllXappConfig getAllXappConfig() { logger.debug("getAllXappConfig"); return xappApi.getAllXappConfig(); @@ -124,6 +129,7 @@ public class AppManagerController { @ApiOperation(value = "Create xApp config.", response = XAppConfig.class) @PostMapping(CONFIG_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public XAppConfig createXappConfig(@RequestBody XAppConfig xAppConfig) { logger.debug("createXappConfig {}", xAppConfig); return xappApi.createXappConfig(xAppConfig); @@ -131,6 +137,7 @@ public class AppManagerController { @ApiOperation(value = "Modify xApp config.", response = XAppConfig.class) @PutMapping(CONFIG_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public XAppConfig modifyXappConfig(@RequestBody XAppConfig xAppConfig) { logger.debug("modifyXappConfig {}", xAppConfig); return xappApi.modifyXappConfig(xAppConfig); @@ -138,6 +145,7 @@ public class AppManagerController { @ApiOperation(value = "Delete xApp configuration.") @DeleteMapping(CONFIG_METHOD + "/{" + PP_XAPP_NAME + "}") + @Secured({ DashboardConstants.ROLE_ADMIN }) public void deleteXappConfig(@RequestBody ConfigMetadata configMetadata, HttpServletResponse response) { logger.debug("deleteXappConfig {}", configMetadata); xappApi.deleteXappConfig(configMetadata); @@ -146,6 +154,7 @@ public class AppManagerController { @ApiOperation(value = "Returns a list of deployable xapps.", response = DashboardDeployableXapps.class) @GetMapping(XAPPS_LIST_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public Object getAvailableXapps() { logger.debug("getAvailableXapps"); AllDeployableXapps appNames = xappApi.listAllXapps(); @@ -160,6 +169,7 @@ public class AppManagerController { @ApiOperation(value = "Returns the status of all deployed xapps.", response = AllDeployedXapps.class) @GetMapping(XAPPS_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public AllDeployedXapps getDeployedXapps() { logger.debug("getDeployedXapps"); return xappApi.getAllXapps(); @@ -167,6 +177,7 @@ public class AppManagerController { @ApiOperation(value = "Returns the status of a given xapp.", response = Xapp.class) @GetMapping(XAPPS_METHOD + "/{" + PP_XAPP_NAME + "}") + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public Xapp getXapp(@PathVariable("xAppName") String xAppName) { logger.debug("getXapp {}", xAppName); return xappApi.getXappByName(xAppName); @@ -174,6 +185,7 @@ public class AppManagerController { @ApiOperation(value = "Deploy a xapp.", response = Xapp.class) @PostMapping(XAPPS_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public Xapp deployXapp(@RequestBody XAppInfo xAppInfo) { logger.debug("deployXapp {}", xAppInfo); return xappApi.deployXapp(xAppInfo); @@ -181,6 +193,7 @@ public class AppManagerController { @ApiOperation(value = "Undeploy an existing xapp.") @DeleteMapping(XAPPS_METHOD + "/{" + PP_XAPP_NAME + "}") + @Secured({ DashboardConstants.ROLE_ADMIN }) public void undeployXapp(@PathVariable("xAppName") String xAppName, HttpServletResponse response) { logger.debug("undeployXapp {}", xAppName); xappApi.undeployXapp(xAppName); diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java index 062d04fe..b1ac2e8f 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/CustomResponseEntityExceptionHandler.java @@ -24,8 +24,10 @@ import java.lang.invoke.MethodHandles; import org.oransc.ric.portal.dashboard.model.ErrorTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.client.HttpStatusCodeException; @@ -55,25 +57,45 @@ public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptio // Superclass has "logger" that is exposed here, so use a different name private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - /** + /* * Generates the response when a REST controller method takes an - * HttpStatusCodeException. Confusingly, the container first redirects to /error - * which invokes the - * {@link org.oransc.ric.portal.dashboard.controller.SimpleErrorController} - * method, and that response arrives here as the response body. + * HttpStatusCodeException. + * + * It appears that the container internally redirects to /error because the web + * request that arrives here has URI /error, and {@link + * org.oransc.ric.portal.dashboard.controller.SimpleErrorController} runs before + * this. + * + * @param ex The exception + * + * @param request The original request * - * @param ex - * The exception - * @param request - * The orignal request * @return A response entity with status code 502 plus some details in the body. */ @ExceptionHandler(HttpStatusCodeException.class) public final ResponseEntity handleHttpStatusCodeException(HttpStatusCodeException ex, WebRequest request) { - log.warn("Request {} failed, status code {}", request.getDescription(false), ex.getStatusCode()); + log.warn("handleHttpStatusCodeException: request {}, status code {}", request.getDescription(false), + ex.getStatusCode()); return new ResponseEntity<>(new ErrorTransport(ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex), HttpStatus.BAD_GATEWAY); } + /* + * This exception also happens when Spring security denies access to a method + * due to missing/wrong roles (granted authorities). Override the method to + * answer permission denied, even though that may obscure a genuine developer + * error. + * + * The web request that arrives here has URI /error; how to obtain the URI of + * the original request?!? + */ + @Override + public final ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + log.warn("handleHttpRequestMethodNotSupported: answering 'permission denied' for method {}", ex.getMethod()); + return new ResponseEntity(new ErrorTransport(HttpStatus.UNAUTHORIZED.value(), + "Permission denied for method " + ex.getMethod(), ex), HttpStatus.UNAUTHORIZED); + } + } diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java index e2523982..b200c9a8 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/controller/E2ManagerController.java @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; +import org.springframework.security.access.annotation.Secured; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -91,12 +92,14 @@ public class E2ManagerController { @ApiOperation(value = "Gets the E2 manager client library MANIFEST.MF property Implementation-Version.", response = SuccessTransport.class) @GetMapping(VERSION_METHOD) + // No role required public SuccessTransport getClientVersion() { return new SuccessTransport(200, DashboardApplication.getImplementationVersion(HealthCheckApi.class)); } @ApiOperation(value = "Gets the health from the E2 manager, expressed as the response code.") @GetMapping(HEALTH_METHOD) + // No role required public void healthGet(HttpServletResponse response) { logger.debug("healthGet"); e2HealthCheckApi.healthGet(); @@ -106,6 +109,7 @@ public class E2ManagerController { // This calls other methods to simplify the task of the front-end. @ApiOperation(value = "Gets all RAN identities and statuses from the E2 manager.", response = RanDetailsTransport.class, responseContainer = "List") @GetMapping(RAN_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public List getRanDetails() { logger.debug("getRanDetails"); List nodebIdList = e2NodebApi.getNodebIdList(); @@ -127,6 +131,7 @@ public class E2ManagerController { @ApiOperation(value = "Get RAN identities list.", response = NodebIdentity.class, responseContainer = "List") @GetMapping(NODEB_LIST_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public List getNodebIdList() { logger.debug("getNodebIdList"); return e2NodebApi.getNodebIdList(); @@ -134,6 +139,7 @@ public class E2ManagerController { @ApiOperation(value = "Get RAN by name.", response = GetNodebResponse.class) @GetMapping(NODEB_METHOD + "/{" + PP_RANNAME + "}") + @Secured({ DashboardConstants.ROLE_ADMIN, DashboardConstants.ROLE_STANDARD }) public GetNodebResponse getNb(@PathVariable(PP_RANNAME) String ranName) { logger.debug("getNb {}", ranName); return e2NodebApi.getNb(ranName); @@ -141,6 +147,7 @@ public class E2ManagerController { @ApiOperation(value = "Close all connections to the RANs and delete the data from the nodeb-rnib DB.") @DeleteMapping(NODEB_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public void nodebDelete(HttpServletResponse response) { logger.debug("nodebDelete"); e2NodebApi.nodebDelete(); @@ -149,6 +156,7 @@ public class E2ManagerController { @ApiOperation(value = "Sets up an EN-DC RAN connection via the E2 manager.") @PostMapping(ENDC_SETUP_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public void endcSetup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { logger.debug("endcSetup {}", setupRequest); e2NodebApi.endcSetup(setupRequest); @@ -157,6 +165,7 @@ public class E2ManagerController { @ApiOperation(value = "Sets up an X2 RAN connection via the E2 manager.") @PostMapping(X2_SETUP_METHOD) + @Secured({ DashboardConstants.ROLE_ADMIN }) public void x2Setup(@RequestBody SetupRequest setupRequest, HttpServletResponse response) { logger.debug("x2Setup {}", setupRequest); e2NodebApi.x2Setup(setupRequest); diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/EcompUserDetails.java new file mode 100644 index 00000000..80919838 --- /dev/null +++ b/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 and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +import org.onap.portalsdk.core.restful.domain.EcompRole; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +public class EcompUserDetails implements UserDetails { + + private static final long serialVersionUID = 1L; + private final EcompUser ecompUser; + + // This is the default Spring role-name prefix. + private static final String ROLEP = "ROLE_"; + + public EcompUserDetails(EcompUser ecompUser) { + this.ecompUser = ecompUser; + } + + /* + * Gets a list of authorities (roles) for this user. To keep Spring happy, every + * item has prefix ROLE_. + */ + public Collection getAuthorities() { + List roleList = new ArrayList<>(); + Iterator 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/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java index 035b9efb..2d3a5c23 100644 --- a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/model/ErrorTransport.java @@ -26,7 +26,7 @@ package org.oransc.ric.portal.dashboard.model; public class ErrorTransport implements IDashboardResponse { private Integer status; - private String error; + private String message; private String exception; /** @@ -62,7 +62,7 @@ public class ErrorTransport implements IDashboardResponse { */ public ErrorTransport(int statusCode, String errMsg, Exception exception) { this.status = statusCode; - this.error = errMsg; + this.message = errMsg; if (exception != null) { final int enough = 512; String exString = exception.toString(); @@ -79,12 +79,12 @@ public class ErrorTransport implements IDashboardResponse { this.status = status; } - public String getError() { - return error; + public String getMessage() { + return message; } - public void setError(String error) { - this.error = error; + public void setMessage(String error) { + this.message = error; } public String getException() { diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java new file mode 100644 index 00000000..b02d0261 --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/DashboardUserManager.java @@ -0,0 +1,122 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; + +import org.onap.portalsdk.core.onboarding.exception.PortalAPIException; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Provides user-management services. + * + * This first implementation serializes user details to a file. TODO: migrate to + * a database. + */ +public class DashboardUserManager { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final File userFile; + private final List users; + + public DashboardUserManager(final String userFilePath) throws IOException { + logger.debug("ctor: userfile {}", userFilePath); + if (userFilePath == null) + throw new IllegalArgumentException("Missing or empty user file property"); + userFile = new File(userFilePath); + logger.debug("ctor: managing users in file {}", userFile.getAbsolutePath()); + if (userFile.exists()) { + final ObjectMapper mapper = new ObjectMapper(); + users = mapper.readValue(userFile, new TypeReference>() { + }); + } else { + users = new ArrayList<>(); + } + } + + /** + * Gets the user with the specified login Id + * + * @param loginId + * Desired login Id + * @return User object; null if Id is not known + */ + public EcompUser getUser(String loginId) { + for (EcompUser u : this.users) { + if (u.getLoginId().equals(loginId)) { + logger.debug("getUser: match on {}", loginId); + return u; + } + } + logger.debug("getUser: no match on {}", loginId); + return null; + } + + private void saveUsers() throws JsonGenerationException, JsonMappingException, IOException { + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(userFile, users); + } + + /* + * Allow at most one thread to create a user at one time. + */ + public synchronized void createUser(EcompUser user) throws PortalAPIException { + logger.debug("createUser: loginId is " + user.getLoginId()); + if (users.contains(user)) + throw new PortalAPIException("User exists: " + user.getLoginId()); + users.add(user); + try { + saveUsers(); + } catch (Exception ex) { + throw new PortalAPIException("Save failed", ex); + } + } + + /* + * Allow at most one thread to modify a user at one time. We still have + * last-edit-wins of course. + */ + public synchronized void updateUser(String loginId, EcompUser user) throws PortalAPIException { + logger.debug("editUser: loginId is " + loginId); + int index = users.indexOf(user); + if (index < 0) + throw new PortalAPIException("User does not exist: " + user.getLoginId()); + users.remove(index); + users.add(user); + try { + saveUsers(); + } catch (Exception ex) { + throw new PortalAPIException("Save failed", ex); + } + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/IPortalSdkDecryptor.java new file mode 100644 index 00000000..9862e163 --- /dev/null +++ b/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 and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import org.onap.portalsdk.core.onboarding.exception.CipherUtilException; + +/** + * Supports an upgrade path among methods in CipherUtil because the PortalSDK is + * changing encryption methods. + */ +public interface IPortalSdkDecryptor { + + /** + * Decrypts the specified value using a known key. + * + * @param cipherText + * Encrypted value + * @return Clear text on success, null otherwise. + * @throws CipherUtilException + * if any decryption step fails + */ + String decrypt(String cipherText) throws CipherUtilException; + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java new file mode 100644 index 00000000..e4714473 --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthManager.java @@ -0,0 +1,118 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; + +import org.onap.portalsdk.core.onboarding.exception.CipherUtilException; +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides services to authenticate requests from/to ONAP Portal. + */ +public class PortalAuthManager { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + final Map credentialsMap; + private final IPortalSdkDecryptor portalSdkDecryptor; + private final String userIdCookieName; + + public PortalAuthManager(final String appName, final String username, final String password, + final String decryptorClassName, final String userCookie) + throws ClassNotFoundException, InstantiationException, IllegalAccessException { + credentialsMap = new HashMap<>(); + // The map keys are hardcoded in EPSDK-FW, no constants are defined :( + credentialsMap.put("appName", appName); + credentialsMap.put("username", username); + credentialsMap.put("password", password); + this.userIdCookieName = userCookie; + // Instantiate here so configuration errors are detected at app-start time + logger.debug("ctor: using decryptor class {}", decryptorClassName); + Class decryptorClass = Class.forName(decryptorClassName); + portalSdkDecryptor = (IPortalSdkDecryptor) decryptorClass.newInstance(); + } + + /** + * @return A map of key-value pairs with application name, user name and + * password. + */ + public Map getAppCredentials() { + return credentialsMap; + } + + /** + * Searches the request for a cookie with the specified name. + * + * @param request + * HttpServletRequest + * @param cookieName + * Cookie name + * @return Cookie, or null if not found. + */ + private Cookie getCookie(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) + for (Cookie cookie : cookies) + if (cookie.getName().equals(cookieName)) + return cookie; + return null; + } + + /** + * Validates whether the ECOMP Portal sign-on process has completed. Checks for + * the ECOMP cookie first, then the user cookie. + * + * @param request + * HttpServletRequest + * @return User ID if the ECOMP cookie is present and the sign-on process + * established a user ID; else null. + */ + public String valdiateEcompSso(HttpServletRequest request) { + // Check ECOMP Portal cookie + Cookie ep = getCookie(request, PortalApiConstants.EP_SERVICE); + if (ep == null) { + logger.debug("valdiateEcompSso: cookie not found: {}", PortalApiConstants.EP_SERVICE); + return null; + } + logger.trace("validateEcompSso: found cookie {}", PortalApiConstants.EP_SERVICE); + Cookie user = getCookie(request, userIdCookieName); + if (user == null) { + logger.debug("valdiateEcompSso: cookie not found: {}", userIdCookieName); + return null; + } + logger.trace("validateEcompSso: user cookie {}", userIdCookieName); + String userid = null; + try { + userid = portalSdkDecryptor.decrypt(user.getValue()); + } catch (CipherUtilException e) { + throw new IllegalArgumentException("valdiateEcompSso failed", e); + } + return userid; + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java new file mode 100644 index 00000000..2ec5938d --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalAuthenticationFilter.java @@ -0,0 +1,177 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.invoke.MethodHandles; +import java.net.URLEncoder; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.onap.portalsdk.core.onboarding.util.PortalApiProperties; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.oransc.ric.portal.dashboard.DashboardConstants; +import org.oransc.ric.portal.dashboard.model.EcompUserDetails; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +/** + * This filter checks every request for the cookie set by the ONAP Portal single + * sign on process. The possible paths and actions: + *
    + *
  1. User starts at an app page via a bookmark. No Portal cookie is set. + * Redirect there to get one; then continue as below. + *
  2. 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. + *
  3. User has valid Portal cookie and session. Reset the max idle in that + * session. + *
+ *

+ * Notes: + *

    + *
  • 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. + *
+ * + * TODO: What about sessions? Will this be stateless? + * + * This filter uses no annotations to avoid Spring's automatic registration, + * which add this filter in the chain in the wrong order. + */ +public class PortalAuthenticationFilter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String REDIRECT_URL_KEY = "redirectUrl"; + + private final PortalAuthManager authManager; + + private final DashboardUserManager userManager; + + public PortalAuthenticationFilter(PortalAuthManager authManager, DashboardUserManager userManager) { + this.authManager = authManager; + this.userManager = userManager; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // complain loudly if this key property is missing + String url = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL); + logger.debug("init: Portal redirect URL {}", url); + if (url == null) + logger.error( + "init: Failed to find property in portal.properties: " + PortalApiConstants.ECOMP_REDIRECT_URL); + } + + @Override + public void destroy() { + // No resources to release + } + + /** + * Checks for valid cookies and allows request to be served if found; redirects + * to Portal otherwise. Requests for pages ignored in the web security config do + * not hit this filter. + */ + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + logger.debug("doFilter {}", req); + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + // Need to authenticate the request + final String userId = authManager.valdiateEcompSso(request); + final EcompUser ecompUser = (userId == null ? null : userManager.getUser(userId)); + if (userId == null || ecompUser == null) { + String redirectURL = buildLoginPageUrl(request); + logger.trace("doFilter: unauthorized, redirecting to {}", redirectURL); + response.sendRedirect(redirectURL); + } else { + EcompUserDetails userDetails = new EcompUserDetails(ecompUser); + // Using portal session as credentials is a hack + PreAuthenticatedAuthenticationToken authToken = new PreAuthenticatedAuthenticationToken(userDetails, + getPortalSessionId(request), userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authToken); + // Pass request back down the filter chain + chain.doFilter(request, response); + } + } + + private String buildLoginPageUrl(HttpServletRequest request) { + logger.trace("buildLoginPageUrl"); + // Why so much work to recover the original request? + final StringBuffer sb = request.getRequestURL(); + sb.append(request.getQueryString() == null ? "" : "?" + request.getQueryString()); + final String requestedUrl = sb.toString(); + String encodedUrl = null; + try { + encodedUrl = URLEncoder.encode(requestedUrl, "UTF-8"); + } catch (UnsupportedEncodingException ex) { + logger.error("buildLoginPageUrl: Failed to encode {}", requestedUrl); + } + return DashboardConstants.LOGIN_PAGE + "?" + REDIRECT_URL_KEY + "=" + encodedUrl; + } + + /** + * Searches the request for a cookie with the specified name. + * + * @param request + * HttpServletRequest + * @param cookieName + * Cookie name + * @return Cookie, or null if not found. + */ + private Cookie getCookie(HttpServletRequest request, String cookieName) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) + for (Cookie cookie : cookies) + if (cookie.getName().equals(cookieName)) + return cookie; + return null; + } + + /** + * Gets the ECOMP Portal service cookie value. + * + * @param request + * @return Cookie value, or null if not found. + */ + private String getPortalSessionId(HttpServletRequest request) { + Cookie ep = getCookie(request, PortalApiConstants.EP_SERVICE); + if (ep == null) + return null; + return ep.getValue(); + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java new file mode 100644 index 00000000..f5d37596 --- /dev/null +++ b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalRestCentralServiceImpl.java @@ -0,0 +1,88 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.onap.portalsdk.core.onboarding.crossapi.IPortalRestCentralService; +import org.onap.portalsdk.core.onboarding.exception.PortalAPIException; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.oransc.ric.portal.dashboard.config.SpringContextCache; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; + +/** + * Implements the contract used by the Portal to transmit user details to this + * on-boarded application. The requests are intercepted first by a servlet in + * the EPSDK-FW library, which proxies the calls to these methods. + * + * An instance of this class is created upon first request to the API. But this + * class is found and instantiated via Class.forName(), so cannot use Spring + * annotations. + */ +public class PortalRestCentralServiceImpl implements IPortalRestCentralService { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final PortalAuthManager authManager; + private final DashboardUserManager userManager; + + public PortalRestCentralServiceImpl() throws IOException, PortalAPIException { + final ApplicationContext context = SpringContextCache.getApplicationContext(); + authManager = (PortalAuthManager) context.getBean(PortalAuthManager.class); + userManager = (DashboardUserManager) context.getBean(DashboardUserManager.class); + } + + /* + * Answers the Portal API credentials. + */ + @Override + public Map getAppCredentials() throws PortalAPIException { + logger.debug("getAppCredentials"); + return authManager.getAppCredentials(); + } + + /* + * Extracts the user ID from a cookie in the header + */ + @Override + public String getUserId(HttpServletRequest request) throws PortalAPIException { + logger.debug("getuserId"); + return authManager.valdiateEcompSso(request); + } + + @Override + public void pushUser(EcompUser user) throws PortalAPIException { + logger.debug("pushUser: {}", user); + userManager.createUser(user); + } + + @Override + public void editUser(String loginId, EcompUser user) throws PortalAPIException { + logger.debug("editUser: {}", user); + userManager.updateUser(loginId, user); + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorAes.java new file mode 100644 index 00000000..3019f527 --- /dev/null +++ b/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 and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import org.onap.portalsdk.core.onboarding.exception.CipherUtilException; +import org.onap.portalsdk.core.onboarding.util.CipherUtil; + +public class PortalSdkDecryptorAes implements IPortalSdkDecryptor { + + @SuppressWarnings("deprecation") + public String decrypt(String cipherText) throws CipherUtilException { + return CipherUtil.decrypt(cipherText); + } + +} diff --git a/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java b/webapp-backend/src/main/java/org/oransc/ric/portal/dashboard/portalapi/PortalSdkDecryptorPkc.java new file mode 100644 index 00000000..04d44cd1 --- /dev/null +++ b/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 and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.portalapi; + +import org.onap.portalsdk.core.onboarding.exception.CipherUtilException; +import org.onap.portalsdk.core.onboarding.util.CipherUtil; + +public class PortalSdkDecryptorPkc implements IPortalSdkDecryptor { + + public String decrypt(String cipherText) throws CipherUtilException { + return CipherUtil.decryptPKC(cipherText); + } + +} diff --git a/webapp-backend/src/main/resources/ESAPI.properties b/webapp-backend/src/main/resources/ESAPI.properties new file mode 100644 index 00000000..f27b1acf --- /dev/null +++ b/webapp-backend/src/main/resources/ESAPI.properties @@ -0,0 +1,385 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +#=========================================================================== +# ESAPI Configuration +# +# If true, then print all the ESAPI properties set here when they are loaded. +# If false, they are not printed. Useful to reduce output when running JUnit tests. +# If you need to troubleshoot a properties related problem, turning this on may help. +# This is 'false' in the src/test/resources/.esapi version. It is 'true' by +# default for reasons of backward compatibility with earlier ESAPI versions. +ESAPI.printProperties=false + +# ESAPI is designed to be easily extensible. You can use the reference implementation +# or implement your own providers to take advantage of your enterprise's security +# infrastructure. The functions in ESAPI are referenced using the ESAPI locator, like: +# +# String ciphertext = +# ESAPI.encryptor().encrypt("Secret message"); // Deprecated in 2.0 +# CipherText cipherText = +# ESAPI.encryptor().encrypt(new PlainText("Secret message")); // Preferred +# +# Below you can specify the classname for the provider that you wish to use in your +# application. The only requirement is that it implement the appropriate ESAPI interface. +# This allows you to switch security implementations in the future without rewriting the +# entire application. +# +# ExperimentalAccessController requires ESAPI-AccessControlPolicy.xml in .esapi directory +ESAPI.AccessControl=org.owasp.esapi.reference.DefaultAccessController +# FileBasedAuthenticator requires users.txt file in .esapi directory +ESAPI.Authenticator=org.owasp.esapi.reference.FileBasedAuthenticator +ESAPI.Encoder=org.owasp.esapi.reference.DefaultEncoder +ESAPI.Encryptor=org.owasp.esapi.reference.crypto.JavaEncryptor + +ESAPI.Executor=org.owasp.esapi.reference.DefaultExecutor +ESAPI.HTTPUtilities=org.owasp.esapi.reference.DefaultHTTPUtilities +ESAPI.IntrusionDetector=org.owasp.esapi.reference.DefaultIntrusionDetector +#ESAPI.Logger=org.owasp.esapi.reference.JavaLogFactory +ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer +ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator + +#=========================================================================== +# ESAPI Authenticator +# +Authenticator.AllowedLoginAttempts=3 +#Authenticator.MaxOldPasswordHashes=13 +Authenticator.UsernameParameterName=username +#Authenticator.PasswordParameterName=password +# RememberTokenDuration (in days) +Authenticator.RememberTokenDuration=14 +# Session Timeouts (in minutes) +Authenticator.IdleTimeoutDuration=20 +Authenticator.AbsoluteTimeoutDuration=120 + +#=========================================================================== +# ESAPI Encoder +# +# ESAPI canonicalizes input before validation to prevent bypassing filters with encoded attacks. +# Failure to canonicalize input is a very common mistake when implementing validation schemes. +# Canonicalization is automatic when using the ESAPI Validator, but you can also use the +# following code to canonicalize data. +# +# ESAPI.Encoder().canonicalize( "%22hello world"" ); +# +# 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/webapp-backend/src/main/resources/application.properties b/webapp-backend/src/main/resources/application.properties index 382cd191..1b7bd1c6 100644 --- a/webapp-backend/src/main/resources/application.properties +++ b/webapp-backend/src/main/resources/application.properties @@ -1,4 +1,3 @@ -### # ========================LICENSE_START================================= # O-RAN-SC # %% @@ -16,26 +15,42 @@ # See the License for the specific language governing permissions and # limitations under the License. # ========================LICENSE_END=================================== -### -# Defines property keys for the RIC Dashboard, and some defaults +# Defines RIC Dashboard property keys and default values. +# Create a copy in the launch directory to override values; or +# copy to "application-abc.properties" as mentioned in the README. -# Confusingly, this key has no "spring" prefix -# The port number is chosen RANDOMLY when running a test +# A spring property but without a "spring" prefix; +# the port number is chosen RANDOMLY when running tests server.port = 8080 +# path to file that stores user details; +# use a persistent volume in a K8S deployment +userfile = users.json + +# class that decrypts ciphertext from Portal +portalapi.decryptor = org.oransc.ric.portal.dashboard.portalapi.PortalSdkDecryptorAes +# name of request cookie with user ID +portalapi.usercookie = UserId + +# portal credentials must be supplied at deployment time +portalapi.appname = RIC Dashboard +portalapi.username = +portalapi.password = + +# endpoint URLs must be supplied at deployment time # A1 Mediator a1med.url.prefix = http://jar-app-props-default-A1-URL-prefix a1med.url.suffix = - # ANR xApp anrxapp.url.prefix = http://jar-app-props-default-ANR-URL-prefix anrxapp.url.suffix = - # App Manager appmgr.url.prefix = http://jar-app-props-default-Xapp-Mgr-URL appmgr.url.suffix = /ric/v1 - # E2 Manager e2mgr.url.prefix = http://jar-app-props-default-E2-URL e2mgr.url.suffix = /v1 + +# Mimic slow endpoints by defining sleep period, in milliseconds +mock.config.delay = 0 diff --git a/webapp-backend/src/main/resources/logback.xml b/webapp-backend/src/main/resources/logback.xml index 26c9ffb2..b17cbe2c 100644 --- a/webapp-backend/src/main/resources/logback.xml +++ b/webapp-backend/src/main/resources/logback.xml @@ -19,16 +19,15 @@ ========================LICENSE_END=================================== --> - - + - + @@ -44,11 +43,11 @@ ${logDirectory}/${componentName}.%i.log.zip 1 - 5 + 9 - 1MB + 10MB @@ -65,7 +64,10 @@ > - + + + + diff --git a/webapp-backend/src/main/resources/validation.properties b/webapp-backend/src/main/resources/validation.properties new file mode 100644 index 00000000..0785d066 --- /dev/null +++ b/webapp-backend/src/main/resources/validation.properties @@ -0,0 +1,19 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +# empty file to suppress OWASP complaints emitted to stdout diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/A1MediatorMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1MediatorMockConfiguration.java similarity index 85% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/A1MediatorMockConfiguration.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1MediatorMockConfiguration.java index a4fb63fd..aef5bc4c 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/A1MediatorMockConfiguration.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/A1MediatorMockConfiguration.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.config; +package org.oransc.ric.portal.dashboard.config; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -30,6 +30,7 @@ import org.oransc.ric.a1med.client.api.A1MediatorApi; import org.oransc.ric.a1med.client.invoker.ApiClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -43,8 +44,10 @@ import org.springframework.http.HttpStatus; public class A1MediatorMockConfiguration { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // Simulate remote method delay for UI testing - private final int delayMs = 500; + @Value("${mock.config.delay:0}") + private int delayMs; public A1MediatorMockConfiguration() { logger.info("Configuring mock A1 Mediator"); @@ -63,13 +66,17 @@ public class A1MediatorMockConfiguration { A1MediatorApi mockApi = mock(A1MediatorApi.class); when(mockApi.getApiClient()).thenReturn(apiClient); doAnswer(inv -> { - logger.debug("a1ControllerGetHandler sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("a1ControllerGetHandler sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).a1ControllerGetHandler(any(String.class)); doAnswer(inv -> { - logger.debug("a1ControllerPutHandler sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("a1ControllerPutHandler sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).a1ControllerPutHandler(any(String.class), any(Object.class)); return mockApi; diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AnrXappMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AnrXappMockConfiguration.java similarity index 85% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AnrXappMockConfiguration.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AnrXappMockConfiguration.java index d291945b..bc189693 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AnrXappMockConfiguration.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AnrXappMockConfiguration.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.config; +package org.oransc.ric.portal.dashboard.config; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -37,6 +37,7 @@ import org.oransc.ric.anrxapp.client.model.NeighborCellRelationMod; import org.oransc.ric.anrxapp.client.model.NeighborCellRelationTable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -51,17 +52,20 @@ public class AnrXappMockConfiguration { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // Simulate remote method delay for UI testing + @Value("${mock.config.delay:0}") + private int delayMs; + private static final String GNODEB1 = "001EF5:0045FE50"; private static final String GNODEB2 = "001EF6:0045FE51"; private static final String GNODEB3 = "001EF7:0045FE52"; + // Sonar wants separate declarations private final NeighborCellRelationTable ncrt; private final NeighborCellRelationTable ncrtNodeB1; private final NeighborCellRelationTable ncrtNodeB2; private final NeighborCellRelationTable ncrtNodeB3; private final GgNodeBTable gNodebTable; - // Simulate remote method delay for UI testing - private final int delayMs = 500; public AnrXappMockConfiguration() { logger.info("Configuring mock ANR xApp client"); @@ -118,44 +122,60 @@ public class AnrXappMockConfiguration { NcrtApi mockApi = mock(NcrtApi.class); when(mockApi.getApiClient()).thenReturn(apiClient); doAnswer(inv -> { - logger.debug("getgNodeB sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getgNodeB sleeping {}", delayMs); + Thread.sleep(delayMs); + } return gNodebTable; }).when(mockApi).getgNodeB(); // Swagger sends nulls; front end sends empty strings doAnswer(inv -> { - logger.debug("getNcrt (1) sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNcrt (1) sleeping {}", delayMs); + Thread.sleep(delayMs); + } return ncrt; }).when(mockApi).getNcrt((String) isNull(), (String) isNull(), (String) isNull()); doAnswer(inv -> { - logger.debug("getNcrt (2) sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNcrt (2) sleeping {}", delayMs); + Thread.sleep(delayMs); + } return ncrt; }).when(mockApi).getNcrt(eq(""), any(String.class), any(String.class)); doAnswer(inv -> { - logger.debug("getNcrt (3) sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNcrt (3) sleeping {}", delayMs); + Thread.sleep(delayMs); + } return ncrtNodeB1; }).when(mockApi).getNcrt(eq(GNODEB1), any(String.class), any(String.class)); doAnswer(inv -> { - logger.debug("getNcrt (4) sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNcrt (4) sleeping {}", delayMs); + Thread.sleep(delayMs); + } return ncrtNodeB2; }).when(mockApi).getNcrt(eq(GNODEB2), any(String.class), any(String.class)); doAnswer(inv -> { - logger.debug("getNcrt (5) sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNcrt (5) sleeping {}", delayMs); + Thread.sleep(delayMs); + } return ncrtNodeB3; }).when(mockApi).getNcrt(eq(GNODEB3), any(String.class), any(String.class)); doAnswer(inv -> { - logger.debug("deleteNcrt sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("deleteNcrt sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).deleteNcrt(any(String.class), any(String.class)); doAnswer(inv -> { - logger.debug("modifyNcrt sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("modifyNcrt sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).modifyNcrt(any(String.class), any(String.class), any(NeighborCellRelationMod.class)); return mockApi; diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AppManagerMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AppManagerMockConfiguration.java similarity index 79% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AppManagerMockConfiguration.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AppManagerMockConfiguration.java index 2df2b3d6..f4cd4955 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/AppManagerMockConfiguration.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/AppManagerMockConfiguration.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.config; +package org.oransc.ric.portal.dashboard.config; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -42,6 +42,7 @@ import org.oransc.ric.plt.appmgr.client.model.Xapp.StatusEnum; import org.oransc.ric.plt.appmgr.client.model.XappInstance; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -57,12 +58,14 @@ public class AppManagerMockConfiguration { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // Simulate remote method delay for UI testing + @Value("${mock.config.delay:0}") + private int delayMs; + private final AllDeployableXapps availXapps; private final AllDeployedXapps deployedXapps; private final AllXappConfig allXappConfigs; private final SubscriptionResponse subRes; - // Simulate remote method delay for UI testing - private final int delayMs = 500; public AppManagerMockConfiguration() { logger.info("Configuring mock xApp Manager"); @@ -105,58 +108,80 @@ public class AppManagerMockConfiguration { XappApi mockApi = mock(XappApi.class); when(mockApi.getApiClient()).thenReturn(mockClient); doAnswer(inv -> { - logger.debug("getAllXappConfig sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getAllXappConfig sleeping {}", delayMs); + Thread.sleep(delayMs); + } return allXappConfigs; }).when(mockApi).getAllXappConfig(); doAnswer(inv -> { - logger.debug("createXappConfig sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("createXappConfig sleeping {}", delayMs); + Thread.sleep(delayMs); + } return allXappConfigs.get(0); }).when(mockApi).createXappConfig(any(XAppConfig.class)); doAnswer(inv -> { - logger.debug("modifyXappConfig sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("modifyXappConfig sleeping {}", delayMs); + Thread.sleep(delayMs); + } return allXappConfigs.get(0); }).when(mockApi).modifyXappConfig(any(XAppConfig.class)); doAnswer(inv -> { - logger.debug("deleteXappConfig sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("deleteXappConfig sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).deleteXappConfig(any(ConfigMetadata.class)); doAnswer(inv -> { - logger.debug("deployXapp of {} sleeping {}", inv.getArgument(0), delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("deployXapp of {} sleeping {}", inv.getArgument(0), delayMs); + Thread.sleep(delayMs); + } return deployedXapps.get(0); }).when(mockApi).deployXapp(any(XAppInfo.class)); doAnswer(inv -> { - logger.debug("listAllXapps sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("listAllXapps sleeping {}", delayMs); + Thread.sleep(delayMs); + } return availXapps; }).when(mockApi).listAllXapps(); doAnswer(inv -> { - logger.debug("getAllXapps sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getAllXapps sleeping {}", delayMs); + Thread.sleep(delayMs); + } return deployedXapps; }).when(mockApi).getAllXapps(); doAnswer(inv -> { - logger.debug("getXappByName of {} sleeping {}", inv.getArgument(0), delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getXappByName of {} sleeping {}", inv.getArgument(0), delayMs); + Thread.sleep(delayMs); + } return deployedXapps.get(0); }).when(mockApi).getXappByName(any(String.class)); doAnswer(inv -> { - logger.debug("undeployXapp of {} sleeping {}", inv.getArgument(0), delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("undeployXapp of {} sleeping {}", inv.getArgument(0), delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).undeployXapp(any(String.class)); doAnswer(inv -> { - logger.debug("addSubscription sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("addSubscription sleeping {}", delayMs); + Thread.sleep(delayMs); + } return subRes; }).when(mockApi).addSubscription(any(SubscriptionRequest.class)); doAnswer(inv -> { - logger.debug("deleteSubscription sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("deleteSubscription sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).deleteSubscription(any(String.class)); return mockApi; diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/E2ManagerMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/E2ManagerMockConfiguration.java similarity index 84% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/E2ManagerMockConfiguration.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/E2ManagerMockConfiguration.java index 9454bfb6..d3385267 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/config/E2ManagerMockConfiguration.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/E2ManagerMockConfiguration.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.config; +package org.oransc.ric.portal.dashboard.config; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -37,6 +37,7 @@ import org.oransc.ric.e2mgr.client.model.NodebIdentityGlobalNbId; import org.oransc.ric.e2mgr.client.model.SetupRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -51,10 +52,12 @@ public class E2ManagerMockConfiguration { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + // Simulate remote method delay for UI testing + @Value("${mock.config.delay:0}") + private int delayMs; + private final List nodebIdList; private final GetNodebResponse nodebResponse; - // Simulate remote method delay for UI testing - private final int delayMs = 500; public E2ManagerMockConfiguration() { logger.info("Configuring mock E2 Manager"); @@ -89,28 +92,38 @@ public class E2ManagerMockConfiguration { NodebApi mockApi = mock(NodebApi.class); when(mockApi.getApiClient()).thenReturn(apiClient); doAnswer(inv -> { - logger.debug("nodebDelete sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("nodebDelete sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).nodebDelete(); doAnswer(inv -> { - logger.debug("getNb sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNb sleeping {}", delayMs); + Thread.sleep(delayMs); + } return nodebResponse; }).when(mockApi).getNb(any(String.class)); doAnswer(inv -> { - logger.debug("getNodebIdList sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("getNodebIdList sleeping {}", delayMs); + Thread.sleep(delayMs); + } return nodebIdList; }).when(mockApi).getNodebIdList(); doAnswer(inv -> { - logger.debug("endcSetup sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("endcSetup sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).endcSetup(any(SetupRequest.class)); doAnswer(inv -> { - logger.debug("x2Setup sleeping {}", delayMs); - Thread.sleep(delayMs); + if (delayMs > 0) { + logger.debug("x2Setup sleeping {}", delayMs); + Thread.sleep(delayMs); + } return null; }).when(mockApi).x2Setup(any(SetupRequest.class)); return mockApi; diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java new file mode 100644 index 00000000..c17baefd --- /dev/null +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/config/WebSecurityMockConfiguration.java @@ -0,0 +1,171 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.config; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import java.io.File; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.onap.portalsdk.core.onboarding.crossapi.PortalRestAPIProxy; +import org.onap.portalsdk.core.onboarding.exception.PortalAPIException; +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.onap.portalsdk.core.restful.domain.EcompRole; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.oransc.ric.portal.dashboard.DashboardConstants; +import org.oransc.ric.portal.dashboard.LoginServlet; +import org.oransc.ric.portal.dashboard.portalapi.DashboardUserManager; +import org.oransc.ric.portal.dashboard.portalapi.PortalAuthManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +@Profile("test") +public class WebSecurityMockConfiguration extends WebSecurityConfigurerAdapter { + + public static final String TEST_CRED_ADMIN = "admin"; + public static final String TEST_CRED_STANDARD = "standard"; + + // Unfortunately EPSDK-FW does not define these as constants + public static final String PORTAL_USERNAME_HEADER_KEY = "username"; + public static final String PORTAL_PASSWORD_HEADER_KEY = "password"; + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public WebSecurityMockConfiguration(@Value("${userfile}") final String userFilePath) { + logger.debug("ctor: user file path {}", userFilePath); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + auth.inMemoryAuthentication() // + .passwordEncoder(encoder) // + // The admin user has the admin AND standard roles + .withUser(TEST_CRED_ADMIN) // + .password(encoder.encode(TEST_CRED_ADMIN)) + .roles(DashboardConstants.ROLE_NAME_ADMIN, DashboardConstants.ROLE_NAME_STANDARD)// + .and()// + // The standard user has only the standard role + .withUser(TEST_CRED_STANDARD) // + .password(encoder.encode(TEST_CRED_STANDARD)) // + .roles(DashboardConstants.ROLE_NAME_STANDARD); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests().anyRequest().authenticated()// + .and().httpBasic() // + .and().csrf().disable(); + } + + @Override + public void configure(WebSecurity web) throws Exception { + // This disables Spring security, but not the app's filter. + web.ignoring().antMatchers(WebSecurityConfiguration.OPEN_PATHS); + } + + @Bean + public ServletRegistrationBean loginServlet() { + LoginServlet servlet = new LoginServlet(); + final ServletRegistrationBean servletBean = new ServletRegistrationBean<>(servlet, + DashboardConstants.LOGIN_PAGE); + servletBean.setName("LoginServlet"); + return servletBean; + } + + @Bean + public ServletRegistrationBean portalApiProxyServlet() { + PortalRestAPIProxy servlet = new PortalRestAPIProxy(); + final ServletRegistrationBean 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 credentialsMap = new HashMap<>(); + credentialsMap.put("appName", "appName"); + credentialsMap.put(PORTAL_USERNAME_HEADER_KEY, PORTAL_USERNAME_HEADER_KEY); + credentialsMap.put(PORTAL_PASSWORD_HEADER_KEY, PORTAL_PASSWORD_HEADER_KEY); + doAnswer(inv -> { + logger.debug("getAppCredentials"); + return credentialsMap; + }).when(mockManager).getAppCredentials(); + doAnswer(inv -> { + logger.debug("getUserId"); + return "userId"; + }).when(mockManager).valdiateEcompSso(any(HttpServletRequest.class)); + doAnswer(inv -> { + logger.debug("getAppCredentials"); + return credentialsMap; + }).when(mockManager).getAppCredentials(); + return mockManager; + } + + // This implementation is so light it can be used during tests. + @Bean + public DashboardUserManager dashboardUserManager() throws IOException, PortalAPIException { + File f = new File("/tmp/users.json"); + if (f.exists()) + f.delete(); + DashboardUserManager um = new DashboardUserManager(f.getAbsolutePath()); + // Mock user for convenience in testing + EcompUser demo = new EcompUser(); + demo.setLoginId("demo"); + demo.setFirstName("Demo"); + demo.setLastName("User"); + demo.setActive(true); + EcompRole role = new EcompRole(); + role.setName("view"); + Set roles = new HashSet<>(); + roles.add(role); + demo.setRoles(roles); + um.createUser(demo); + return um; + } + +} diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AbstractControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java similarity index 87% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AbstractControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java index c4d9a4d6..0fa2785e 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AbstractControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AbstractControllerTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; import java.net.URI; @@ -26,6 +26,7 @@ import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -98,4 +99,14 @@ public class AbstractControllerTest { logger.info("Context loads on mock profile"); } + public TestRestTemplate testRestTemplateAdminRole() { + return restTemplate.withBasicAuth(WebSecurityMockConfiguration.TEST_CRED_ADMIN, + WebSecurityMockConfiguration.TEST_CRED_ADMIN); + } + + public TestRestTemplate testRestTemplateStandardRole() { + return restTemplate.withBasicAuth(WebSecurityMockConfiguration.TEST_CRED_STANDARD, + WebSecurityMockConfiguration.TEST_CRED_STANDARD); + } + } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AcXappControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AcXappControllerTest.java similarity index 88% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AcXappControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AcXappControllerTest.java index f0ba8b58..8f8292f1 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AcXappControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AcXappControllerTest.java @@ -17,15 +17,14 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.net.URI; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - -import org.oransc.ric.portal.dashboard.controller.AcXappController; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +52,8 @@ public class AcXappControllerTest extends AbstractControllerTest { // Always returns 501; surprised that no exception is thrown. URI uri = buildUri(null, AcXappController.CONTROLLER_PATH, AcXappController.ADMCTRL_METHOD); logger.info("Invoking {}", uri); - ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class); + ResponseEntity response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null, + String.class); Assertions.assertTrue(response.getStatusCode().is5xxServerError()); } @@ -64,7 +64,8 @@ public class AcXappControllerTest extends AbstractControllerTest { URI uri = buildUri(null, AcXappController.CONTROLLER_PATH, AcXappController.ADMCTRL_METHOD); HttpEntity entity = new HttpEntity<>(body); logger.info("Invoking {}", uri); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.PUT, entity, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.PUT, entity, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AdminControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AdminControllerTest.java similarity index 80% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AdminControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AdminControllerTest.java index be66270f..018350d6 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AdminControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AdminControllerTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; import java.net.URI; @@ -25,7 +25,6 @@ import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.oransc.ric.portal.dashboard.controller.AdminController; import org.oransc.ric.portal.dashboard.model.DashboardUser; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; @@ -55,13 +54,22 @@ public class AdminControllerTest extends AbstractControllerTest { } @Test - public void usersTest() { + public void getUsersTest() { URI uri = buildUri(null, AdminController.CONTROLLER_PATH, AdminController.USER_METHOD); logger.info("Invoking {}", uri); - ResponseEntity> response = restTemplate.exchange(uri, HttpMethod.GET, null, + ResponseEntity> response = testRestTemplateAdminRole().exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() { }); Assertions.assertFalse(response.getBody().isEmpty()); } + @Test + public void getUsersTestRoleAuthFail() { + URI uri = buildUri(null, AdminController.CONTROLLER_PATH, AdminController.USER_METHOD); + logger.info("Invoking {}", uri); + ResponseEntity response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, null, + String.class); + Assertions.assertTrue(response.getStatusCode().is4xxClientError()); + } + } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AnrXappControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AnrXappControllerTest.java similarity index 88% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AnrXappControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AnrXappControllerTest.java index 7ce5976e..3155563b 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AnrXappControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AnrXappControllerTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; import java.net.URI; @@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test; import org.oransc.ric.anrxapp.client.model.GgNodeBTable; import org.oransc.ric.anrxapp.client.model.NeighborCellRelationMod; import org.oransc.ric.anrxapp.client.model.NeighborCellRelationTable; -import org.oransc.ric.portal.dashboard.controller.AnrXappController; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,7 +66,7 @@ public class AnrXappControllerTest extends AbstractControllerTest { public void gnodebsTest() { URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.GNODEBS_METHOD); logger.info("Invoking {}", uri); - GgNodeBTable list = restTemplate.getForObject(uri, GgNodeBTable.class); + GgNodeBTable list = testRestTemplateStandardRole().getForObject(uri, GgNodeBTable.class); Assertions.assertFalse(list.getGNodeBIds().isEmpty()); } @@ -75,7 +74,8 @@ public class AnrXappControllerTest extends AbstractControllerTest { public void ncrtGetTest() { URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.NCRT_METHOD); logger.info("Invoking {}", uri); - NeighborCellRelationTable table = restTemplate.getForObject(uri, NeighborCellRelationTable.class); + NeighborCellRelationTable table = testRestTemplateStandardRole().getForObject(uri, + NeighborCellRelationTable.class); Assertions.assertFalse(table.getNcrtRelations().isEmpty()); } @@ -85,7 +85,8 @@ public class AnrXappControllerTest extends AbstractControllerTest { AnrXappController.PP_SERVING, "serving", AnrXappController.PP_NEIGHBOR, "neighbor"); logger.info("Invoking {}", uri); HttpEntity entity = new HttpEntity<>(new NeighborCellRelationMod()); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.PUT, entity, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.PUT, entity, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } @@ -94,7 +95,8 @@ public class AnrXappControllerTest extends AbstractControllerTest { URI uri = buildUri(null, AnrXappController.CONTROLLER_PATH, AnrXappController.NCRT_METHOD, AnrXappController.PP_SERVING, "serving", AnrXappController.PP_NEIGHBOR, "neighbor"); logger.info("Invoking {}", uri); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AppManagerControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AppManagerControllerTest.java similarity index 84% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AppManagerControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AppManagerControllerTest.java index 936d698f..a216c764 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/AppManagerControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/AppManagerControllerTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; import java.net.URI; @@ -30,7 +30,6 @@ import org.oransc.ric.plt.appmgr.client.model.ConfigMetadata; import org.oransc.ric.plt.appmgr.client.model.XAppConfig; import org.oransc.ric.plt.appmgr.client.model.XAppInfo; import org.oransc.ric.plt.appmgr.client.model.Xapp; -import org.oransc.ric.portal.dashboard.controller.AppManagerController; import org.oransc.ric.portal.dashboard.model.DashboardDeployableXapps; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; @@ -71,7 +70,8 @@ public class AppManagerControllerTest extends AbstractControllerTest { public void appListTest() { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_LIST_METHOD); logger.info("Invoking {}", uri); - DashboardDeployableXapps apps = restTemplate.getForObject(uri, DashboardDeployableXapps.class); + DashboardDeployableXapps apps = testRestTemplateStandardRole().getForObject(uri, + DashboardDeployableXapps.class); Assertions.assertFalse(apps.isEmpty()); } @@ -79,7 +79,7 @@ public class AppManagerControllerTest extends AbstractControllerTest { public void appStatusesTest() { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD); logger.info("Invoking {}", uri); - AllDeployedXapps apps = restTemplate.getForObject(uri, AllDeployedXapps.class); + AllDeployedXapps apps = testRestTemplateStandardRole().getForObject(uri, AllDeployedXapps.class); Assertions.assertFalse(apps.isEmpty()); } @@ -87,7 +87,7 @@ public class AppManagerControllerTest extends AbstractControllerTest { public void appStatusTest() { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD, "app"); logger.info("Invoking {}", uri); - Xapp app = restTemplate.getForObject(uri, Xapp.class); + Xapp app = testRestTemplateStandardRole().getForObject(uri, Xapp.class); Assertions.assertFalse(app.getName().isEmpty()); } @@ -96,7 +96,7 @@ public class AppManagerControllerTest extends AbstractControllerTest { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD); logger.info("Invoking {}", uri); XAppInfo info = new XAppInfo(); - Xapp app = restTemplate.postForObject(uri, info, Xapp.class); + Xapp app = testRestTemplateAdminRole().postForObject(uri, info, Xapp.class); Assertions.assertFalse(app.getName().isEmpty()); } @@ -104,7 +104,8 @@ public class AppManagerControllerTest extends AbstractControllerTest { public void undeployAppTest() { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.XAPPS_METHOD, "app"); logger.info("Invoking {}", uri); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } @@ -112,7 +113,7 @@ public class AppManagerControllerTest extends AbstractControllerTest { public void getConfigTest() { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.CONFIG_METHOD); logger.info("Invoking {}", uri); - AllXappConfig config = restTemplate.getForObject(uri, AllXappConfig.class); + AllXappConfig config = testRestTemplateStandardRole().getForObject(uri, AllXappConfig.class); Assertions.assertFalse(config.isEmpty()); } @@ -121,7 +122,7 @@ public class AppManagerControllerTest extends AbstractControllerTest { URI uri = buildUri(null, AppManagerController.CONTROLLER_PATH, AppManagerController.CONFIG_METHOD); logger.info("Invoking {}", uri); XAppConfig newConfig = new XAppConfig(); - XAppConfig response = restTemplate.postForObject(uri, newConfig, XAppConfig.class); + XAppConfig response = testRestTemplateAdminRole().postForObject(uri, newConfig, XAppConfig.class); Assertions.assertNotNull(response.getConfig()); } @@ -131,7 +132,8 @@ public class AppManagerControllerTest extends AbstractControllerTest { logger.info("Invoking {}", uri); ConfigMetadata delConfig = new ConfigMetadata(); HttpEntity entity = new HttpEntity<>(delConfig); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, entity, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, entity, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/DefaultContextTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java similarity index 96% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/DefaultContextTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java index b82c3346..328be6d6 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/DefaultContextTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/DefaultContextTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/E2ManagerControllerTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/E2ManagerControllerTest.java similarity index 81% rename from webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/E2ManagerControllerTest.java rename to webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/E2ManagerControllerTest.java index 10baeb6a..d65f2b47 100644 --- a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/test/controller/E2ManagerControllerTest.java +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/E2ManagerControllerTest.java @@ -17,7 +17,7 @@ * limitations under the License. * ========================LICENSE_END=================================== */ -package org.oransc.ric.portal.dashboard.test.controller; +package org.oransc.ric.portal.dashboard.controller; import java.lang.invoke.MethodHandles; import java.net.URI; @@ -28,7 +28,6 @@ import org.junit.jupiter.api.Test; import org.oransc.ric.e2mgr.client.model.GetNodebResponse; import org.oransc.ric.e2mgr.client.model.NodebIdentity; import org.oransc.ric.e2mgr.client.model.SetupRequest; -import org.oransc.ric.portal.dashboard.controller.E2ManagerController; import org.oransc.ric.portal.dashboard.model.RanDetailsTransport; import org.oransc.ric.portal.dashboard.model.SuccessTransport; import org.slf4j.Logger; @@ -62,8 +61,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest { public void ranDetailsTest() { URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.RAN_METHOD); logger.info("Invoking {}", uri); - ResponseEntity> response = restTemplate.exchange(uri, HttpMethod.GET, null, - new ParameterizedTypeReference>() { + ResponseEntity> response = testRestTemplateStandardRole().exchange(uri, + HttpMethod.GET, null, new ParameterizedTypeReference>() { }); Assertions.assertFalse(response.getBody().isEmpty()); } @@ -72,8 +71,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest { public void nodebListTest() { URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_LIST_METHOD); logger.info("Invoking {}", uri); - ResponseEntity> response = restTemplate.exchange(uri, HttpMethod.GET, null, - new ParameterizedTypeReference>() { + ResponseEntity> response = testRestTemplateStandardRole().exchange(uri, HttpMethod.GET, + null, new ParameterizedTypeReference>() { }); Assertions.assertFalse(response.getBody().isEmpty()); } @@ -82,7 +81,7 @@ public class E2ManagerControllerTest extends AbstractControllerTest { public void nodebStatusTest() { URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_METHOD, "nodeb"); logger.info("Invoking {}", uri); - GetNodebResponse response = restTemplate.getForObject(uri, GetNodebResponse.class); + GetNodebResponse response = testRestTemplateStandardRole().getForObject(uri, GetNodebResponse.class); Assertions.assertNotNull(response.getRanName()); } @@ -90,7 +89,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest { public void bigRedButtonTest() { URI uri = buildUri(null, E2ManagerController.CONTROLLER_PATH, E2ManagerController.NODEB_METHOD); logger.info("Invoking {}", uri); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.DELETE, null, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.DELETE, null, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } @@ -100,7 +100,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest { logger.info("Invoking {}", uri); SetupRequest setup = new SetupRequest(); HttpEntity entity = new HttpEntity<>(setup); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.POST, entity, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } @@ -110,7 +111,8 @@ public class E2ManagerControllerTest extends AbstractControllerTest { logger.info("Invoking {}", uri); SetupRequest setup = new SetupRequest(); HttpEntity entity = new HttpEntity<>(setup); - ResponseEntity voidResponse = restTemplate.exchange(uri, HttpMethod.POST, entity, Void.class); + ResponseEntity voidResponse = testRestTemplateAdminRole().exchange(uri, HttpMethod.POST, entity, + Void.class); Assertions.assertTrue(voidResponse.getStatusCode().is2xxSuccessful()); } diff --git a/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java new file mode 100644 index 00000000..509dda7c --- /dev/null +++ b/webapp-backend/src/test/java/org/oransc/ric/portal/dashboard/controller/PortalRestCentralServiceTest.java @@ -0,0 +1,103 @@ +/*- + * ========================LICENSE_START================================= + * O-RAN-SC + * %% + * Copyright (C) 2019 AT&T Intellectual Property and Nokia + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================LICENSE_END=================================== + */ +package org.oransc.ric.portal.dashboard.controller; + +import java.lang.invoke.MethodHandles; +import java.net.URI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.onap.portalsdk.core.onboarding.util.PortalApiConstants; +import org.onap.portalsdk.core.restful.domain.EcompUser; +import org.oransc.ric.portal.dashboard.DashboardConstants; +import org.oransc.ric.portal.dashboard.config.WebSecurityMockConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +public class PortalRestCentralServiceTest extends AbstractControllerTest { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // paths are hardcoded here exactly like the EPSDK-FW library :( + + @Test + public void getAnalyticsTest() { + // paths are hardcoded here exactly like the EPSDK-FW library :( + URI uri = buildUri(null, PortalApiConstants.API_PREFIX, "/analytics"); + logger.info("Invoking {}", uri); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class); + // No Portal is available so this always fails + Assertions.assertTrue(response.getStatusCode().is4xxClientError()); + } + + @Test + public void getLoginPageTest() { + URI uri = buildUri(null, DashboardConstants.LOGIN_PAGE); + logger.info("Invoking {}", uri); + ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class); + Assertions.assertTrue(response.getStatusCode().is2xxSuccessful()); + Assertions.assertTrue(response.getBody().contains("Please log in")); + } + + private HttpEntity getEntityWithHeaders(Object body) { + HttpHeaders headers = new HttpHeaders(); + headers.set(WebSecurityMockConfiguration.PORTAL_USERNAME_HEADER_KEY, + WebSecurityMockConfiguration.PORTAL_USERNAME_HEADER_KEY); + headers.set(WebSecurityMockConfiguration.PORTAL_PASSWORD_HEADER_KEY, + WebSecurityMockConfiguration.PORTAL_PASSWORD_HEADER_KEY); + HttpEntity entity = new HttpEntity<>(body, headers); + return entity; + } + + @Test + public void createUserTest() { + final String loginId = "login1"; + URI create = buildUri(null, PortalApiConstants.API_PREFIX, "user"); + logger.info("Invoking {}", create); + EcompUser user = new EcompUser(); + user.setLoginId(loginId); + HttpEntity requestEntity = getEntityWithHeaders(user); + ResponseEntity response = restTemplate.exchange(create, HttpMethod.POST, requestEntity, String.class); + Assertions.assertTrue(response.getStatusCode().is2xxSuccessful()); + } + + @Test + public void updateUserTest() { + final String loginId = "login2"; + URI create = buildUri(null, PortalApiConstants.API_PREFIX, "user"); + logger.info("Invoking {}", create); + EcompUser user = new EcompUser(); + user.setLoginId(loginId); + HttpEntity requestEntity = getEntityWithHeaders(user); + // Create + ResponseEntity response = restTemplate.exchange(create, HttpMethod.POST, requestEntity, String.class); + Assertions.assertTrue(response.getStatusCode().is2xxSuccessful()); + URI update = buildUri(null, PortalApiConstants.API_PREFIX, "user", loginId); + user.setEmail("user@company.org"); + requestEntity = getEntityWithHeaders(user); + response = restTemplate.exchange(update, HttpMethod.POST, requestEntity, String.class); + Assertions.assertTrue(response.getStatusCode().is2xxSuccessful()); + } + +} diff --git a/webapp-backend/src/test/resources/key.properties b/webapp-backend/src/test/resources/key.properties new file mode 100644 index 00000000..ff9d220e --- /dev/null +++ b/webapp-backend/src/test/resources/key.properties @@ -0,0 +1,22 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +# Test properties for the EPSDK-FW library. +# This file must be present on the Java classpath. + +cipher.enc.key = bogus diff --git a/webapp-backend/src/test/resources/portal.properties b/webapp-backend/src/test/resources/portal.properties new file mode 100644 index 00000000..94e7391b --- /dev/null +++ b/webapp-backend/src/test/resources/portal.properties @@ -0,0 +1,26 @@ +# ========================LICENSE_START================================= +# O-RAN-SC +# %% +# Copyright (C) 2019 AT&T Intellectual Property and Nokia +# %% +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ========================LICENSE_END=================================== + +# Test properties for the EPSDK-FW library. +# This file must be present on the Java classpath. + +portal.api.impl.class = org.oransc.ric.portal.dashboard.portalapi.PortalRestCentralServiceImpl +role_access_centralized = remote +ecomp_redirect_url = https://www.wikipedia.org +ecomp_rest_url = http://localhost/portal +ueb_app_key = abcdef1234567890 diff --git a/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts b/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts index cbf353f5..5d923c32 100644 --- a/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts +++ b/webapp-frontend/src/app/anr-xapp/anr-xapp.datasource.ts @@ -50,9 +50,9 @@ export class ANRXappDataSource extends DataSource { this.loadingSubject.next(true); this.anrXappService.getNcrtInfo(ggnodeb, servingCellNrcgi, neighborCellNrpci) .pipe( - catchError( (err: HttpErrorResponse) => { - console.log('ANRXappDataSource failed: ' + err.message); - this.notificationService.error('Failed to get data.'); + catchError( (her: HttpErrorResponse) => { + console.log('ANRXappDataSource failed: ' + her.message); + this.notificationService.error('Failed to get data: ' + her.message); return of([]); }), finalize(() => this.loadingSubject.next(false)) diff --git a/webapp-frontend/src/app/app-control/app-control.component.ts b/webapp-frontend/src/app/app-control/app-control.component.ts index 341754b0..35c910d8 100644 --- a/webapp-frontend/src/app/app-control/app-control.component.ts +++ b/webapp-frontend/src/app/app-control/app-control.component.ts @@ -69,7 +69,7 @@ export class AppControlComponent implements OnInit { } onUndeployApp(app: XappControlRow): void { - this.confirmDialogService.openConfirmDialog('Are you sure you want to undeploy xApp ' + app.xapp + '?') + this.confirmDialogService.openConfirmDialog('Are you sure you want to undeploy App ' + app.xapp + '?') .afterClosed().subscribe( (res: boolean) => { if (res) { this.appMgrSvc.undeployXapp(app.xapp).subscribe( @@ -77,14 +77,19 @@ export class AppControlComponent implements OnInit { this.dataSource.loadTable(); switch (httpResponse.status) { case 200: - this.notificationService.success('xApp undeployed successfully!'); + this.notificationService.success('App undeployed successfully!'); break; default: - this.notificationService.warn('xApp undeploy failed.'); + this.notificationService.warn('App undeploy failed.'); } }, - ( (error: HttpErrorResponse) => { - this.notificationService.warn(error.message); + ( (her: HttpErrorResponse) => { + // the error field should have an ErrorTransport object + let msg = her.message; + if (her.error && her.error.message) { + msg = her.error.message; + } + this.notificationService.warn('App undeploy failed: ' + msg); }) ); } diff --git a/webapp-frontend/src/app/app-control/app-control.datasource.ts b/webapp-frontend/src/app/app-control/app-control.datasource.ts index fa98dfad..6a19286c 100644 --- a/webapp-frontend/src/app/app-control/app-control.datasource.ts +++ b/webapp-frontend/src/app/app-control/app-control.datasource.ts @@ -59,9 +59,9 @@ export class AppControlDataSource extends DataSource { this.loadingSubject.next(true); this.appMgrSvc.getDeployed() .pipe( - catchError( (err: HttpErrorResponse) => { - console.log('AppControlDataSource failed: ' + err.message); - this.notificationService.error('Failed to get applications.'); + catchError( (her: HttpErrorResponse) => { + console.log('AppControlDataSource failed: ' + her.message); + this.notificationService.error('Failed to get applications: ' + her.message); return of([]); }), finalize(() => this.loadingSubject.next(false)) diff --git a/webapp-frontend/src/app/catalog/catalog.component.ts b/webapp-frontend/src/app/catalog/catalog.component.ts index 4eab4083..21860ac7 100644 --- a/webapp-frontend/src/app/catalog/catalog.component.ts +++ b/webapp-frontend/src/app/catalog/catalog.component.ts @@ -60,11 +60,16 @@ export class CatalogComponent implements OnInit { if (res) { this.appMgrService.deployXapp(app.name).subscribe( (response: HttpResponse) => { - this.notificationService.success('Deploy succeeded!'); + this.notificationService.success('App deploy succeeded!'); }, - (error: HttpErrorResponse) => { - this.notificationService.warn('Deploy failed: ' + error.message); - } + ( (her: HttpErrorResponse) => { + // the error field should have an ErrorTransport object + let msg = her.message; + if (her.error && her.error.message) { + msg = her.error.message; + } + this.notificationService.warn('App deploy failed: ' + msg); + }) ); } } diff --git a/webapp-frontend/src/app/catalog/catalog.datasource.ts b/webapp-frontend/src/app/catalog/catalog.datasource.ts index 5c9ac947..fb54b84f 100644 --- a/webapp-frontend/src/app/catalog/catalog.datasource.ts +++ b/webapp-frontend/src/app/catalog/catalog.datasource.ts @@ -50,9 +50,9 @@ export class CatalogDataSource extends DataSource { this.loadingSubject.next(true); this.appMgrSvc.getDeployable() .pipe( - catchError( (err: HttpErrorResponse) => { - console.log('CatalogDataSource failed: ' + err.message); - this.notificationService.error('Failed to get applications.'); + catchError( (her: HttpErrorResponse) => { + console.log('CatalogDataSource failed: ' + her.message); + this.notificationService.error('Failed to get applications: ' + her.message); return of([]); }), finalize(() => this.loadingSubject.next(false)) diff --git a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html index 2b586ffa..2ab5f7c3 100644 --- a/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html +++ b/webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.html @@ -20,7 +20,7 @@ - + home Home diff --git a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts index 25442f77..7c63f132 100644 --- a/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-connection-dialog.component.ts @@ -86,9 +86,14 @@ export class RanControlConnectDialogComponent implements OnInit { this.notifService.success('Connect succeeded!'); this.dialogRef.close(true); }, - ( (error: HttpErrorResponse) => { + ( (her: HttpErrorResponse) => { this.processing = false; - this.errorService.displayError('RAN Connection Failed: ' + error.message); + // the error field carries the server's response + let msg = her.message; + if (her.error && her.error.message) { + msg = her.error.message; + } + this.errorService.displayError('Connect failed: ' + msg); // keep the dialog open }) ); diff --git a/webapp-frontend/src/app/ran-control/ran-control.component.ts b/webapp-frontend/src/app/ran-control/ran-control.component.ts index 26665ca6..f5ef36ca 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.component.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.component.ts @@ -66,12 +66,17 @@ export class RanControlComponent implements OnInit { this.e2MgrSvc.nodebDelete().subscribe( ( response: HttpResponse) => { if (response.status === 200) { - this.notificationService.success('Disconnect all RAN Connections Succeeded!'); + this.notificationService.success('Disconnect succeeded!'); this.dataSource.loadTable(); } }, - ( (error: HttpErrorResponse) => { - this.errorDialogService.displayError(aboutError + error.message); + ( (her: HttpErrorResponse) => { + // the error field should have an ErrorTransport object + let msg = her.message; + if (her.error && her.error.message) { + msg = her.error.message; + } + this.errorDialogService.displayError('Disconnect failed: ' + msg); }) ); } diff --git a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts index d919f4ea..50626b55 100644 --- a/webapp-frontend/src/app/ran-control/ran-control.datasource.ts +++ b/webapp-frontend/src/app/ran-control/ran-control.datasource.ts @@ -47,9 +47,9 @@ export class RANControlDataSource extends DataSource { this.loadingSubject.next(true); this.e2MgrSvcservice.getRan() .pipe( - catchError( (err: HttpErrorResponse) => { - console.log('RANControlDataSource failed: ' + err.message); - this.notificationService.error('Failed to get RAN details.'); + catchError( (her: HttpErrorResponse) => { + console.log('RANControlDataSource failed: ' + her.message); + this.notificationService.error('Failed to get RAN details: ' + her.message); return of([]); }), finalize( () => this.loadingSubject.next(false) ) diff --git a/webapp-frontend/src/app/services/stats/stats.service.ts b/webapp-frontend/src/app/services/stats/stats.service.ts index 255dbca7..e4e667a5 100644 --- a/webapp-frontend/src/app/services/stats/stats.service.ts +++ b/webapp-frontend/src/app/services/stats/stats.service.ts @@ -134,8 +134,8 @@ export class StatsService { this.delayMax = res[5].value; this.loadMax = res[6].value; }, - (err: HttpErrorResponse) => { - console.log (err.message); + (her: HttpErrorResponse) => { + console.log ('loadConfig failed: ' + her.message); }); } } diff --git a/webapp-frontend/src/app/user/user.datasource.ts b/webapp-frontend/src/app/user/user.datasource.ts index dcc03690..f53a2d3e 100644 --- a/webapp-frontend/src/app/user/user.datasource.ts +++ b/webapp-frontend/src/app/user/user.datasource.ts @@ -50,9 +50,9 @@ export class UserDataSource extends DataSource { this.loadingSubject.next(true); this.dashboardSvc.getUsers() .pipe( - catchError( (err: HttpErrorResponse) => { - console.log('UserDataSource failed: ' + err.message); - this.notificationService.error('Failed to get users.'); + catchError( (her: HttpErrorResponse) => { + console.log('UserDataSource failed: ' + her.message); + this.notificationService.error('Failed to get users: ' + her.message); return of([]); }), finalize(() => this.loadingSubject.next(false))