This preserves original scheme, either HTTP or HTTPS.
Correct URL of Dashboard REST URL in config-deploy doc.
Bump version to 1.2.3.
Change-Id: Ia7da1e2a7a0f189c95072ddda51ac59a738d9247
Signed-off-by: Lott, Christopher (cl778h) <cl778h@att.com>
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<!-- This groupId will NOT allow deployment in LF -->
<groupId>org.o-ran-sc.ric.plt.a1med.client</groupId>
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<!-- This groupId will NOT allow deployment in LF -->
<groupId>org.o-ran-sc.ric.xapp.anr.client</groupId>
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<!-- This groupId will NOT allow deployment in LF -->
<groupId>org.o-ran-sc.ric.plt.appmgr.client</groupId>
``http://dashboard.simpledemo.onap.org:8080``
- Dashboard REST URL that is reachable by the Portal back-end server.
This can be a host name or an IP address, because it does not use
- cookie-based authentication. This should be a value like
- ``http://192.168.1.1:8080/auxapi/v3``
+ cookie-based authentication. This must be a URL with suffix "/api/v3"
+ for example ``http://192.168.1.1:8080/api/v3``.
The Dashboard server only listens on a single port, so the examples
above both use the same port number. Different port numbers might be
RIC Dashboard Release Notes
===========================
+Version 1.2.3, 4 Oct 2019
+-------------------------
+* Serve unauthenticated user a login-at-portal page without using redirect
+* Upgrade to Spring-Boot 2.1.9.RELEASE
+
Version 1.2.2, 27 Sep 2019
--------------------------
* Support Portal security using EPSDK-FW cookie and user management
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<!-- This groupId will NOT allow deployment in LF -->
<groupId>org.o-ran-sc.ric.plt.e2mgr.client</groupId>
<!-- this group Id must match LF gerrit repository -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.6.RELEASE</version>
+ <version>2.1.9.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
<name>RIC Dashboard project</name>
<packaging>pom</packaging>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
<properties>
<java.version>11</java.version>
<!-- Properties for the license-maven-plugin in child POMs -->
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<artifactId>ric-dash-be</artifactId>
<name>RIC Dashboard Webapp backend</name>
}
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";
public static final String APP_NAME_AC = "AC";
+++ /dev/null
-/*-
- * ========================LICENSE_START=================================
- * O-RAN-SC
- * %%
- * Copyright (C) 2019 AT&T Intellectual Property and Nokia
- * %%
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ========================LICENSE_END===================================
- */
-package org.oransc.ric.portal.dashboard;
-
-import java.io.IOException;
-import java.lang.invoke.MethodHandles;
-import java.net.URLEncoder;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.onap.portalsdk.core.onboarding.util.PortalApiConstants;
-import org.onap.portalsdk.core.onboarding.util.PortalApiProperties;
-import org.oransc.ric.portal.dashboard.portalapi.PortalAuthenticationFilter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.http.MediaType;
-
-/**
- * Serves a login page that contains a link from configuration to ONAP Portal.
- * This avoids the immediate redirect to Portal that is confusing to users and
- * infuriating to developers.
- *
- * Basically this is do-it-yourself JSP :)
- */
-public class LoginServlet extends HttpServlet {
-
- private static final long serialVersionUID = 1191385178190976568L;
-
- private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
- @Override
- public void init(ServletConfig servletConfig) throws ServletException {
- logger.debug("init");
- super.init(servletConfig);
- final String portalURL = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
- if (portalURL == null || portalURL.length() == 0)
- throw new ServletException("Failed to get property " + PortalApiConstants.ECOMP_REDIRECT_URL);
- }
-
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws IOException, ServletException {
- logger.debug("doGet {}", request.getRequestURI());
- // The original page URL should arrive as a query parameter
- String appUrl = request.getParameter(PortalAuthenticationFilter.REDIRECT_URL_KEY);
- // If a user bookmarks the login page, then nothing arrives;
- // use the original URL without the login suffix.
- if (appUrl == null || appUrl.isEmpty()) {
- String loginUrl = request.getRequestURL().toString();
- int indexOfLogin = loginUrl.indexOf(DashboardConstants.LOGIN_PAGE);
- appUrl = loginUrl.substring(0, indexOfLogin);
- }
- String encodedAppUrl = URLEncoder.encode(appUrl, "UTF-8");
- String portalBaseUrl = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
- String redirectUrl = portalBaseUrl + "?" + PortalAuthenticationFilter.REDIRECT_URL_KEY + "=" + encodedAppUrl;
- String aHref = "<a href=\"" + redirectUrl + "\">";
- // If only Java had "here" documents.
- String body = String.join(//
- System.getProperty("line.separator"), //
- "<html>", //
- "<head>", //
- "<title>RIC Dashboard</title>", //
- "<style>", //
- "html, body { ", //
- " font-family: Helvetica, Arial, sans-serif;", //
- "}", //
- "</style>", //
- "</head>", //
- "<body>", //
- "<h2>RIC Dashboard</h2>", //
- "<h4>Please log in.</h4>", //
- "<p>", //
- aHref, "Click here to authenticate at the ONAP Portal</a>", //
- "</p>", //
- "</body>", //
- "</html>");
- writeAndFlush(response, MediaType.TEXT_HTML_VALUE, body);
- }
-
- /**
- * Sets the content type and writes the response.
- *
- * @param response
- * @param contentType
- * @param responseBody
- * @throws IOException
- */
- private void writeAndFlush(HttpServletResponse response, String contentType, String responseBody)
- throws IOException {
- response.setContentType(contentType);
- response.getWriter().print(responseBody);
- response.getWriter().flush();
- }
-
-}
import java.lang.reflect.InvocationTargetException;
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.A1MediatorController;
import org.oransc.ric.portal.dashboard.controller.AdminController;
import org.oransc.ric.portal.dashboard.controller.AnrXappController;
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;
AppManagerController.CONTROLLER_PATH + "/" + AppManagerController.VERSION_METHOD, //
E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.HEALTH_METHOD, //
E2ManagerController.CONTROLLER_PATH + "/" + E2ManagerController.VERSION_METHOD, //
- SimpleErrorController.ERROR_PATH, //
- DashboardConstants.LOGIN_PAGE //
+ SimpleErrorController.ERROR_PATH
};
@Override
return portalAuthenticationFilter;
}
- /**
- * Instantiates a trivial login servlet that serves a basic page with a link to
- * authenticate at Portal. The login filter redirects to this page instead of
- * Portal.
- *
- * @return Servlet registration bean for the Dashboard login servlet.
- */
- @Bean
- public ServletRegistrationBean<LoginServlet> loginServletBean() {
- LoginServlet servlet = new LoginServlet();
- final ServletRegistrationBean<LoginServlet> servletBean = new ServletRegistrationBean<>(servlet,
- DashboardConstants.LOGIN_PAGE);
- servletBean.setName("LoginServlet");
- return servletBean;
- }
-
}
@Value("${metrics.url.mc}")
private String mcAppMetricsUrl;
+
public AdminController() {
// Mock data
users = new DashboardUser[] { //
public static final String HEALTH_METHOD = "health";
public static final String VERSION_METHOD = DashboardConstants.VERSION_METHOD;
// Keep these consistent with the E2M implementation
- /*package*/ static final String NODEB_PREFIX = "/nodeb";
+ /* package */ static final String NODEB_PREFIX = "/nodeb";
public static final String RAN_METHOD = NODEB_PREFIX + "/ran";
public static final String NODEB_SHUTDOWN_METHOD = NODEB_PREFIX + "/shutdown";
public static final String NODEB_LIST_METHOD = NODEB_PREFIX + "/ids";
}
@ApiOperation(value = "Abort any other ongoing procedures over X2 between the RIC and the RAN.")
- @PutMapping(NODEB_PREFIX + "/{" + PP_RANNAME + "}"+ RESET_METHOD)
+ @PutMapping(NODEB_PREFIX + "/{" + PP_RANNAME + "}" + RESET_METHOD)
@Secured({ DashboardConstants.ROLE_ADMIN })
public void reset(@PathVariable(PP_RANNAME) String ranName, @RequestBody ResetRequest resetRequest,
HttpServletResponse response) {
*/
package org.oransc.ric.portal.dashboard.controller;
+import java.io.IOException;
import java.lang.invoke.MethodHandles;
+import java.net.URL;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
* https://stackoverflow.com/questions/44692781/configure-spring-boot-to-redirect-404-to-a-single-page-app
*
* @param request
- * HttpServletRequest
- * @return Forward directive to index.html
+ * HttpServletRequest
+ * @param response
+ * HttpServletResponse
+ * @throws IOException
+ * On error
*/
@RequestMapping(method = { RequestMethod.OPTIONS, RequestMethod.GET }, //
path = { "/catalog", "/control", "/stats", "/user" })
- public String forwardAngularRoutes(HttpServletRequest request) {
- logger.debug("forwardAngularRoutes: {}", request.getRequestURI());
- return "forward:/index.html";
+ public void forwardAngularRoutes(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ URL url = new URL(request.getScheme(), request.getServerName(), request.getServerPort(), "/index.html");
+ if (logger.isDebugEnabled())
+ logger.debug("forwardAngularRoutes: {} redirected to {}", request.getRequestURI(), url);
+ response.sendRedirect(url.toString());
}
}
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.onap.portalsdk.core.onboarding.exception.PortalAPIException;
+import org.onap.portalsdk.core.restful.domain.EcompRole;
import org.onap.portalsdk.core.restful.domain.EcompUser;
+import org.oransc.ric.portal.dashboard.DashboardConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides user-management services.
*
- * This first implementation serializes user details to a file. TODO: migrate to
- * a database.
+ * 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());
+ public static final String USER_FILE_PATH = "/tmp/dashboard-users.json";
+
private final File userFile;
private final List<EcompUser> users;
+ /**
+ * convenience constructor that uses default file path
+ *
+ * @param clear
+ * If true, start empty and remove any existing file.
+ *
+ * @throws IOException
+ * On file error
+ */
+ public DashboardUserManager(boolean clear) throws IOException {
+ this(USER_FILE_PATH);
+ if (clear) {
+ logger.debug("ctor: removing file {}", userFile.getAbsolutePath());
+ File f = new File(DashboardUserManager.USER_FILE_PATH);
+ if (f.exists())
+ f.delete();
+ users.clear();
+ }
+ }
+
+ /**
+ * Uses specified file path
+ *
+ * @param userFilePath
+ * File path
+ * @throws IOException
+ * If file cannot be read
+ */
public DashboardUserManager(final String userFilePath) throws IOException {
logger.debug("ctor: userfile {}", userFilePath);
if (userFilePath == null)
}
}
+ // Test infrastructure
+ public static void main(String[] args) throws Exception {
+ DashboardUserManager dum = new DashboardUserManager(false);
+ EcompUser user = new EcompUser();
+ user.setActive(true);
+ user.setLoginId("demo");
+ user.setFirstName("First");
+ user.setLastName("Last");
+ EcompRole role = new EcompRole();
+ role.setId(1L);
+ role.setName(DashboardConstants.ROLE_NAME_ADMIN);
+ Set<EcompRole> roles = new HashSet<>();
+ roles.add(role);
+ user.setRoles(roles);
+ dum.createUser(user);
+ logger.debug("Created user {}", user);
+ }
+
}
import org.oransc.ric.portal.dashboard.model.EcompUserDetails;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
throws IOException, ServletException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.getAuthorities().isEmpty()) {
- logger.debug("doFilter adding auth to request {}", req);
+ if (logger.isDebugEnabled()) {
+ logger.debug("doFilter adding auth to request URI {}",
+ (req instanceof HttpServletRequest) ? ((HttpServletRequest) req).getRequestURL() : req);
+ }
EcompRole admin = new EcompRole();
admin.setId(1L);
admin.setName(DashboardConstants.ROLE_ADMIN);
*/
private void doFilterEPSDKFW(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
- logger.debug("doFilter {}", req);
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
+ if (logger.isTraceEnabled())
+ logger.trace("doFilter: req {}", request.getRequestURI());
// Need to authenticate the request
final String userId = authManager.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);
+ logger.debug("doFilter: unauthorized user requests URI {}, serving login page", request.getRequestURI());
+ StringBuffer sb = request.getRequestURL();
+ sb.append(request.getQueryString() == null ? "" : "?" + request.getQueryString());
+ String body = generateLoginRedirectPage(sb.toString());
+ response.setContentType(MediaType.TEXT_HTML_VALUE);
+ response.getWriter().print(body);
+ response.getWriter().flush();
} else {
EcompUserDetails userDetails = new EcompUserDetails(ecompUser);
// Using portal session as credentials is a hack
}
}
- 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;
+ /**
+ * Generates a page with text only, absolutely no references to any webapp
+ * resources, so this can be served to an unauthenticated user without
+ * triggering a new authentication attempt. The page has a link to the Portal
+ * URL from configuration, with a return URL that is the original request.
+ *
+ * @param appUrl
+ * Original requested URL
+ * @return HTML
+ * @throws UnsupportedEncodingException
+ * On error
+ */
+ private static String generateLoginRedirectPage(String appUrl) throws UnsupportedEncodingException {
+ String encodedAppUrl = URLEncoder.encode(appUrl, "UTF-8");
+ String portalBaseUrl = PortalApiProperties.getProperty(PortalApiConstants.ECOMP_REDIRECT_URL);
+ String redirectUrl = portalBaseUrl + "?" + PortalAuthenticationFilter.REDIRECT_URL_KEY + "=" + encodedAppUrl;
+ String aHref = "<a href=\"" + redirectUrl + "\">";
+ // If only Java had "here" documents.
+ String body = String.join(//
+ System.getProperty("line.separator"), //
+ "<html>", //
+ "<head>", //
+ "<title>RIC Dashboard</title>", //
+ "<style>", //
+ "html, body { ", //
+ " font-family: Helvetica, Arial, sans-serif;", //
+ "}", //
+ "</style>", //
+ "</head>", //
+ "<body>", //
+ "<h2>RIC Dashboard</h2>", //
+ "<h4>Please log in.</h4>", //
+ "<p>", //
+ aHref, "Click here to authenticate at the ONAP Portal</a>", //
+ "</p>", //
+ "</body>", //
+ "</html>");
+ return body;
}
/**
userfile = users.json
# boolean flag whether to enforce Portal user and roles on requests
-portalapi.security = false
+portalapi.security = true
# class that decrypts ciphertext from Portal
portalapi.decryptor = org.oransc.ric.portal.dashboard.portalapi.PortalSdkDecryptorAes
# name of request cookie with user ID
*/
package org.oransc.ric.portal.dashboard.config;
-import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.HashSet;
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.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;
web.ignoring().antMatchers("/", "/csrf"); // allow swagger-ui to load
}
- @Bean
- public ServletRegistrationBean<LoginServlet> loginServlet() {
- LoginServlet servlet = new LoginServlet();
- final ServletRegistrationBean<LoginServlet> servletBean = new ServletRegistrationBean<>(servlet,
- DashboardConstants.LOGIN_PAGE);
- servletBean.setName("LoginServlet");
- return servletBean;
- }
-
// 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());
+ DashboardUserManager dum = new DashboardUserManager(true);
// Mock user for convenience in testing
EcompUser demo = new EcompUser();
demo.setLoginId("demo");
Set<EcompRole> roles = new HashSet<>();
roles.add(role);
demo.setRoles(roles);
- um.createUser(demo);
- return um;
+ dum.createUser(demo);
+ return dum;
}
}
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.PortalApIMockConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 :(
}
@Test
- public void getLoginPageTest() {
- URI uri = buildUri(null, DashboardConstants.LOGIN_PAGE);
+ public void getErrorPageTest() {
+ // Send unauthorized request
+ URI uri = buildUri(null, "/favicon.ico");
logger.info("Invoking {}", uri);
ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.GET, null, String.class);
- Assertions.assertTrue(response.getStatusCode().is2xxSuccessful());
- Assertions.assertTrue(response.getBody().contains("Please log in"));
+ Assertions.assertTrue(response.getStatusCode().is4xxClientError());
+ Assertions.assertTrue(response.getBody().contains("Static error page"));
}
private HttpEntity<Object> getEntityWithHeaders(Object body) {
<parent>
<groupId>org.o-ran-sc.portal.ric-dashboard</groupId>
<artifactId>ric-dash-parent</artifactId>
- <version>1.2.2-SNAPSHOT</version>
+ <version>1.2.3-SNAPSHOT</version>
</parent>
<artifactId>ric-dash-fe</artifactId>
<name>RIC Dashboard Webapp frontend</name>