Author: [email protected]
Date: Tue Mar 27 12:22:42 2012
New Revision: 2158

Log:
[AMDATUAUTH-122] Implemented password restrictions, configurable using OSGi 
config admin.

Added:
   
trunk/amdatu-auth/config/src/main/resources/org.amdatu.auth.useradmin.rest.cfg
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/Label.java
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/PasswordPolicy.java
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/util/
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/util/ConfigurationUtil.java
Modified:
   trunk/amdatu-auth/test-integration/base/pom.xml
   
trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java
   trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin.js
   
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin_rest.js
   
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/labels/useradmin_ALL_ALL.xml
   trunk/amdatu-auth/useradmin-rest/pom.xml
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/osgi/Activator.java
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/ResourceBase.java
   
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/UsersResource.java

Added: 
trunk/amdatu-auth/config/src/main/resources/org.amdatu.auth.useradmin.rest.cfg
==============================================================================
--- (empty file)
+++ 
trunk/amdatu-auth/config/src/main/resources/org.amdatu.auth.useradmin.rest.cfg  
    Tue Mar 27 12:22:42 2012
@@ -0,0 +1,26 @@
+# Copyright (c) 2010, 2011 The Amdatu Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.verning permissions and limitations
+# under the License.
+
+# Requires that the password consists of at least this amount of characters
+password.minimum_length=6
+
+# Requires that the password contains letters in both lowercase and uppercase 
(true or false)
+password.require_mixed_case=false
+
+# Minimum amount of digits the password must have
+password.minimum_digits=0
+
+# Minimum amount of special characters (other then digits and letters) the 
password must have
+password.minimum_special_characters=0
\ No newline at end of file

Modified: trunk/amdatu-auth/test-integration/base/pom.xml
==============================================================================
--- trunk/amdatu-auth/test-integration/base/pom.xml     (original)
+++ trunk/amdatu-auth/test-integration/base/pom.xml     Tue Mar 27 12:22:42 2012
@@ -41,6 +41,13 @@
       <scope>provided</scope>
       <type>bundle</type>
     </dependency>
+    <dependency>
+      <groupId>org.amdatu.auth</groupId>
+      <artifactId>org.amdatu.auth.useradmin.rest</artifactId>
+      <version>${project.version}</version>
+      <scope>provided</scope>
+      <type>bundle</type>
+    </dependency>
   </dependencies>
 
   <!--

Modified: 
trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java
==============================================================================
--- 
trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java
        (original)
+++ 
trunk/amdatu-auth/test-integration/base/src/main/java/org/amdatu/auth/test/integration/base/AuthFixture.java
        Tue Mar 27 12:22:42 2012
@@ -21,6 +21,7 @@
 import org.amdatu.auth.oauth.server.OAuthServerConfig;
 import org.amdatu.auth.tokenprovider.TokenProvider;
 import org.amdatu.core.itest.base.TestContext;
+import org.amdatu.auth.useradmin.rest.service.UsersResource;
 
 import java.util.Properties;
 
@@ -73,6 +74,7 @@
     public void configureOAuthServer(TestContext testContext) throws Exception 
{
         testContext.updateConfig(OAuthServerConfig.PID, getOAuthServerCfg());
         testContext.updateConfig(TokenProvider.PID, getTokenProviderCfg());
+        testContext.updateConfig(UsersResource.PID, getUserAdminCfg());
     }
 
     private Properties getOAuthServerCfg() {
@@ -117,4 +119,13 @@
         properties.put("org.apache.felix.log.storeDebug", "true");
         return properties;
     }
+    
+    private Properties getUserAdminCfg() {
+        Properties properties = new Properties();
+        properties.put("password.minimum_length", "6");
+        properties.put("password.require_mixed_case", "false");
+        properties.put("password.minimum_digits", "0");
+        properties.put("password.minimum_special_characters", "0");
+        return properties;
+    }
 }

Modified: 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin.js
==============================================================================
--- 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin.js    
    (original)
+++ 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin.js    
    Tue Mar 27 12:22:42 2012
@@ -37,7 +37,7 @@
     var prevIcon = "ui-icon-seek-prev' style='float: left;";
     var nextIcon = "ui-icon-seek-next' style='float: right;";
     var endIcon = "ui-icon-seek-end' style='float: right;";
