Basic Northbound APIs for User Management 48/1948/3
authormrajvaid <mrajvaid@cisco.com>
Thu, 17 Oct 2013 00:37:32 +0000 (17:37 -0700)
committerGerrit Code Review <gerrit@opendaylight.org>
Fri, 18 Oct 2013 00:17:17 +0000 (00:17 +0000)
Change-Id: Ie0d36b9faf92fd0663fe17179d66e5f471f79e38
Signed-off-by: mrajvaid <mrajvaid@cisco.com>
opendaylight/distribution/opendaylight/pom.xml
opendaylight/northbound/usermanager/enunciate.xml [new file with mode: 0644]
opendaylight/northbound/usermanager/pom.xml [new file with mode: 0644]
opendaylight/northbound/usermanager/src/main/java/org/opendaylight/controller/usermanager/northbound/UserManagerNorthbound.java [new file with mode: 0644]
opendaylight/northbound/usermanager/src/main/resources/WEB-INF/web.xml [new file with mode: 0644]
opendaylight/northbound/usermanager/src/test/java/org/opendaylight/controller/subnets/northbound/UserManagerNorthboundTest.java [new file with mode: 0644]
opendaylight/usermanager/api/pom.xml
opendaylight/usermanager/api/src/main/java/org/opendaylight/controller/usermanager/UserConfig.java
pom.xml

index 1744144..4122357 100644 (file)
       <artifactId>connectionmanager.northbound</artifactId>
       <version>${connectionmanager.version}</version>
     </dependency>
-
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>usermanager.northbound</artifactId>
+      <version>0.0.1-SNAPSHOT</version>
+    </dependency>
     <!-- Debug and logging -->
 
     <dependency>
