Improve tokens
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 2 Mar 2020 10:23:22 +0000 (11:23 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 2 Mar 2020 10:23:22 +0000 (11:23 +0100)
org.argeo.cms/src/org/argeo/cms/CmsUserManager.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.cms/src/org/argeo/cms/integration/CmsTokenServlet.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/integration/TokenDescriptor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java
org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java

index 39d4be622f9a09219c633e3dea94b5193b789e50..1dafe7c810636308b7b9482b69a35aea44f51807 100644 (file)
@@ -1,5 +1,6 @@
 package org.argeo.cms;
 
+import java.time.ZonedDateTime;
 import java.util.List;
 import java.util.Set;
 
@@ -73,6 +74,8 @@ public interface CmsUserManager {
 
        void addAuthToken(String userDn, String token, Integer hours, String... roles);
 
+       void addAuthToken(String userDn, String token, ZonedDateTime expiryDate, String... roles);
+
        void expireAuthToken(String token);
 
        void expireAuthTokens(Subject subject);
index 6a3ac97dfa76dfac6bd955edee1ef256790cdef2..692d214bcccc23ce5fe51303ef9e66336bccc3a4 100644 (file)
@@ -354,7 +354,9 @@ public class UserAdminLoginModule implements LoginModule {
 //                             return null;
 //                     }
 //             }
-               Authorization auth = userAdmin.getAuthorization(tokenGroup);
+               String userDn = TokenUtils.userDn(tokenGroup);
+               User user = (User) userAdmin.getRole(userDn);
+               Authorization auth = userAdmin.getAuthorization(user);
                return auth;
        }
 }
diff --git a/org.argeo.cms/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.cms/src/org/argeo/cms/integration/CmsTokenServlet.java
new file mode 100644 (file)
index 0000000..b2e5a7e
--- /dev/null
@@ -0,0 +1,114 @@
+package org.argeo.cms.integration;
+
+import java.io.IOException;
+import java.time.ZonedDateTime;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.cms.CmsUserManager;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.argeo.naming.NamingUtils;
+import org.argeo.node.NodeConstants;
+import org.osgi.service.useradmin.Authorization;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/** Provides access to tokens. */
+public class CmsTokenServlet extends HttpServlet {
+       private static final long serialVersionUID = 302918711430864140L;
+
+       public final static String PARAM_EXPIRY_DATE = "expiryDate";
+       public final static String PARAM_TOKEN = "token";
+
+       private final static int DEFAULT_HOURS = 24;
+
+       private CmsUserManager userManager;
+       private ObjectMapper objectMapper = new ObjectMapper();
+
+       @Override
+       protected void doPost(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               LoginContext lc = null;
+               try {
+                       lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response) {
+                               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                       for (Callback callback : callbacks) {
+                                               if (callback instanceof HttpRequestCallback) {
+                                                       ((HttpRequestCallback) callback).setRequest(request);
+                                                       ((HttpRequestCallback) callback).setResponse(response);
+                                               }
+                                       }
+                               }
+                       });
+                       lc.login();
+               } catch (LoginException e) {
+                       // ignore
+               }
+
+               try {
+                       Subject subject = lc.getSubject();
+                       Authorization authorization = extractFrom(subject.getPrivateCredentials(Authorization.class));
+                       String token = UUID.randomUUID().toString();
+                       String expiryDateStr = request.getParameter(PARAM_EXPIRY_DATE);
+                       ZonedDateTime expiryDate;
+                       if (expiryDateStr != null) {
+                               expiryDate = NamingUtils.ldapDateToZonedDateTime(expiryDateStr);
+                       } else {
+                               expiryDate = ZonedDateTime.now().plusHours(DEFAULT_HOURS);
+                               expiryDateStr = NamingUtils.instantToLdapDate(expiryDate);
+                       }
+                       userManager.addAuthToken(authorization.getName(), token, expiryDate);
+
+                       TokenDescriptor tokenDescriptor = new TokenDescriptor();
+                       tokenDescriptor.setUsername(authorization.getName());
+                       tokenDescriptor.setToken(token);
+                       tokenDescriptor.setExpiryDate(expiryDateStr);
+//                     tokenDescriptor.setRoles(Collections.unmodifiableSortedSet(new TreeSet<>(Arrays.asList(roles))));
+
+                       response.setContentType("application/json");
+                       JsonGenerator jg = objectMapper.getFactory().createGenerator(response.getWriter());
+                       jg.writeObject(tokenDescriptor);
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(objectMapper, response);
+               }
+       }
+
+       @Override
+       protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               // temporarily wrap POST for ease of testing
+               doPost(req, resp);
+       }
+
+       @Override
+       protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+               try {
+                       String token = req.getParameter(PARAM_TOKEN);
+                       userManager.expireAuthToken(token);
+               } catch (Exception e) {
+                       new CmsExceptionsChain(e).writeAsJson(objectMapper, resp);
+               }
+       }
+
+       protected <T> T extractFrom(Set<T> creds) {
+               if (creds.size() > 0)
+                       return creds.iterator().next();
+               else
+                       return null;
+       }
+
+       public void setUserManager(CmsUserManager userManager) {
+               this.userManager = userManager;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/integration/TokenDescriptor.java b/org.argeo.cms/src/org/argeo/cms/integration/TokenDescriptor.java
new file mode 100644 (file)
index 0000000..1541b4f
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.integration;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** A serializable descriptor of a token. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class TokenDescriptor implements Serializable {
+       private static final long serialVersionUID = -6607393871416803324L;
+
+       private String token;
+       private String username;
+       private String expiryDate;
+//     private Set<String> roles;
+
+       public String getToken() {
+               return token;
+       }
+
+       public void setToken(String token) {
+               this.token = token;
+       }
+
+       public String getUsername() {
+               return username;
+       }
+
+       public void setUsername(String username) {
+               this.username = username;
+       }
+
+//     public Set<String> getRoles() {
+//             return roles;
+//     }
+//
+//     public void setRoles(Set<String> roles) {
+//             this.roles = roles;
+//     }
+
+       public String getExpiryDate() {
+               return expiryDate;
+       }
+
+       public void setExpiryDate(String expiryDate) {
+               this.expiryDate = expiryDate;
+       }
+
+}
index 109a0d4066e65561a76d4801f3ed035989a685ff..869dbb23c9a2e76989091de5432563a56687ac8e 100644 (file)
@@ -36,7 +36,6 @@ import org.argeo.node.NodeConstants;
 import org.argeo.osgi.useradmin.TokenUtils;
 import org.argeo.osgi.useradmin.UserAdminConf;
 import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.framework.ServiceReference;
 import org.osgi.service.useradmin.Authorization;
 import org.osgi.service.useradmin.Group;
 import org.osgi.service.useradmin.Role;
@@ -59,8 +58,6 @@ public class CmsUserManagerImpl implements CmsUserManager {
        private final static Log log = LogFactory.getLog(CmsUserManagerImpl.class);
 
        private UserAdmin userAdmin;
-       @Deprecated
-       private ServiceReference<UserAdmin> userAdminServiceReference;
        private Map<String, String> serviceProperties;
        private UserTransaction userTransaction;
 
@@ -206,8 +203,7 @@ public class CmsUserManagerImpl implements CmsUserManager {
 
        public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
                Map<String, String> dns = new HashMap<String, String>();
-               String[] propertyKeys = userAdminServiceReference != null ? userAdminServiceReference.getPropertyKeys()
-                               : serviceProperties.keySet().toArray(new String[serviceProperties.size()]);
+               String[] propertyKeys = serviceProperties.keySet().toArray(new String[serviceProperties.size()]);
                for (String uri : propertyKeys) {
                        if (!uri.startsWith("/"))
                                continue;
@@ -374,25 +370,31 @@ public class CmsUserManagerImpl implements CmsUserManager {
 
        @Override
        public void addAuthToken(String userDn, String token, Integer hours, String... roles) {
+               addAuthToken(userDn, token, ZonedDateTime.now().plusHours(hours), roles);
+       }
+
+       @Override
+       public void addAuthToken(String userDn, String token, ZonedDateTime expiryDate, String... roles) {
                try {
                        userTransaction.begin();
                        User user = (User) userAdmin.getRole(userDn);
                        String tokenDn = cn + "=" + token + "," + NodeConstants.TOKENS_BASEDN;
                        Group tokenGroup = (Group) userAdmin.createRole(tokenDn, Role.GROUP);
-                       for (String role : roles) {
-                               Role r = userAdmin.getRole(role);
-                               if (r != null)
-                                       tokenGroup.addMember(r);
-                               else {
-                                       if (!role.equals(NodeConstants.ROLE_USER)) {
-                                               throw new IllegalStateException(
-                                                               "Cannot add role " + role + " to token " + token + " for " + userDn);
+                       if (roles != null)
+                               for (String role : roles) {
+                                       Role r = userAdmin.getRole(role);
+                                       if (r != null)
+                                               tokenGroup.addMember(r);
+                                       else {
+                                               if (!role.equals(NodeConstants.ROLE_USER)) {
+                                                       throw new IllegalStateException(
+                                                                       "Cannot add role " + role + " to token " + token + " for " + userDn);
+                                               }
                                        }
                                }
-                       }
                        tokenGroup.getProperties().put(owner.name(), user.getName());
-                       if (hours != null) {
-                               String ldapDate = NamingUtils.instantToLdapDate(ZonedDateTime.now().plusHours(hours));
+                       if (expiryDate != null) {
+                               String ldapDate = NamingUtils.instantToLdapDate(expiryDate);
                                tokenGroup.getProperties().put(description.name(), ldapDate);
                        }
                        userTransaction.commit();
index 2f3bdcefd02185dd6474e47893f79603dc7a99fa..6c97fa14132c6628129e63affccac7c1adb10faa 100644 (file)
@@ -113,7 +113,7 @@ public class CmsState implements NodeState {
                bc.registerService(RepositoryFactory.class, repositoryFactory, null);
 
                // Security
-               NodeUserAdmin userAdmin = new NodeUserAdmin(NodeConstants.ROLES_BASEDN);
+               NodeUserAdmin userAdmin = new NodeUserAdmin(NodeConstants.ROLES_BASEDN, NodeConstants.TOKENS_BASEDN);
                stopHooks.add(() -> userAdmin.destroy());
                bc.registerService(ManagedServiceFactory.class, userAdmin,
                                LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_USER_ADMIN_PID));
index f00c3c769f5ef00c6a4b37f0518bacb6f9d3b6bb..ba4ad836041ae8de7ce8093f14f3bb055a9dd0ec 100644 (file)
@@ -85,8 +85,8 @@ class NodeUserAdmin extends AggregatingUserAdmin implements ManagedServiceFactor
        private boolean singleUser = false;
        private boolean systemRolesAvailable = false;
 
-       public NodeUserAdmin(String systemRolesBaseDn) {
-               super(systemRolesBaseDn);
+       public NodeUserAdmin(String systemRolesBaseDn, String tokensBaseDn) {
+               super(systemRolesBaseDn, tokensBaseDn);
                tmTracker = new ServiceTracker<>(bc, TransactionManager.class, null);
                tmTracker.open();
        }
index cb93e825786d56f3c90edc20f7759743cb793d13..5a868ddb42b885afbad49925c247407ff3907182 100644 (file)
@@ -19,6 +19,7 @@ import java.util.List;
 import java.util.Map;
 
 public class NamingUtils {
+       /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */
        private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX")
                        .withZone(ZoneOffset.UTC);
 
@@ -31,6 +32,15 @@ public class NamingUtils {
                }
        }
 
+       /** @return null if not parseable */
+       public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) {
+               try {
+                       return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime();
+               } catch (DateTimeParseException e) {
+                       return null;
+               }
+       }
+
        public static Calendar ldapDateToCalendar(String ldapDate) {
                OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate);
                GregorianCalendar calendar = new GregorianCalendar();
index b09c8df5eee20cb633d3fbe602352202a4ac7ecf..75ca9ae884605a81ac07a219455030e5f5990172 100644 (file)
@@ -26,14 +26,20 @@ import org.osgi.service.useradmin.UserAdmin;
  */
 public class AggregatingUserAdmin implements UserAdmin {
        private final LdapName systemRolesBaseDn;
+       private final LdapName tokensBaseDn;
 
        // DAOs
        private AbstractUserDirectory systemRoles = null;
+       private AbstractUserDirectory tokens = null;
        private Map<LdapName, AbstractUserDirectory> businessRoles = new HashMap<LdapName, AbstractUserDirectory>();
 
-       public AggregatingUserAdmin(String systemRolesBaseDn) {
+       public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) {
                try {
                        this.systemRolesBaseDn = new LdapName(systemRolesBaseDn);
+                       if (tokensBaseDn != null)
+                               this.tokensBaseDn = new LdapName(tokensBaseDn);
+                       else
+                               this.tokensBaseDn = null;
                } catch (InvalidNameException e) {
                        throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e);
                }
@@ -130,6 +136,9 @@ public class AggregatingUserAdmin implements UserAdmin {
                if (isSystemRolesBaseDn(baseDn)) {
                        this.systemRoles = userDirectory;
                        systemRoles.setExternalRoles(this);
+               } else if (isTokensBaseDn(baseDn)) {
+                       this.tokens = userDirectory;
+                       tokens.setExternalRoles(this);
                } else {
                        if (businessRoles.containsKey(baseDn))
                                throw new UserDirectoryException("There is already a user admin for " + baseDn);
@@ -155,6 +164,8 @@ public class AggregatingUserAdmin implements UserAdmin {
        private UserAdmin findUserAdmin(LdapName name) {
                if (name.startsWith(systemRolesBaseDn))
                        return systemRoles;
+               if (tokensBaseDn != null && name.startsWith(tokensBaseDn))
+                       return tokens;
                List<UserAdmin> res = new ArrayList<UserAdmin>(1);
                for (LdapName baseDn : businessRoles.keySet()) {
                        if (name.startsWith(baseDn)) {
@@ -174,6 +185,10 @@ public class AggregatingUserAdmin implements UserAdmin {
                return baseDn.equals(systemRolesBaseDn);
        }
 
+       protected boolean isTokensBaseDn(LdapName baseDn) {
+               return tokensBaseDn != null && baseDn.equals(tokensBaseDn);
+       }
+
        protected Dictionary<String, Object> currentState() {
                Dictionary<String, Object> res = new Hashtable<String, Object>();
                // res.put(NodeConstants.CN, NodeConstants.DEFAULT);