-    
+
     var links = jsonData["links"];
     for (i=0; i<links.length; i++) {
       if (links[i]["rel"] == "Start") {
@@ -70,7 +70,7 @@
     if (start == null) {
       start = getDisabledNavigationLink(startIcon);
     }
-       
+
     var html = "";
     var roles = ensureArray(jsonData["roles"]);
     if (roles) {
@@ -79,7 +79,7 @@
       } else {
         nameLabel = prefs.getMsg('group_name');
       }
-      
+
       html += "<table width='100%'><tr class='ui-widget-header'><th 
width='80%' align='left'>" + nameLabel + "</th><th width='20%'>" + 
prefs.getMsg('delete') + "</th></tr>";
       for (i = 0; i < roles.length; ++i) {
         var role = roles[i];
@@ -92,14 +92,14 @@
       }
       html += "<tr class='ui-widget-header'><td /><td /></tr></table>";
     }
-         
+
     html += "<center id='"+currentView+"display'>";
     html += start + " " + prev + " ";
     html += prefs.getMsg('results_nav_1') + " <b>" + pageStartIndex + "-" + 
pageEndIndex + "</b> ";
     html += prefs.getMsg('results_nav_2') + " <b>" + resultCount + "</b>";
     html += " " + end + " " + next + "</center>";
-    
-    document.getElementById(currentView).innerHTML = html;    
+
+    document.getElementById(currentView).innerHTML = html;
   }
   else if (response.rc == 401) {
     hide('add_' + currentView + '_button');
@@ -107,7 +107,7 @@
             +"<span class='ui-icon ui-icon-alert' style='float: left; 
margin-right: .3em;'></span><span>"+prefs.getMsg('unauthorized')+"</span>"
             +"</p></div>";
     document.getElementById(currentView).innerHTML = html;
-  } 
+  }
   else {
     showError(prefs.getMsg('unexpected_error') + response.errors);
   }
@@ -127,7 +127,7 @@
       }
       html += "</table>";
       document.getElementById('implied_roles').innerHTML = html;