diff --git a/opendaylight/northbound/usermanager/enunciate.xml b/opendaylight/northbound/usermanager/enunciate.xml
new file mode 100644 (file)
index 0000000..a0b9546
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<enunciate label="full" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:noNamespaceSchemaLocation="http://enunciate.codehaus.org/schemas/enunciate-1.26.xsd">
+
+  <services>
+    <rest defaultRestSubcontext="/controller/nb/v2/usermanager"/>
+  </services>
+
+  <modules>
+    <docs docsDir="rest" title="User Management  REST API" includeExampleXml="true" includeExampleJson="true"/>
+  </modules>
+</enunciate>
diff --git a/opendaylight/northbound/usermanager/pom.xml b/opendaylight/northbound/usermanager/pom.xml
new file mode 100644 (file)
index 0000000..8a85dbb
--- /dev/null
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.opendaylight.controller</groupId>
+    <artifactId>commons.opendaylight</artifactId>
+    <version>1.4.1-SNAPSHOT</version>
+    <relativePath>../../commons/opendaylight</relativePath>
+  </parent>
+  <scm>
+    <connection>scm:git:ssh://git.opendaylight.org:29418/controller.git</connection>
+    <developerConnection>scm:git:ssh://git.opendaylight.org:29418/controller.git</developerConnection>
+    <url>https://wiki.opendaylight.org/view/OpenDaylight_Controller:Main</url>
+    <tag>HEAD</tag>
+  </scm>
+
+  <artifactId>usermanager.northbound</artifactId>
+  <version>0.0.1-SNAPSHOT</version>
+  <packaging>bundle</packaging>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.enunciate</groupId>
+        <artifactId>maven-enunciate-plugin</artifactId>
+        <version>${enunciate.version}</version>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <version>${bundle.plugin.version}</version>
+        <extensions>true</extensions>
+        <configuration>
+          <instructions>
+            <Export-Package>
+            </Export-Package>
+            <Import-Package>
+              org.opendaylight.controller.sal.utils,
+              org.opendaylight.controller.northbound.commons,
+              org.opendaylight.controller.northbound.commons.exception,
+              org.opendaylight.controller.northbound.commons.utils,
+              com.sun.jersey.spi.container.servlet,
+              org.opendaylight.controller.sal.authorization,
+              org.opendaylight.controller.usermanager,
+              javax.ws.rs,
+              javax.ws.rs.core,
+              javax.xml.bind,
+              javax.xml.bind.annotation,
+              org.slf4j,
+              org.apache.catalina.filters,
+              org.codehaus.jackson.jaxrs,
+              !org.codehaus.enunciate.jaxrs
+            </Import-Package>
+            <Export-Package>
+            </Export-Package>
+            <Web-ContextPath>/controller/nb/v2/usermanager</Web-ContextPath>
+            <Jaxrs-Resources>,${classes;ANNOTATION;javax.ws.rs.Path}</Jaxrs-Resources>
+          </instructions>
+          <manifestLocation>${project.basedir}/src/main/resources/META-INF</manifestLocation>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal</artifactId>
+      <version>0.5.1-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>usermanager</artifactId>
+      <version>0.4.1-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>commons.northbound</artifactId>
+      <version>0.4.1-SNAPSHOT</version>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.enunciate</groupId>
+      <artifactId>enunciate-core-annotations</artifactId>
+      <version>${enunciate.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>${junit.version}</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/opendaylight/northbound/usermanager/src/main/java/org/opendaylight/controller/usermanager/northbound/UserManagerNorthbound.java b/opendaylight/northbound/usermanager/src/main/java/org/opendaylight/controller/usermanager/northbound/UserManagerNorthbound.java
new file mode 100644 (file)
index 0000000..ad7e774
--- /dev/null
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.usermanager.northbound;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.codehaus.enunciate.jaxrs.ResponseCode;
+import org.codehaus.enunciate.jaxrs.StatusCodes;
+import org.codehaus.enunciate.jaxrs.TypeHint;
+import org.opendaylight.controller.northbound.commons.RestMessages;
+import org.opendaylight.controller.northbound.commons.exception.BadRequestException;
+import org.opendaylight.controller.northbound.commons.exception.ResourceConflictException;
+import org.opendaylight.controller.northbound.commons.exception.ServiceUnavailableException;
+import org.opendaylight.controller.northbound.commons.exception.UnauthorizedException;
+import org.opendaylight.controller.northbound.commons.utils.NorthboundUtils;
+import org.opendaylight.controller.sal.authorization.UserLevel;
+import org.opendaylight.controller.sal.utils.ServiceHelper;
+import org.opendaylight.controller.sal.utils.Status;
+import org.opendaylight.controller.usermanager.IUserManager;
+import org.opendaylight.controller.usermanager.UserConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class provides REST APIs to manage users.
+ * This API will only be availalbe via HTTPS.
+ * <br>
+ * <br>
+ * Authentication scheme : <b>HTTP Basic</b><br>
+ * Authentication realm : <b>opendaylight</b><br>
+ * Transport : <b> HTTPS </b><br>
+ * <br>
+ * HTTPS Authentication is disabled by default so to
+ * use UserManager APIs turn on HTTPS on Web Server
+ */
+
+@Path("/")
+public class UserManagerNorthbound {
+    protected static final Logger logger = LoggerFactory.getLogger(UserManagerNorthbound.class);
+
+    private String username;
+
+    @Context
+    public void setSecurityContext(SecurityContext context) {
+        if (context != null && context.getUserPrincipal() != null) {
+            username = context.getUserPrincipal().getName();
+        }
+    }
+
+    protected String getUserName() {
+        return username;
+    }
+
+    private void handleNameMismatch(String name, String nameinURL) {
+        if (name == null || nameinURL == null) {
+            throw new BadRequestException(RestMessages.INVALIDDATA.toString() + " : Name is null");
+        }
+
+        if (name.equals(nameinURL)) {
+            return;
+        }
+        throw new ResourceConflictException(RestMessages.INVALIDDATA.toString()
+                + " : Name in URL does not match the name in request body");
+    }
+
+    /**
+     * Add a user
+     *
+     * @param userName
+     *           name of new user to be added
+     * @param userConfigData
+     *           the {@link UserConfig} user config structure in request body
+     *
+     * @return Response as dictated by the HTTP Response Status code
+     *
+     *         <pre>
+     * Example:
+     *
+     * Request URL:
+     * https://localhost/controller/nb/v2/usermanager/user/testuser
+     *
+     * Request body in XML:
+     *  &lt;userConfig&gt;
+     *      &lt;name&gt;testuser&lt;/name&gt;
+     *      &lt;roles&gt;Network-Admin&lt;/roles&gt;
+     *      &lt;password&gt;pass!23&lt;/password&gt;
+     *  &lt;/userConfig&gt;
+     *
+     * Request body in JSON:
+     * {
+     *  "name":"testuser",
+     *  "password":"pass!23",
+     *  "roles":[
+     *       "Network-Admin"
+     *       ]
+     * }
+     * </pre>
+     */
+
+    @Path("/user/{userName}")
+    @PUT
+    @StatusCodes({ @ResponseCode(code = 201, condition = "User created successfully"),
+        @ResponseCode(code = 400, condition = "Invalid data passed"),
+        @ResponseCode(code = 401, condition = "User not authorized to perform this operation"),
+        @ResponseCode(code = 409, condition = "User name in url conflicts with name in request body"),
+        @ResponseCode(code = 404, condition = "User config is null"),
+        @ResponseCode(code = 500, condition = "Internal Server Error: Addition of user failed"),
+        @ResponseCode(code = 503, condition = "Service unavailable") })
+    @Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
+    public Response addLocalUser(@PathParam("userName") String newUserName,@TypeHint(UserConfig.class) UserConfig userConfigData) {
+
+        if (!isAdminUser()) {
+            throw new UnauthorizedException("User is not authorized to perform user management operations ");
+        }
+
+        // Reconstructing the object so password can be hashed in userConfig
+        UserConfig userCfgObject = new UserConfig(userConfigData.getUser(),userConfigData.getPassword(),
+                 userConfigData.getRoles());
+
+        handleNameMismatch(userCfgObject.getUser(), newUserName);
+
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
+        if (userManager == null) {
+            throw new ServiceUnavailableException("UserManager " + RestMessages.SERVICEUNAVAILABLE.toString());
+        }
+        Status status = userManager.addLocalUser(userCfgObject);
+        if (status.isSuccess()) {
+
+            NorthboundUtils.auditlog("User", username, "added", newUserName);
+            return Response.status(Response.Status.CREATED).build();
+        }
+        return NorthboundUtils.getResponse(status);
+    }
+
+    /**
+     * Delete a user
+     *
+     * @param userName
+     *            name of user to be deleted
+     * @return Response as dictated by the HTTP Response Status code
+     *
+     * <pre>
+     * Example:
+     *
+     * Request URL:
+     * https://localhost/controller/nb/v2/usermanager/user/testuser
+     *
+     * </pre>
+     */
+    @Path("/user/{userName}")
+    @DELETE
+    @StatusCodes({ @ResponseCode(code = 204, condition = "User Deleted Successfully"),
+        @ResponseCode(code = 401, condition = "User not authorized to perform this operation"),
+        @ResponseCode(code = 404, condition = "The userName passed was not found"),
+        @ResponseCode(code = 500, condition = "Internal Server Error : Removal of user failed"),
+        @ResponseCode(code = 503, condition = "Service unavailable") })
+    public Response removeLocalUser(@PathParam("userName") String userToBeRemoved) {
+
+        if (!isAdminUser()) {
+            throw new UnauthorizedException("User is not authorized to perform user management operations ");
+        }
+
+        IUserManager userManager = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
+        if (userManager == null) {
+            throw new ServiceUnavailableException("UserManager " + RestMessages.SERVICEUNAVAILABLE.toString());
+        }
+        Status status = userManager.removeLocalUser(userToBeRemoved);
+        if (status.isSuccess()) {
+            NorthboundUtils.auditlog("User", username, "removed", userToBeRemoved);
+            return Response.noContent().build();
+        }
+        return NorthboundUtils.getResponse(status);
+    }
+
+    private boolean isAdminUser(){
+        // get UserManager's instance
+        IUserManager auth = (IUserManager) ServiceHelper.getGlobalInstance(IUserManager.class, this);
+        // check if logged in user has privileges of NETWORK_ADMIN or SYSTEM_ADMIN, if so return true
+        return auth.getUserLevel(getUserName()).ordinal() <= UserLevel.NETWORKADMIN.ordinal();
+    }
+
+}
diff --git a/opendaylight/northbound/usermanager/src/main/resources/WEB-INF/web.xml b/opendaylight/northbound/usermanager/src/main/resources/WEB-INF/web.xml
new file mode 100644 (file)
index 0000000..9a191ac
--- /dev/null
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+        version="3.0">
+  <servlet>
+    <servlet-name>JAXRSUserManager</servlet-name>
+    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
+    <init-param>
+      <param-name>javax.ws.rs.Application</param-name>
+      <param-value> org.opendaylight.controller.northbound.commons.NorthboundApplication</param-value>
+    </init-param>
+    <load-on-startup>1</load-on-startup>
+  </servlet>
+
+  <servlet-mapping>
+    <servlet-name>JAXRSUserManager</servlet-name>
+    <url-pattern>/*</url-pattern>
+  </servlet-mapping>
+        <filter>
+          <filter-name>CorsFilter</filter-name>
+          <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
+          <init-param>
+            <param-name>cors.allowed.origins</param-name>
+            <param-value>*</param-value>
+          </init-param>
+          <init-param>
+            <param-name>cors.allowed.methods</param-name>
+            <param-value>GET,POST,HEAD,OPTIONS,PUT,DELETE</param-value>
+          </init-param>
+          <init-param>
+            <param-name>cors.allowed.headers</param-name>
+            <param-value>Content-Type,X-Requested-With,accept,authorization, origin,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>
+          </init-param>
+          <init-param>
+            <param-name>cors.exposed.headers</param-name>
+            <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>
+          </init-param>
+          <init-param>
+            <param-name>cors.support.credentials</param-name>
+            <param-value>true</param-value>
+          </init-param>
+          <init-param>
+            <param-name>cors.preflight.maxage</param-name>
+            <param-value>10</param-value>
+          </init-param>
+        </filter>
+        <filter-mapping>
+          <filter-name>CorsFilter</filter-name>
+          <url-pattern>/*</url-pattern>
+        </filter-mapping>
+
+        <security-constraint>
+          <web-resource-collection>
+            <web-resource-name>NB api</web-resource-name>
+            <url-pattern>/*</url-pattern>
+            <http-method>POST</http-method>
+            <http-method>GET</http-method>
+            <http-method>PUT</http-method>
+            <http-method>PATCH</http-method>
+            <http-method>DELETE</http-method>
+            <http-method>HEAD</http-method>
+          </web-resource-collection>
+          <auth-constraint>
+            <role-name>System-Admin</role-name>
+            <role-name>Network-Admin</role-name>
+          </auth-constraint>
+          <!--  To enable SSL only access for this bundle, with this setting UserManager
+          northbound APIs will only be available over HTTPS-->
+          <user-data-constraint>
+            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
+          </user-data-constraint>
+        </security-constraint>
+
+        <security-role>
+                <role-name>System-Admin</role-name>
+        </security-role>
+        <security-role>
+                <role-name>Network-Admin</role-name>
+        </security-role>
+
+        <login-config>
+                <auth-method>BASIC</auth-method>
+                <realm-name>opendaylight</realm-name>
+        </login-config>
+</web-app>
diff --git a/opendaylight/northbound/usermanager/src/test/java/org/opendaylight/controller/subnets/northbound/UserManagerNorthboundTest.java b/opendaylight/northbound/usermanager/src/test/java/org/opendaylight/controller/subnets/northbound/UserManagerNorthboundTest.java
new file mode 100644 (file)
index 0000000..03eb8a6
--- /dev/null
@@ -0,0 +1,33 @@
+package org.opendaylight.controller.subnets.northbound;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.opendaylight.controller.sal.authorization.AuthResultEnum;
+import org.opendaylight.controller.usermanager.AuthResponse;
+import org.opendaylight.controller.usermanager.UserConfig;
+
+public class UserManagerNorthboundTest {
+
+    @Test
+    public void testUserConfigs() {
+       List<String> roles = new ArrayList<String>();
+       roles.add("Network-Admin");
+
+       UserConfig userConfig = new UserConfig("test","testPass",roles);
+
+        Assert.assertNotNull(userConfig);
+        Assert.assertNotNull(userConfig.getUser());
+        Assert.assertNotNull(userConfig.getPassword());
+        Assert.assertTrue(userConfig.getRoles().equals(roles));
+
+
+        AuthResponse authResponse = userConfig.authenticate("testPass");
+        Assert.assertNotNull(authResponse);
+
+        Assert.assertEquals(AuthResultEnum.AUTH_ACCEPT_LOC,authResponse.getStatus());
+    }
+
+}
index 14a3059..5d9e6c7 100644 (file)
@@ -47,7 +47,8 @@
               org.apache.commons.lang3,
               org.springframework.security.authentication,
               org.springframework.security.core.authority,
-              org.springframework.security.core.userdetails
+              org.springframework.security.core.userdetails,
+              javax.xml.bind.annotation
             </Import-Package>
             <Export-Package>
               org.opendaylight.controller.usermanager,
index 2e03db1..6867ef4 100644 (file)
@@ -18,21 +18,48 @@ import java.util.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
 import org.opendaylight.controller.sal.authorization.AuthResultEnum;
 import org.opendaylight.controller.sal.utils.HexEncode;
 import org.opendaylight.controller.sal.utils.Status;
 import org.opendaylight.controller.sal.utils.StatusCode;
-import org.opendaylight.controller.usermanager.AuthResponse;
 
 /**
  * Configuration Java Object which represents a Local AAA user configuration
  * information for User Manager.
  */
+@XmlRootElement
+@XmlAccessorType(XmlAccessType.NONE)
 public class UserConfig implements Serializable {
     private static final long serialVersionUID = 1L;
 
+    /**
+     * User Id
+     */
+    @XmlElement
     protected String user;
+
+    /**
+     * List of roles a user can have
+     * example
+     * System-Admin
+     * Network-Admin
+     * Netowrk-Operator
+     */
+    @XmlElement
     protected List<String> roles;
+
+    /**
+     * Password
+     * Should be 8 to 256 characters long,
+     * contain both upper and lower case letters, at least one number,
+     * and at least one non alphanumeric character.
+     */
+    @XmlElement
     private String password;
 
     private static final boolean strongPasswordCheck = Boolean.getBoolean("enableStrongPasswordCheck");
@@ -41,6 +68,7 @@ public class UserConfig implements Serializable {
     protected static final String PASSWORD_REGEX = "(?=.*[^a-zA-Z0-9])(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,256}$";
     private static final Pattern INVALID_USERNAME_CHARACTERS = Pattern.compile("([/\\s\\.\\?#%;\\\\]+)");
     private static MessageDigest oneWayFunction = null;
+
     static {
         try {
             UserConfig.oneWayFunction = MessageDigest.getInstance("SHA-1");
diff --git a/pom.xml b/pom.xml
index f45fb62..97b3088 100644 (file)
--- a/pom.xml
+++ b/pom.xml
     <module>opendaylight/northbound/networkconfiguration/bridgedomain</module>
     <module>opendaylight/northbound/httpservice-bridge</module>
     <module>opendaylight/northbound/connectionmanager</module>
+    <module>opendaylight/northbound/usermanager</module>
 
     <!-- Northbound integration tests -->
     <module>opendaylight/northbound/integrationtest</module>