-    } 
+    }
     else {
       selectedRole = role["name"];
       var requiredMembers = ensureArray(role["requiredMembers"]);
@@ -171,6 +171,9 @@
 function onPasswordChanged(response) {
   if (response.rc == 200) {
     showInfo(prefs.getMsg('password_changed'));
+  } else if (response.rc == 400) {
+    var msg = prefs.getMsg('password_strength_violation');
+    showPasswordPolicy(msg);
   } else if (response.rc == 404) {
     showError(prefs.getMsg('role_not_found'));
   } else if (response.rc == 401) {
@@ -231,6 +234,9 @@
     showInfo(prefs.getMsg('role_added'));
     loadRoles(currentUrl, onRolesLoaded);
     gadgets.window.adjustHeight();
+  } else if (response.rc == 400) {
+    showError(prefs.getMsg('password_strength_violation'));
+    getPasswordPolicy();
   } else if (response.rc == 404) {
     showError(prefs.getMsg('role_not_found'));
   } else if (response.rc == 401) {

Modified: 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin_rest.js
==============================================================================
--- 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin_rest.js
   (original)
+++ 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/js/useradmin_rest.js
   Tue Mar 27 12:22:42 2012
@@ -116,6 +116,20 @@
   gadgets.io.makeRequest(url, callback, params);
 }
 
+function showPasswordPolicy(basemsg) {
+  var url = "/rest/users/passwordpolicy";
+  url = getBaseUrl() + addNoCache(url);
+
+  var params = {};
+  params[gadgets.io.RequestParameters.CONTENT_TYPE] = 
gadgets.io.ContentType.JSON;
+  params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.GET;
+  addAmdatuToken(params);
+  gadgets.io.makeRequest(url, function(response) {
+    var policy = response.data.passwordpolicy.description.label.content;
+    showError(basemsg + ' ' + policy);
+  }, params);
+}
+
 function addNoCache(url) {
   var ts = new Date().getTime();
   if (url.indexOf("?") == -1) {

Modified: 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/labels/useradmin_ALL_ALL.xml
==============================================================================
--- 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/labels/useradmin_ALL_ALL.xml
   (original)
+++ 
trunk/amdatu-auth/useradmin-gadget/src/main/resources/static/labels/useradmin_ALL_ALL.xml
   Tue Mar 27 12:22:42 2012
@@ -28,6 +28,7 @@
   <msg name="password">Password</msg>
   <msg name="new_password">New password</msg>
   <msg name="changepwd_upon_login">User must change password at first 
login</msg>
+  <msg name="password_strength_violation">The entered password is not strong 
enough.</msg>
   <msg name="implied_roles">Implied roles</msg>
   <msg name="details">Details</msg>
 

Modified: trunk/amdatu-auth/useradmin-rest/pom.xml
==============================================================================
--- trunk/amdatu-auth/useradmin-rest/pom.xml    (original)
+++ trunk/amdatu-auth/useradmin-rest/pom.xml    Tue Mar 27 12:22:42 2012
@@ -66,6 +66,12 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+      <version>3.1</version>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>1.5.6</version>
@@ -89,7 +95,7 @@
             
<Bundle-Activator>org.amdatu.auth.useradmin.rest.osgi.Activator</Bundle-Activator>
             <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
             
<Private-Package>org.amdatu.auth.useradmin.rest.*,org.apache.wink.common.internal.uri</Private-Package>
-            
<Embed-Dependency>json,org.amdatu.libraries.utilities;scope=compile</Embed-Dependency>
+            
<Embed-Dependency>commons-lang3,json,org.amdatu.libraries.utilities;scope=compile</Embed-Dependency>
             <Export-Package>
               !*
             </Export-Package>

Added: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/Label.java
==============================================================================
--- (empty file)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/Label.java
       Tue Mar 27 12:22:42 2012
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2010, 2011 The Amdatu Foundation
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.amdatu.auth.useradmin.rest.bean;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlValue;
+
+/**
+ * This means represents a language label.
+ * 
+ * @author <a href="mailto:[email protected]";>Amdatu Project 
Team</a>
+ */
+@SuppressWarnings("restriction")
+@XmlAccessorType(XmlAccessType.NONE)
+public class Label {
+    @XmlAttribute(name="locale")
+
+    private String m_locale;
+    
+    @XmlValue
+    private String m_content;
+
+    public Label() {
+    }
+    
+    public Label(String locale, String content) {
+        m_locale = locale;
+        m_content = content;
+    }
+
+    public String getLocale() {
+        return m_locale;
+    }
+
+    public void setLocale(String locale) {
+        m_locale = locale;
+    }
+    
+    public String getContent() {
+        return m_content;
+    }
+
+    public void setContent(String content) {
+        m_content = content;
+    }
+}

Added: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/PasswordPolicy.java
==============================================================================
--- (empty file)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/bean/PasswordPolicy.java
      Tue Mar 27 12:22:42 2012
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2010, 2011 The Amdatu Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.amdatu.auth.useradmin.rest.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * This bean represents the current password policy, which is configurable 
using 
+ * OSGi config admin. It contains all properties of the policy but also 
provides 
+ * a language specific description of the policy (used by clients to describe 
the
+ * policy)
+ * 
+ * @author <a href="mailto:[email protected]";>Amdatu Project 
Team</a>
+ */
+@SuppressWarnings("restriction")
+@XmlRootElement(name = "passwordpolicy")
+@XmlAccessorType(XmlAccessType.PUBLIC_MEMBER)
+public class PasswordPolicy {
+    private int m_minimumlength;
+    private int m_minimumdigits;
+    private int m_minimumspecialcharacters;
+    private boolean m_requiremixedcase;
+    private List<Label> m_description;
+
+    public int getMinimumlength() {
+        return m_minimumlength;
+    }
+
+    public void setMinimumlength(int minimumlength) {
+        m_minimumlength = minimumlength;
+    }
+
+    public int getMinimumdigits() {
+        return m_minimumdigits;
+    }
+
+    public void setMinimumdigits(int minimumdigits) {
+        m_minimumdigits = minimumdigits;
+    }
+
+    public int getMinimumspecialcharacters() {
+        return m_minimumspecialcharacters;
+    }
+
+    public void setMinimumspecialcharacters(int minimumspecialcharacters) {
+        m_minimumspecialcharacters = minimumspecialcharacters;
+    }
+
+    public boolean isRequiremixedcase() {
+        return m_requiremixedcase;
+    }
+
+    public void setRequiremixedcase(boolean requiremixedcase) {
+        m_requiremixedcase = requiremixedcase;
+    }
+
+    @XmlElementWrapper(name = "description")
+    @XmlElement(name = "label")
+    public List<Label> getDescription() {
+        return m_description;
+    }
+
+    public void setDescription(List<Label> titles) {
+        m_description = titles;
+    }
+
+    public void addDescription(Label description) {
+        if (m_description == null) {
+            m_description = new ArrayList<Label>();
+        }
+        m_description.add(description);
+    }
+}

Modified: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/osgi/Activator.java
==============================================================================
--- 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/osgi/Activator.java
   (original)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/osgi/Activator.java
   Tue Mar 27 12:22:42 2012
@@ -23,9 +23,11 @@
 import org.amdatu.libraries.utilities.osgi.ServiceDependentActivator;
 import org.amdatu.web.rest.jaxrs.JaxRsSpi;
 import org.amdatu.web.rest.jaxrs.RESTService;
+
 import org.apache.felix.dm.DependencyManager;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
 import org.osgi.service.log.LogService;
 import org.osgi.service.useradmin.UserAdmin;
 
@@ -50,10 +52,11 @@
 
         // Create the users resource service and register it as REST service
         manager.add(createAdapterService(UserAdmin.class, filter)
-            .setInterface(RESTService.class.getName(), null)
+            .setInterface(new String[]{ManagedService.class.getName(), 
RESTService.class.getName()}, null)
             .setImplementation(UsersResource.class)
             
.add(createServiceDependency().setService(LogService.class).setRequired(true))
-            
.add(createServiceDependency().setService(TenantManagementService.class).setRequired(true)));
+            
.add(createServiceDependency().setService(TenantManagementService.class).setRequired(true))
+            .add(createConfigurationDependency().setPid(UsersResource.PID)));
 
         // Create the groups resource service and register it as REST service
         manager.add(createAdapterService(UserAdmin.class, filter)

Modified: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/ResourceBase.java
==============================================================================
--- 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/ResourceBase.java
     (original)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/ResourceBase.java
     Tue Mar 27 12:22:42 2012
@@ -15,6 +15,17 @@
  */
 package org.amdatu.auth.useradmin.rest.service;
 
+import org.amdatu.auth.tokenprovider.InvalidTokenException;
+import org.amdatu.auth.tokenprovider.TokenProvider;
+import org.amdatu.auth.tokenprovider.TokenProviderException;
+import org.amdatu.auth.useradmin.rest.bean.RoleBean;
+import org.amdatu.auth.useradmin.rest.bean.SearchResultBean;
+import org.amdatu.core.tenant.Tenant;
+import org.amdatu.core.tenant.TenantException;
+import org.amdatu.core.tenant.TenantManagementService;
+import org.amdatu.libraries.utilities.rest.AtomSyndicationLink;
+import org.amdatu.web.rest.jaxrs.RESTService;
+
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -27,17 +38,8 @@
 import javax.ws.rs.core.CacheControl;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 
-import org.amdatu.auth.tokenprovider.InvalidTokenException;
-import org.amdatu.auth.tokenprovider.TokenProvider;
-import org.amdatu.auth.tokenprovider.TokenProviderException;
-import org.amdatu.auth.useradmin.rest.bean.RoleBean;
-import org.amdatu.auth.useradmin.rest.bean.SearchResultBean;
-import org.amdatu.core.tenant.Tenant;
-import org.amdatu.core.tenant.TenantException;
-import org.amdatu.core.tenant.TenantManagementService;
-import org.amdatu.libraries.utilities.rest.AtomSyndicationLink;
-import org.amdatu.web.rest.jaxrs.RESTService;
 import org.apache.felix.dm.Component;
 import org.apache.felix.dm.DependencyManager;
 import org.apache.felix.dm.ServiceDependency;
@@ -292,7 +294,7 @@
             return buildNotModified("Role to remove not found");
         }
     }
-
+   
     protected Response buildOK() {
         return Response.ok().cacheControl(NO_CACHE_CONTROL).build();
     }
@@ -312,6 +314,16 @@
 
     protected Response buildServerError() {
         return Response.serverError().cacheControl(NO_CACHE_CONTROL).build();
+    }
+    
+    // Returns a 200 - OK
+    protected Response get200(Object entity) {
+        return 
Response.status(Status.OK).entity(entity).cacheControl(NO_CACHE_CONTROL).build();
+    }
+    
+    // Returns a 400 - BAD REQUEST
+    protected Response get400() {
+        return 
Response.status(Status.BAD_REQUEST).cacheControl(NO_CACHE_CONTROL).build();
     }
 
     class Paging {
@@ -469,5 +481,9 @@
 
     protected UserAdmin getUserAdmin() {
         return m_userAdmin;
+    }
+    
+    protected LogService getLogService() {
+        return m_logService;
     }
 }

Modified: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/UsersResource.java
==============================================================================
--- 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/UsersResource.java
    (original)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/service/UsersResource.java
    Tue Mar 27 12:22:42 2012
@@ -15,6 +15,14 @@
  */
 package org.amdatu.auth.useradmin.rest.service;
 
+import org.amdatu.auth.useradmin.rest.bean.Label;
+import org.amdatu.auth.useradmin.rest.bean.PasswordPolicy;
+import org.amdatu.auth.useradmin.rest.util.ConfigurationUtil;
+import org.amdatu.auth.useradmin.rest.util.ConfigurationUtil.ConfigProperty;
+import org.amdatu.core.tenant.Tenant;
+
+import java.util.Dictionary;
+
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
@@ -31,8 +39,10 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
-import org.amdatu.core.tenant.Tenant;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.felix.dm.Component;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
 import org.osgi.service.useradmin.Role;
 
 /**
@@ -46,18 +56,55 @@
  * 
  * @author ivol
  */
+@SuppressWarnings("rawtypes")
 @Path("users")
-public class UsersResource extends ResourceBase {
+public class UsersResource extends ResourceBase implements ManagedService {
+    // Configuration PID and property keys
+    public final static String PID = "org.amdatu.auth.useradmin.rest";
+    private final static String PASSWORD_MIN_LENGTH = 
"password.minimum_length";
+    private final static String PASSWORD_REQ_MIXED_CASE = 
"password.require_mixed_case";
+    private final static String PASSWORD_MIN_DIGITS = 
"password.minimum_digits";
+    private final static String PASSWORD_MIN_SPEC_CHAR = 
"password.minimum_special_characters";
+    private static ConfigProperty[] CONFIG_PROPERTIES =
+        new ConfigProperty[] {
+            new ConfigProperty(PASSWORD_MIN_LENGTH, Integer.class, 6),
+            new ConfigProperty(PASSWORD_REQ_MIXED_CASE, Boolean.class, false),
+            new ConfigProperty(PASSWORD_MIN_DIGITS, Integer.class, 0),
+            new ConfigProperty(PASSWORD_MIN_SPEC_CHAR, Integer.class, 0)};
+
+    private final static String PASSWORD_CREDENTIAL_KEY = "password";
+
+    private Dictionary m_properties;
+    private ConfigurationUtil m_configUtil;
+
     // Service dependencies injected by the dependency manager
     private volatile Component m_component;
 
+    public void updated(Dictionary properties) throws ConfigurationException {
+        if (properties == null) {
+            // This is just initialization of the configuration
+            m_properties = properties;
+        }
+        else {
+            m_properties = properties;
+            if (m_configUtil != null) {
+                m_configUtil.init(CONFIG_PROPERTIES, properties);
+            }
+        }
+    }
+
+    public void start() {
+        m_configUtil = new ConfigurationUtil(getLogService());
+        m_configUtil.init(CONFIG_PROPERTIES, m_properties);
+    }
+
     /**
      * This method can be used to check the availability of the Users 
management service.
      * 
      * @return The text "UserAdmin Users management service online"
      */
     @GET
-    @Produces({ MediaType.TEXT_PLAIN })
+    @Produces({MediaType.TEXT_PLAIN})
     @Path("status")
     public String status() {
         String tenantId = (String) 
m_component.getServiceProperties().get(Tenant.TENANT_ID_SERVICEPROPERTY);
@@ -66,6 +113,52 @@
     }
 
     /**
+     * This method can be used to return the password policy.
+     * 
+     * @return <ul>
+     *         <li>200 (OK) : The password policy is returned.</li>
+     *         <li>401 (UNAUTHORIZED) : The user is not authenticated, the 
password policy can only
+     *         be retrieved by authenticated users.</li>
+     *         </ul>
+     */
+    @GET
+    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
+    @Path("passwordpolicy")
+    public Response getPasswordPolicy(@Context final HttpServletRequest 
request) {
+        if (!isAuthorized(request)) {
+            return Response.status(Response.Status.UNAUTHORIZED).build();
+        }
+
+        PasswordPolicy policy = new PasswordPolicy();
+        policy.setMinimumdigits(m_configUtil.get(PASSWORD_MIN_DIGITS, 
Integer.class));
+        policy.setMinimumlength(m_configUtil.get(PASSWORD_MIN_LENGTH, 
Integer.class));
+        
policy.setMinimumspecialcharacters(m_configUtil.get(PASSWORD_MIN_SPEC_CHAR, 
Integer.class));
+        policy.setRequiremixedcase(m_configUtil.get(PASSWORD_REQ_MIXED_CASE, 
Boolean.class));
+
+        // Append en_US description
+        String descr = "";
+        if (m_configUtil.get(PASSWORD_MIN_LENGTH, Integer.class) > 0) {
+            descr += "Minimum length: " + 
m_configUtil.get(PASSWORD_MIN_LENGTH, Integer.class) + ". ";
+        }
+        if (m_configUtil.get(PASSWORD_REQ_MIXED_CASE, Boolean.class)) {
+            descr +=
+                "Password must contain both lowercase and uppercase 
characters. ";
+        }
+        if (m_configUtil.get(PASSWORD_MIN_DIGITS, Integer.class) > 0) {
+            descr += "Minimum amount of digits: " + 
m_configUtil.get(PASSWORD_MIN_DIGITS, Integer.class) + ". ";
+        }
+        if (m_configUtil.get(PASSWORD_MIN_SPEC_CHAR, Integer.class) > 0) {
+            descr +=
+                "Minimum amount of non-alphanumeric characters: "
+                    + m_configUtil.get(PASSWORD_MIN_SPEC_CHAR, Integer.class) 
+ ". ";
+        }
+        Label label = new Label("en_US", descr);
+        policy.addDescription(label);
+
+        return get200(policy);
+    }
+
+    /**
      * Returns all users that match the specified filter options. This method 
can be invoked by making the following
      * REST call:<code><pre>
      * GET 
/rest/users?filter={filter}&sortOrder={sortOrder}&startIndex={startIndex}&maxResults={maxResults}
@@ -103,7 +196,7 @@
      *         </pre></code> a 500 response in case any exception occurred.
      */
     @GET
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Produces({MediaType.APPLICATION_JSON})
     public Response getUsers(
         @QueryParam("filter") final String filter,
         @QueryParam("sortOrder") final String sortOrder,
@@ -134,7 +227,7 @@
      */
     @GET
     @Path("{name}")
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Produces({MediaType.APPLICATION_JSON})
     public Response getUser(@PathParam("name") final String name, @Context 
final HttpServletRequest request) {
         if (!isAuthorized(request)) {
             return Response.status(Response.Status.UNAUTHORIZED).build();
@@ -156,7 +249,7 @@
      */
     @PUT
     @Path("{name}")
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Produces({MediaType.APPLICATION_JSON})
     public Response createUser(@PathParam("name") final String name, @Context 
final HttpServletRequest request) {
         if (!isAuthorized(request)) {
             return Response.status(Response.Status.UNAUTHORIZED).build();
@@ -176,18 +269,26 @@
      *        The key of the credential to set
      * @param value
      *        The value of the credential to set (this should be a posted form 
field named 'value')
-     * @return a 400 response in case the credential was updated.<br/>
+     * @return a 200 response in case the credential was updated.<br/>
+     *         a 400 in case the password violates the password policy
      *         a 404 response in case the user with the specified name does 
not exist.
      */
     @POST
     @Path("{name}/credentials/{key}")
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Produces({MediaType.APPLICATION_JSON})
     public Response setCredential(@PathParam("name") final String name,
         @PathParam("key") final String key, @FormParam("value") final String 
value,
         @Context final HttpServletRequest request) {
         if (!isAuthorized(request)) {
             return Response.status(Response.Status.UNAUTHORIZED).build();
         }
+
+        // Verify that the password value matches our constraints
+        if (PASSWORD_CREDENTIAL_KEY.equals(key)) {
+            if (!validatePasswordStrength(value)) {
+                return get400();
+            }
+        }
         return setCredential(name, key, value, Role.USER);
     }
 
@@ -208,8 +309,8 @@
      */
     @PUT
     @Path("{name}/properties/{key}")
-    @Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Consumes({MediaType.APPLICATION_FORM_URLENCODED})
+    @Produces({MediaType.APPLICATION_JSON})
     public Response setProperty(@PathParam("name") final String name, 
@PathParam("key") final String key,
         @FormParam("value") final String value, @Context final 
HttpServletRequest request) {
         if (!isAuthorized(request)) {
@@ -231,7 +332,7 @@
      */
     @DELETE
     @Path("{name}")
-    @Produces({ MediaType.APPLICATION_JSON })
+    @Produces({MediaType.APPLICATION_JSON})
     public Response removeUser(@PathParam("name") final String name, @Context 
final HttpServletRequest request) {
         if (!isAuthorized(request)) {
             return Response.status(Response.Status.UNAUTHORIZED).build();
@@ -243,4 +344,49 @@
     protected String getBaseUrl(final String contextPath) {
         return contextPath + "/rest/users";
     }
+
+    private boolean validatePasswordStrength(String password) {
+        // Verify password length
+        int minLen = m_configUtil.get(PASSWORD_MIN_LENGTH, Integer.class);
+        if (password.length() < minLen) {
+            return false;
+        }
+
+        // Verify mixed case
+        if (m_configUtil.get(PASSWORD_REQ_MIXED_CASE, Boolean.class)) {
+            if (password.toLowerCase().equals(password) || 
password.toUpperCase().equals(password)) {
+                return false;
+            }
+        }
+
+        // Verify minimum amount of digits
+        int minDigits = m_configUtil.get(PASSWORD_MIN_DIGITS, Integer.class);
+        if (minDigits > 0) {
+            int count = 0;
+            for (int i = 0; i < password.length(); i++) {
+                if (StringUtils.isNumeric(password.subSequence(i, i + 1))) {
+                    count++;
+                }
+            }
+            if (count < minDigits) {
+                return false;
+            }
+        }
+
+        // Verify minimum amount of special characters
+        int minSpecialCharacters = m_configUtil.get(PASSWORD_MIN_SPEC_CHAR, 
Integer.class);
+        if (minSpecialCharacters > 0) {
+            int count = 0;
+            for (int i = 0; i < password.length(); i++) {
+                if (!StringUtils.isAlphanumeric(password.subSequence(i, i + 
1))) {
+                    count++;
+                }
+            }
+            if (count < minSpecialCharacters) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }

Added: 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/util/ConfigurationUtil.java
==============================================================================
--- (empty file)
+++ 
trunk/amdatu-auth/useradmin-rest/src/main/java/org/amdatu/auth/useradmin/rest/util/ConfigurationUtil.java
   Tue Mar 27 12:22:42 2012
@@ -0,0 +1,154 @@
+package org.amdatu.auth.useradmin.rest.util;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+import org.osgi.service.log.LogService;
+
+/**
+ * This class provides a handy utility method for handling configuration 
properties managed by OSGi
+ * config admin. It adds support for providing default values in case 
configuration properties are
+ * not available in OSGi config admin (useful for backwards compatibility) and 
provides type-safe
+ * casting the properties to required types.
+ * This utility class can be reused.
+ * 
+ * @author <a href="mailto:[email protected]";>Amdatu Project 
Team</a>
+ */
+public class ConfigurationUtil {
+    private LogService m_logService;
+    private Dictionary<?, ?> m_config = null;
+    private Dictionary<String, Object> m_values = new Hashtable<String, 
Object>();
+
+    public static class ConfigProperty {
+        private String m_name;
+        private Class<?> m_type;
+        private Object m_defaultValue;
+
+        public ConfigProperty(final String aName, final Class<?> aType, final 
Object aDefaultValue) {
+            m_name = aName;
+            m_type = aType;
+            m_defaultValue = aDefaultValue;
+        }
+
+        public String getName() {
+            return m_name;
+        }
+
+        public Class<?> getType() {
+            return m_type;
+        }
+
+        public Object getDefaultValue() {
+            return m_defaultValue;
+        }
+    }
+
+    public ConfigurationUtil(final LogService logService) {
+        m_logService = logService;
+    }
+
+    public void init(final ConfigProperty[] configProperties, final 
Dictionary<?, ?> config) {
+        m_config = config;
+        for (ConfigProperty property : configProperties) {
+            if (property.getType().isAssignableFrom(String.class)) {
+                m_values.put(property.getName(), 
getStringProperty(property.getName(),
+                    (String) property.getDefaultValue()));
+            }
+            else if (property.getType().isAssignableFrom(Integer.class)) {
+                m_values.put(property.getName(), 
getIntProperty(property.getName(),
+                    (Integer) property.getDefaultValue()));
+            }
+            else if (property.getType().isAssignableFrom(Long.class)) {
+                m_values.put(property.getName(), 
getLongProperty(property.getName(),
+                    (Long) property.getDefaultValue()));
+            }
+            else if (property.getType().isAssignableFrom(Boolean.class)) {
+                m_values.put(property.getName(), 
getBooleanProperty(property.getName(),
+                    (Boolean) property.getDefaultValue()));
+            }
+        }
+
+        // Log the config properties
+        Enumeration<String> keys = m_values.keys();
+        m_logService.log(LogService.LOG_DEBUG, "Starting Cassandra Client with 
configuration: ");
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            m_logService.log(LogService.LOG_DEBUG, "  " + key + " = " + 
m_values.get(key).toString());
+        }
+    }
+
+    /**
+     * Returns a property from the configuration.
+     * 
+     * @param key The key of the property to retrieve.
+     * @param class The type of the property value to retrieve
+     * @return The value of this property as type T
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T get(final String key, final Class<T> clazz) {
+        Object val = m_values.get(key);
+        if (val != null && val.getClass().isAssignableFrom(clazz)) {
+            return (T) val;
+        }
+        m_logService.log(LogService.LOG_ERROR, "Property '" + key + "' 
retrieved as '" + clazz.toString()
+            + "', but its value is of type '" + val.getClass() + "'. Returning 
null.");
+        return null;
+    }
+
+    private String getStringProperty(final String key, final String 
defaultValue) {
+        Object value = m_config.get(key);
+        if (value == null) {
+            String msg = "No value set for property '" + key + "', switching 
to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_INFO, msg);
+            return defaultValue;
+        }
+        return value.toString();
+    }
+
+    private int getIntProperty(final String key, final int defaultValue) {
+        Object value = m_config.get(key);
+        if (value == null) {
+            String msg = "No value set for property '" + key + "', switching 
to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_INFO, msg);
+            return defaultValue;
+        }
+        try {
+            return Integer.parseInt(value.toString().trim());
+        }
+        catch (NumberFormatException e) {
+            String msg = "Invalid value set for property '" + key + "' ('" + 
value.toString()
+                + "'), switching to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_ERROR, msg);
+            return defaultValue;
+        }
+    }
+
+    private long getLongProperty(final String key, final long defaultValue) {
+        Object value = m_config.get(key);
+        if (value == null) {
+            String msg = "No value set for property '" + key + "', switching 
to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_INFO, msg);
+            return defaultValue;
+        }
+        try {
+            return Long.parseLong(value.toString().trim());
+        }
+        catch (NumberFormatException e) {
+            String msg = "Invalid value set for property '" + key + "' ('" + 
value.toString()
+                + "'), switching to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_ERROR, msg);
+            return defaultValue;
+        }
+    }
+
+    private boolean getBooleanProperty(final String key, final boolean 
defaultValue) {
+        Object value = m_config.get(key);
+        if (value == null) {
+            String msg = "No value set for property '" + key + "', switching 
to default value '" + defaultValue + "'";
+            m_logService.log(LogService.LOG_INFO, msg);
+            return defaultValue;
+        }
+        return "true".equalsIgnoreCase(value.toString()) || 
"1".equals(value.toString());
+    }
+}
_______________________________________________
Amdatu-commits mailing list
[email protected]
http://lists.amdatu.org/mailman/listinfo/amdatu-commits

Reply via email to