You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2019/10/31 15:32:27 UTC

[sling-org-apache-sling-app-cms] branch master updated: Fixes SLING-8817 - Adding a User / Group console

This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-app-cms.git


The following commit(s) were added to refs/heads/master by this push:
     new f34cd6d  Fixes SLING-8817 - Adding a User / Group console
f34cd6d is described below

commit f34cd6d53c1c4389f8255fa85063f539044febd9
Author: Dan Klco <dk...@apache.org>
AuthorDate: Thu Oct 31 11:32:18 2019 -0400

    Fixes SLING-8817 - Adding a User / Group console
---
 .../org/apache/sling/cms/AuthorizableWrapper.java  |  98 ++++++++
 .../java/org/apache/sling/cms/CurrentUser.java     |  49 ----
 builder/src/main/provisioning/composum.txt         |   1 -
 .../sling/cms/core/internal/CommonUtils.java       |  48 ++++
 .../sling/cms/core/internal/SimplePrincipal.java   |  37 +++
 .../internal/models/AuthorizableWrapperImpl.java   | 161 ++++++++++++
 .../core/internal/models/CMSJobManagerImpl.java    |   7 +-
 .../cms/core/internal/models/CurrentUserImpl.java  |  91 -------
 .../operations/ChangePasswordOperation.java        |  77 ++++++
 .../internal/operations/CreateGroupOperation.java  |  83 +++++++
 .../internal/operations/CreateUserOperation.java   |  87 +++++++
 .../core/internal/operations/MembersOperation.java | 108 ++++++++
 .../internal/operations/MembershipOperation.java   | 106 ++++++++
 .../operations/TouchLastModifiedPostOperation.java |   2 +-
 .../internal/operations/UpdateStatusOperation.java |  80 ++++++
 .../cms/core/helpers/SlingCMSContextHelper.java    |  42 ----
 .../sling/cms/core/helpers/SlingCMSTestHelper.java | 158 ++++++++++++
 .../models/AuthorizableWrapperImplTest.java        | 271 +++++++++++++++++++++
 .../operations/ChangePasswordOperationTest.java    |  87 +++++++
 .../operations/CreateGroupOperationTest.java       | 103 ++++++++
 .../operations/CreateUserOperationTest.java        | 105 ++++++++
 .../internal/operations/MembersOperationTest.java  | 122 ++++++++++
 .../operations/MembershipOperationTest.java        | 122 ++++++++++
 .../operations/UpdateStatusOperationTest.java      | 110 +++++++++
 .../internal/servlets/DownloadFileServletTest.java |   4 +-
 core/src/test/resources/auth.json                  | 142 +++++++++++
 docs/admin-tools.md                                |   2 +-
 docs/img/users-groups.png                          | Bin 31634 -> 41752 bytes
 .../components/cms/staticnav/staticnav.jsp         |   8 +-
 .../components/editor/fields/auth/members.json     |   6 +
 .../auth/members/options.jsp}                      |  16 +-
 .../auth/members/values.jsp}                       |  16 +-
 .../components/editor/fields/auth/membership.json  |   6 +
 .../auth/membership/options.jsp}                   |  16 +-
 .../auth/membership/values.jsp}                    |  16 +-
 .../auth/status/status.jsp}                        |  30 ++-
 .../components/editor/scripts/localeOptions.jsp    |  10 +-
 .../libs/sling-cms/components/pages/base/nav.jsp   |  29 ++-
 .../libs/sling-cms/content/auth/group/create.json  |  35 +++
 .../libs/sling-cms/content/auth/group/members.json |  32 +++
 .../jcr_root/libs/sling-cms/content/auth/list.json | 229 +++++++++++++++++
 .../libs/sling-cms/content/auth/membership.json    |  32 +++
 .../libs/sling-cms/content/auth/newfolder.json     |  36 +++
 .../libs/sling-cms/content/auth/user/create.json   |  43 ++++
 .../libs/sling-cms/content/auth/user/password.json |  35 +++
 .../libs/sling-cms/content/auth/user/profile.json  |  41 ++++
 .../libs/sling-cms/content/auth/user/status.json   |  31 +++
 .../jcr_root/libs/sling-cms/content/start.json     |   5 +-
 ....internal.ResourceEditorAssociation-auth.config |  20 ++
 49 files changed, 2753 insertions(+), 242 deletions(-)

diff --git a/api/src/main/java/org/apache/sling/cms/AuthorizableWrapper.java b/api/src/main/java/org/apache/sling/cms/AuthorizableWrapper.java
new file mode 100644
index 0000000..417f420
--- /dev/null
+++ b/api/src/main/java/org/apache/sling/cms/AuthorizableWrapper.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms;
+
+import java.util.Iterator;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+
+/**
+ * A wrapper for working with JackRabbit Authorizables in JSPs and Sling Models
+ * from a Resource
+ */
+public interface AuthorizableWrapper {
+
+    /**
+     * Gets the JackRabbit Authorizable
+     * 
+     * @return a JackRabbit Authorizable
+     */
+    Authorizable getAuthorizable();
+
+    /**
+     * Gets the declared members of this authorizable. For Users this will return an
+     * empty iterator.
+     * 
+     * @return the declared members of this authorizable
+     */
+    Iterator<Authorizable> getDeclaredMembers();
+
+    /**
+     * Get the groups this authorizable is a member of
+     * 
+     * @return the direct membership
+     */
+    Iterator<Group> getDeclaredMembership();
+
+    /**
+     * Gets a collection of all of the groups this user belongs to including
+     * containing groups.
+     * 
+     * @return the groups the user belongs to
+     */
+    Iterator<String> getGroupNames();
+
+    /**
+     * Get the id of the current user.
+     * 
+     * @return the current user's ID
+     */
+    public String getId();
+
+    /**
+     * Gets the transitive members of this authorizable. For Users this will return
+     * an empty iterator.
+     * 
+     * @return the transitive members of this authorizable
+     */
+    Iterator<Authorizable> getMembers();
+
+    /**
+     * Gets the transitive membership of this authorizable
+     * 
+     * @return the transitive membership
+     */
+    Iterator<Group> getMembership();
+
+    /**
+     * Returns true if the authorizable is a user and is the admin user or is a
+     * member of the administrators group.
+     * 
+     * @return true if the user is a super user
+     */
+    public boolean isAdministrator();
+
+    /**
+     * Returns true if the authorizable is a member of the group
+     * 
+     * @param groupName the name of the group to check
+     * @return true if the authorizable is a member of the group
+     */
+    public boolean isMember(String groupName);
+
+}
diff --git a/api/src/main/java/org/apache/sling/cms/CurrentUser.java b/api/src/main/java/org/apache/sling/cms/CurrentUser.java
deleted file mode 100644
index 292e61b..0000000
--- a/api/src/main/java/org/apache/sling/cms/CurrentUser.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.sling.cms;
-
-import java.util.Collection;
-
-/**
- * Represents the current user, adaptable from a ResourceResolver.
- */
-public interface CurrentUser {
-
-    /**
-     * Gets a collection of all of the groups this user belongs to including
-     * containing groups.
-     * 
-     * @return the groups the user belongs to
-     */
-    public Collection<String> getGroups();
-
-    /**
-     * Get the id of the current user.
-     * 
-     * @return the current user's ID
-     */
-    public String getId();
-
-    /**
-     * Returns true if the user is a member of the group, is the admin user or is a
-     * member of the administrators group.
-     * 
-     * @param groupName the name of the group to check
-     * @return true if the use is a member of the group or is a super user
-     */
-    public boolean isMember(String groupName);
-}
diff --git a/builder/src/main/provisioning/composum.txt b/builder/src/main/provisioning/composum.txt
index 56e6a43..1d68a9a 100644
--- a/builder/src/main/provisioning/composum.txt
+++ b/builder/src/main/provisioning/composum.txt
@@ -24,7 +24,6 @@
   com.composum.sling.core/composum-sling-core-commons/${composum.nodes.version}
   com.composum.sling.core/composum-sling-core-console/${composum.nodes.version}
   com.composum.sling.core/composum-sling-core-jslibs/${composum.nodes.version}
-  com.composum.sling.core/composum-sling-user-management/${composum.nodes.version}
   com.composum.sling.core/composum-sling-package-manager/${composum.nodes.version}
   com.composum.sling.core.osgi/composum-sling-osgi-package-installer/${composum.nodes.version}
   org.apache.jackrabbit.vault/org.apache.jackrabbit.vault/3.2.8
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/CommonUtils.java b/core/src/main/java/org/apache/sling/cms/core/internal/CommonUtils.java
new file mode 100644
index 0000000..1f411d8
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/CommonUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal;
+
+import java.util.Optional;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CommonUtils {
+
+    private static final Logger log = LoggerFactory.getLogger(CommonUtils.class);
+
+    public static final UserManager getUserManager(ResourceResolver resolver) throws RepositoryException {
+        return Optional.ofNullable(resolver.adaptTo(Session.class)).map(session -> {
+            UserManager userManager = null;
+            if (session instanceof JackrabbitSession) {
+                try {
+                    userManager = ((JackrabbitSession) session).getUserManager();
+                } catch (RepositoryException e) {
+                    log.error("Failed to get user manager", e);
+                }
+
+            }
+            return userManager;
+        }).orElseThrow(() -> new RepositoryException("Failed to get user manager"));
+    }
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/SimplePrincipal.java b/core/src/main/java/org/apache/sling/cms/core/internal/SimplePrincipal.java
new file mode 100644
index 0000000..076e5dc
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/SimplePrincipal.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal;
+
+import java.security.Principal;
+
+/**
+ * Simple String-based principal
+ */
+public class SimplePrincipal implements Principal {
+
+    private final String name;
+
+    public SimplePrincipal(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImpl.java
new file mode 100644
index 0000000..a170163
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImpl.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.models;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.models.annotations.Model;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation of the AuthorizableWrapper Sling Model.
+ */
+@Model(adaptables = { Resource.class, ResourceResolver.class }, adapters = AuthorizableWrapper.class)
+public class AuthorizableWrapperImpl implements AuthorizableWrapper {
+
+    private static final Logger log = LoggerFactory.getLogger(AuthorizableWrapperImpl.class);
+    private final Authorizable authorizable;
+
+    public AuthorizableWrapperImpl(ResourceResolver resolver) throws RepositoryException {
+        authorizable = CommonUtils.getUserManager(resolver).getAuthorizable(resolver.getUserID());
+    }
+
+    public AuthorizableWrapperImpl(Resource resource)
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        this.authorizable = CommonUtils.getUserManager(resource.getResourceResolver())
+                .getAuthorizableByPath(resource.getPath());
+        if (authorizable == null) {
+            throw new RepositoryException("Failed to get authorizable from " + resource);
+        }
+    }
+
+    @Override
+    public Authorizable getAuthorizable() {
+        return authorizable;
+    }
+
+    @Override
+    public Iterator<Authorizable> getDeclaredMembers() {
+        try {
+            if (authorizable.isGroup()) {
+                return ((Group) authorizable).getDeclaredMembers();
+            } else {
+                List<Authorizable> empty = Collections.emptyList();
+                return empty.iterator();
+            }
+        } catch (RepositoryException e) {
+            log.error("Failed to get membership of authorizable: {}", authorizable, e);
+            return Collections.emptyIterator();
+        }
+    }
+
+    @Override
+    public Iterator<Group> getDeclaredMembership() {
+        try {
+            return authorizable.declaredMemberOf();
+        } catch (RepositoryException e) {
+            log.error("Failed to get membership of authorizable: {}", authorizable, e);
+            return Collections.emptyIterator();
+        }
+    }
+
+    @Override
+    public Iterator<String> getGroupNames() {
+        Iterable<Group> iterable = () -> getMembership();
+        return StreamSupport.stream(iterable.spliterator(), false).map(g -> {
+            try {
+                return g.getPrincipal().getName();
+            } catch (RepositoryException e) {
+                log.error("Failed to get name from group: {}", g, e);
+                return null;
+            }
+        }).iterator();
+    }
+
+    @Override
+    public String getId() {
+        try {
+            return authorizable.getID();
+        } catch (RepositoryException e) {
+            log.error("Failed to get ID from authorizable: {}", e);
+            return null;
+        }
+    }
+
+    @Override
+    public Iterator<Authorizable> getMembers() {
+        try {
+            if (authorizable.isGroup()) {
+                return ((Group) authorizable).getMembers();
+            } else {
+                List<Authorizable> empty = Collections.emptyList();
+                return empty.iterator();
+            }
+        } catch (RepositoryException e) {
+            log.error("Failed to get membership of authorizable: {}", authorizable, e);
+            return Collections.emptyIterator();
+        }
+    }
+
+    @Override
+    public Iterator<Group> getMembership() {
+        try {
+            return authorizable.memberOf();
+        } catch (RepositoryException e) {
+            log.error("Failed to get membership of authorizable: {}", authorizable, e);
+            return Collections.emptyIterator();
+        }
+    }
+
+    @Override
+    public boolean isAdministrator() {
+        try {
+            return !authorizable.isGroup() && ("admin".equals(authorizable.getID()) || isMember("administrators"));
+        } catch (RepositoryException e) {
+            log.error("Failed to check if authorizable is an administrator", e);
+            return false;
+        }
+    }
+
+    @Override
+    public boolean isMember(String groupName) {
+        Iterable<Group> iterable = () -> getMembership();
+        return StreamSupport.stream(iterable.spliterator(), false).anyMatch(g -> {
+            try {
+                return groupName.equals(g.getID());
+            } catch (RepositoryException e) {
+                log.error("Failed to get ID from authorizable: {}", g, e);
+                return false;
+            }
+        });
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/models/CMSJobManagerImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/models/CMSJobManagerImpl.java
index d7a46f0..9abcfef 100644
--- a/core/src/main/java/org/apache/sling/cms/core/internal/models/CMSJobManagerImpl.java
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/models/CMSJobManagerImpl.java
@@ -27,10 +27,10 @@ import java.util.stream.Stream;
 import org.apache.sling.api.SlingException;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.cms.AuthorizableWrapper;
 import org.apache.sling.cms.CMSConstants;
 import org.apache.sling.cms.CMSJobManager;
 import org.apache.sling.cms.ConfigurableJobExecutor;
-import org.apache.sling.cms.CurrentUser;
 import org.apache.sling.event.jobs.Job;
 import org.apache.sling.event.jobs.JobManager;
 import org.apache.sling.event.jobs.JobManager.QueryType;
@@ -58,8 +58,9 @@ public class CMSJobManagerImpl implements CMSJobManager {
     private SlingHttpServletRequest request;
 
     public CMSJobManagerImpl(SlingHttpServletRequest request) {
-        CurrentUser currentUser = request.getResourceResolver().adaptTo(CurrentUser.class);
-        if (currentUser == null || !currentUser.isMember(CMSConstants.GROUP_JOB_USERS)) {
+        AuthorizableWrapper currentUser = request.getResourceResolver().adaptTo(AuthorizableWrapper.class);
+        if (currentUser == null
+                || (!currentUser.isAdministrator() && !currentUser.isMember(CMSConstants.GROUP_JOB_USERS))) {
             throw new SlingException(
                     "User " + request.getResourceResolver().getUserID() + " is not a member of job-users", null);
         }
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/models/CurrentUserImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/models/CurrentUserImpl.java
deleted file mode 100644
index 4568822..0000000
--- a/core/src/main/java/org/apache/sling/cms/core/internal/models/CurrentUserImpl.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.sling.cms.core.internal.models;
-
-import java.util.Collection;
-import java.util.HashSet;
-
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-
-import org.apache.jackrabbit.api.JackrabbitSession;
-import org.apache.jackrabbit.api.security.user.User;
-import org.apache.jackrabbit.api.security.user.UserManager;
-import org.apache.sling.api.resource.ResourceResolver;
-import org.apache.sling.cms.CMSConstants;
-import org.apache.sling.cms.CurrentUser;
-import org.apache.sling.models.annotations.Model;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-@Model(adaptables = { ResourceResolver.class }, adapters = CurrentUser.class)
-public class CurrentUserImpl implements CurrentUser {
-
-    private ResourceResolver resolver;
-    private UserManager userManager;
-
-    private Collection<String> groupNames;
-
-    private static final Logger log = LoggerFactory.getLogger(CurrentUserImpl.class);
-
-    public CurrentUserImpl(ResourceResolver resolver) {
-        this.resolver = resolver;
-
-        try {
-            Session session = resolver.adaptTo(Session.class);
-            JackrabbitSession js = (JackrabbitSession) session;
-            if (js != null) {
-                userManager = js.getUserManager();
-            }
-        } catch (RepositoryException e) {
-            log.warn("Failed to get user manager", e);
-        }
-    }
-
-    @Override
-    public String getId() {
-        return resolver.getUserID();
-    }
-
-    @Override
-    public Collection<String> getGroups() {
-        if (groupNames == null) {
-            groupNames = new HashSet<>();
-            User user = null;
-            try {
-                user = (User) userManager.getAuthorizable(getId());
-                user.memberOf().forEachRemaining(g -> {
-                    try {
-                        groupNames.add(g.getID());
-                    } catch (RepositoryException e) {
-                        log.warn("Failed to get group name", e);
-                    }
-                });
-            } catch (RepositoryException re) {
-                log.warn("Failed to get user", re);
-            }
-        }
-        return groupNames;
-    }
-
-    @Override
-    public boolean isMember(String groupName) {
-        return CMSConstants.USER_ADMIN.equals(getId()) || getGroups().contains(CMSConstants.GROUP_ADMINISTRATORS)
-                || getGroups().contains(groupName);
-    }
-
-}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperation.java
new file mode 100644
index 0000000..533ebb5
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperation.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>ChangePasswordOperation</code> will change a user's password
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=changepassword")
+public class ChangePasswordOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(ChangePasswordOperation.class);
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            String password = request.getParameter(CreateUserOperation.PN_PASSWORD);
+
+            AuthorizableWrapper authWrapper = request.getResource().adaptTo(AuthorizableWrapper.class);
+
+            if (authWrapper.getAuthorizable().isGroup()) {
+                throw new RepositoryException("Authorizable is a group!");
+            }
+            User user = (User) authWrapper.getAuthorizable();
+
+            user.changePassword(password);
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.setPath(user.getPath());
+            response.onModified(user.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to change user password", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperation.java
new file mode 100644
index 0000000..42183d7
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperation.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.cms.core.internal.SimplePrincipal;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>CreateGroupOperation</code> will create a new group
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=creategroup")
+public class CreateGroupOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(CreateGroupOperation.class);
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            String name = request.getParameter(SlingPostConstants.RP_NODE_NAME);
+
+            ResourceResolver resolver = request.getResourceResolver();
+            UserManager userManager = CommonUtils.getUserManager(resolver);
+
+            if (userManager.getAuthorizable(new SimplePrincipal(name)) != null) {
+                throw new RepositoryException("Authorizable with id " + name + " already exists");
+            }
+            String intermediatePath = StringUtils.substringBeforeLast(request.getResource().getPath(), "/")
+                    .replaceAll("\\/home\\/groups\\/?", "");
+            Group group = userManager.createGroup(name, new SimplePrincipal(name), intermediatePath);
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.setPath(group.getPath());
+            response.onCreated(group.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to create group", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateUserOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateUserOperation.java
new file mode 100644
index 0000000..f03db3d
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/CreateUserOperation.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.cms.core.internal.SimplePrincipal;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>CreateUserOperation</code> will create a new user
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=createuser")
+public class CreateUserOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(CreateUserOperation.class);
+
+    public static final String PN_PASSWORD = ":password";
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            String name = request.getParameter(SlingPostConstants.RP_NODE_NAME);
+            String password = request.getParameter(PN_PASSWORD);
+
+            ResourceResolver resolver = request.getResourceResolver();
+            UserManager userManager = CommonUtils.getUserManager(resolver);
+
+            if (userManager.getAuthorizable(new SimplePrincipal(name)) != null) {
+                throw new RepositoryException("Authorizable with id " + name + " already exists");
+            }
+            String intermediatePath = StringUtils.substringBeforeLast(request.getResource().getPath(), "/")
+                    .replaceAll("\\/home\\/users\\/?", "");
+
+            User user = userManager.createUser(name, password, new SimplePrincipal(name), intermediatePath);
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.setPath(user.getPath());
+            response.onCreated(user.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to create user", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembersOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembersOperation.java
new file mode 100644
index 0000000..c21c661
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembersOperation.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>MembersOperation</code> will update the membership of a group.
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=members")
+public class MembersOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(MembersOperation.class);
+    public static final String PN_MEMBERS = ":members";
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            List<String> auths = new ArrayList<>();
+            Optional.ofNullable(request.getParameterValues(PN_MEMBERS)).ifPresent(p -> {
+                auths.addAll(Arrays.asList(p));
+            });
+
+            AuthorizableWrapper groupWrapper = request.getResource().adaptTo(AuthorizableWrapper.class);
+            if (!groupWrapper.getAuthorizable().isGroup()) {
+                throw new RepositoryException("Provided authorizable is not a group");
+            }
+            Group group = (Group) groupWrapper.getAuthorizable();
+            response.setPath(group.getPath());
+            changes.add(Modification.onModified(group.getPath()));
+
+            group.getDeclaredMembers().forEachRemaining(member -> {
+                try {
+                    if (!auths.contains(member.getPath())) {
+                        log.debug("Removing member {} from {}", member, group);
+                        group.removeMember(member);
+                        changes.add(Modification.onModified(member.getPath()));
+                    } else {
+                        auths.remove(member.getPath());
+                    }
+                } catch (RepositoryException e) {
+                    log.warn("Failed to remove members", e);
+                }
+            });
+
+            for (String path : auths) {
+                Resource resource = request.getResourceResolver().getResource(path);
+                if (resource == null) {
+                    throw new RepositoryException("Failed to resolve authorizable at " + path);
+                }
+                Authorizable authorizable = resource.adaptTo(AuthorizableWrapper.class).getAuthorizable();
+                group.addMember(authorizable);
+                changes.add(Modification.onModified(authorizable.getPath()));
+                log.debug("Adding member {} to {}", authorizable, group);
+            }
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.onModified(group.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to update members", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembershipOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembershipOperation.java
new file mode 100644
index 0000000..36e49c1
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/MembershipOperation.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>MembersOperation</code> will update the membership of a group.
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=membership")
+public class MembershipOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(MembershipOperation.class);
+    public static final String PN_MEMBERSHIP = ":membership";
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            List<String> groups = new ArrayList<>();
+            Optional.ofNullable(request.getParameterValues(PN_MEMBERSHIP)).ifPresent(p -> {
+                groups.addAll(Arrays.asList(p));
+            });
+            
+            AuthorizableWrapper authWrapper = request.getResource().adaptTo(AuthorizableWrapper.class);
+
+            Authorizable auth = authWrapper.getAuthorizable();
+            response.setPath(auth.getPath());
+            changes.add(Modification.onModified(auth.getPath()));
+
+            auth.declaredMemberOf().forEachRemaining(group -> {
+                try {
+                    if (!groups.contains(group.getPath())) {
+                        log.debug("Removing member {} from {}", auth, group);
+                        group.removeMember(auth);
+                        changes.add(Modification.onModified(group.getPath()));
+                    } else {
+                        groups.remove(group.getPath());
+                    }
+                } catch (RepositoryException e) {
+                    log.warn("Failed to remove members", e);
+                }
+            });
+
+            for (String path : groups) {
+                Resource resource = request.getResourceResolver().getResource(path);
+                if (resource == null) {
+                    throw new RepositoryException("Failed to resolve authorizable at " + path);
+                }
+                Group group = (Group) resource.adaptTo(AuthorizableWrapper.class).getAuthorizable();
+                group.addMember(auth);
+                changes.add(Modification.onModified(group.getPath()));
+                log.debug("Adding member {} to {}", auth, group);
+            }
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.onModified(auth.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to update membership", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/TouchLastModifiedPostOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/TouchLastModifiedPostOperation.java
index caf940e..7aabdb4 100644
--- a/core/src/main/java/org/apache/sling/cms/core/internal/operations/TouchLastModifiedPostOperation.java
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/TouchLastModifiedPostOperation.java
@@ -69,7 +69,7 @@ public class TouchLastModifiedPostOperation implements SlingPostProcessor {
         Set<String> parentPaths = new HashSet<>();
         List<Resource> resources = paths.stream().map(p -> request.getResourceResolver().getResource(p))
                 .map(CMSUtils::findPublishableParent).filter(p -> {
-                    if (parentPaths.contains(p.getPath())) {
+                    if (p == null || parentPaths.contains(p.getPath())) {
                         return false;
                     } else {
                         parentPaths.add(p.getPath());
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperation.java
new file mode 100644
index 0000000..8f91cf9
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperation.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.servlets.post.Modification;
+import org.apache.sling.servlets.post.PostOperation;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.jsoup.helper.StringUtil;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>UpdateStatusOperation</code> will update the status of a user.
+ */
+@Component(immediate = true, service = { PostOperation.class }, property = PostOperation.PROP_OPERATION_NAME
+        + "=updatestatus")
+public class UpdateStatusOperation implements PostOperation {
+
+    private static final Logger log = LoggerFactory.getLogger(UpdateStatusOperation.class);
+
+    public static final String PN_REASON = ":reason";
+
+    @Override
+    public void run(SlingHttpServletRequest request, PostResponse response, SlingPostProcessor[] processors) {
+        final List<Modification> changes = new ArrayList<>();
+        try {
+
+            String reason = request.getParameter(PN_REASON);
+
+            AuthorizableWrapper authWrapper = request.getResource().adaptTo(AuthorizableWrapper.class);
+
+            if (authWrapper.getAuthorizable().isGroup()) {
+                throw new RepositoryException("Authorizable is not a user");
+            }
+
+            User user = (User) authWrapper.getAuthorizable();
+            user.disable(StringUtil.isBlank(reason) ? null : reason);
+
+            // invoke processors
+            if (processors != null) {
+                for (SlingPostProcessor processor : processors) {
+                    processor.process(request, changes);
+                }
+            }
+
+            request.getResourceResolver().commit();
+
+            response.setPath(user.getPath());
+            response.onCreated(user.getPath());
+        } catch (Exception e) {
+            log.warn("Failed to update user status", e);
+            response.setError(e);
+        }
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.java b/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.java
deleted file mode 100644
index c77cdfa..0000000
--- a/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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.apache.sling.cms.core.helpers;
-
-import java.io.InputStream;
-
-import org.apache.sling.api.resource.Resource;
-import org.apache.sling.testing.mock.sling.junit.SlingContext;
-
-import com.google.common.base.Function;
-
-public class SlingCMSContextHelper {
-
-    public static final void initContext(SlingContext context) {
-        context.addModelsForPackage("org.apache.sling.cms.core.internal.models");
-        context.addModelsForPackage("org.apache.sling.cms.core.models");
-
-        context.load().json("/content.json", "/content");
-        context.load().binaryResource("/apache.png", "/content/apache/sling-apache-org/index/apache.png/jcr:content");
-
-        context.registerAdapter(Resource.class, InputStream.class, new Function<Resource, InputStream>() {
-            public InputStream apply(Resource input) {
-                return input.getValueMap().get("jcr:content/jcr:data", InputStream.class);
-            }
-        });
-
-    }
-}
diff --git a/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSTestHelper.java b/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSTestHelper.java
new file mode 100644
index 0000000..80476bf
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSTestHelper.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.helpers;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.cms.ResourceTree;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.mockito.Mockito;
+
+import com.google.common.base.Function;
+
+public class SlingCMSTestHelper {
+
+    public static final void initContext(SlingContext context) {
+        context.addModelsForPackage("org.apache.sling.cms.core.internal.models");
+        context.addModelsForPackage("org.apache.sling.cms.core.models");
+
+        context.load().json("/content.json", "/content");
+        context.load().binaryResource("/apache.png", "/content/apache/sling-apache-org/index/apache.png/jcr:content");
+
+        context.registerAdapter(Resource.class, InputStream.class, new Function<Resource, InputStream>() {
+            public InputStream apply(Resource input) {
+                return input.getValueMap().get("jcr:content/jcr:data", InputStream.class);
+            }
+        });
+    }
+
+    public static final Map<String, Authorizable> AUTH_REGISTRY = new HashMap<>();
+
+    public static final void initAuthContext(SlingContext context)
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        initContext(context);
+        context.load().json("/auth.json", "/home");
+
+        JackrabbitSession session = Mockito.mock(JackrabbitSession.class);
+
+        AUTH_REGISTRY.clear();
+
+        ResourceTree.stream(context.resourceResolver().getResource("/home/users"), "rep:User").forEach(u -> {
+
+            User user = Mockito.mock(User.class);
+            try {
+                Mockito.when(user.getID()).thenReturn(u.getResource().getValueMap().get("rep:principalName", ""));
+                Mockito.when(user.getPath()).thenReturn(u.getResource().getPath());
+                Mockito.when(user.declaredMemberOf()).thenAnswer((ans) -> {
+                    final List<Group> groups = new ArrayList<>();
+                    AUTH_REGISTRY.values().forEach(a -> {
+                        if (a instanceof Group) {
+                            try {
+                                ((Group) a).getDeclaredMembers().forEachRemaining(m -> {
+                                    if (m == user) {
+                                        groups.add((Group) a);
+                                    }
+                                });
+                            } catch (RepositoryException e) {
+                                throw new RuntimeException(e);
+                            }
+                        }
+                    });
+                    return groups.iterator();
+                });
+            } catch (RepositoryException e) {
+                throw new RuntimeException(e);
+            }
+            AUTH_REGISTRY.put(u.getResource().getPath(), user);
+        });
+
+        ResourceTree.stream(context.resourceResolver().getResource("/home/groups/sling-cms"), "rep:Group")
+                .forEach(g -> {
+
+                    Group group = Mockito.mock(Group.class);
+                    try {
+                        Mockito.when(group.getID())
+                                .thenReturn(g.getResource().getValueMap().get("rep:principalName", ""));
+                        Mockito.when(group.getPath()).thenReturn(g.getResource().getPath());
+                        Mockito.when(group.isGroup()).thenReturn(true);
+                        Mockito.when(group.declaredMemberOf()).thenAnswer((ans) -> {
+                            final List<Group> groups = new ArrayList<>();
+                            AUTH_REGISTRY.values().forEach(a -> {
+                                if (a instanceof Group) {
+                                    try {
+                                        ((Group) a).getDeclaredMembers().forEachRemaining(m -> {
+                                            if (m == group) {
+                                                groups.add((Group) a);
+                                            }
+                                        });
+                                    } catch (RepositoryException e) {
+                                        throw new RuntimeException(e);
+                                    }
+                                }
+                            });
+                            return groups.iterator();
+                        });
+                        Mockito.when(group.getDeclaredMembers()).thenAnswer((ans) -> {
+                            final List<Authorizable> members = new ArrayList<>();
+                            for (String member : g.getResource().getValueMap().get("members", new String[0])) {
+                                if (AUTH_REGISTRY.containsKey(member)) {
+                                    members.add(AUTH_REGISTRY.get(member));
+                                }
+                            }
+
+                            return members.iterator();
+                        });
+                    } catch (RepositoryException e) {
+                        throw new RuntimeException(e);
+                    }
+
+                    AUTH_REGISTRY.put(g.getResource().getPath(), group);
+                });
+
+        UserManager userManager = Mockito.mock(UserManager.class);
+        Mockito.when(session.getUserManager()).thenReturn(userManager);
+        Mockito.when(userManager.getAuthorizableByPath(Mockito.anyString())).thenAnswer((ans) -> {
+            String path = ans.getArgument(0);
+            return AUTH_REGISTRY.get(path);
+        });
+        context.registerAdapter(ResourceResolver.class, Session.class, session);
+    }
+
+    public static final <I> Stream<I> toStream(Iterator<I> iterator) {
+        Iterable<I> iterable = () -> iterator;
+        return StreamSupport.stream(iterable.spliterator(), false);
+    }
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImplTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImplTest.java
new file mode 100644
index 0000000..0c9fe89
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/models/AuthorizableWrapperImplTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.models;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.cms.AuthorizableWrapper;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class AuthorizableWrapperImplTest {
+
+    private static final String ADMIN_PATH = "/home/users/admin";
+    private static final String GROUP_PATH = "/home/groups/group1";
+    private static final String GROUP2_PATH = "/home/groups/group2";
+    private static final String USER_PATH = "/home/users/user1";
+    private static final String THROWY_PATH = "/home/users/throwy";
+    private Resource adminResource;
+    private Resource contentResource;
+    private Group group;
+    private Group group2;
+    private Resource groupResource;
+    private User user;
+    private Resource userResource;
+    private Authorizable throwy;
+    private Resource throwyResource;
+    private Resource group2Resource;
+
+    @Before
+    public void init() throws AccessDeniedException, AuthorizableExistsException,
+            UnsupportedRepositoryOperationException, RepositoryException {
+
+        userResource = Mockito.mock(Resource.class);
+        Mockito.when(userResource.getPath()).thenReturn(USER_PATH);
+        groupResource = Mockito.mock(Resource.class);
+        Mockito.when(groupResource.getPath()).thenReturn(GROUP_PATH);
+        contentResource = Mockito.mock(Resource.class);
+        Mockito.when(contentResource.getPath()).thenReturn("/content");
+        adminResource = Mockito.mock(Resource.class);
+        Mockito.when(adminResource.getPath()).thenReturn(ADMIN_PATH);
+        throwyResource = Mockito.mock(Resource.class);
+        Mockito.when(throwyResource.getPath()).thenReturn(THROWY_PATH);
+        group2Resource = Mockito.mock(Resource.class);
+        Mockito.when(group2Resource.getPath()).thenReturn(GROUP2_PATH);
+
+        ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+        Mockito.when(userResource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(groupResource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(contentResource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(adminResource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(throwyResource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(group2Resource.getResourceResolver()).thenReturn(resolver);
+        Mockito.when(resolver.getUserID()).thenReturn("123");
+
+        JackrabbitSession session = Mockito.mock(JackrabbitSession.class);
+        Mockito.when(resolver.adaptTo(Mockito.any())).thenReturn(session);
+
+        UserManager userManager = Mockito.mock(UserManager.class);
+        Mockito.when(session.getUserManager()).thenReturn(userManager);
+
+        throwy = Mockito.mock(Authorizable.class, inv -> {
+            throw new RepositoryException("YAY JCR!");
+        });
+        Mockito.when(userManager.getAuthorizableByPath(THROWY_PATH)).thenReturn(throwy);
+
+        User adminUser = Mockito.mock(User.class);
+        Mockito.when(adminUser.getID()).thenReturn("admin");
+
+        Mockito.when(userManager.getAuthorizableByPath(ADMIN_PATH)).thenReturn(adminUser);
+
+        user = Mockito.mock(User.class);
+        Mockito.when(user.getID()).thenReturn("123");
+        Mockito.when(userManager.getAuthorizableByPath(USER_PATH)).thenReturn(user);
+        Mockito.when(userManager.getAuthorizable("123")).thenReturn(user);
+
+        group = Mockito.mock(Group.class);
+        Mockito.when(group.getID()).thenReturn("456");
+        Mockito.when(group.isGroup()).thenReturn(true);
+        Mockito.when(group.getMembers()).thenReturn(Collections.singletonList((Authorizable) user).iterator());
+        Mockito.when(group.getPrincipal()).thenReturn(new Principal() {
+            @Override
+            public String getName() {
+                return "456";
+            }
+        });
+        Mockito.when(user.memberOf()).thenReturn(Collections.singletonList(group).iterator());
+        Mockito.when(userManager.getAuthorizableByPath(GROUP_PATH)).thenReturn(group);
+
+        group2 = Mockito.mock(Group.class);
+        Mockito.when(group2.isGroup()).thenReturn(true);
+        Mockito.when(adminUser.declaredMemberOf()).thenReturn(Collections.singletonList(group2).iterator());
+        Mockito.when(group2.getDeclaredMembers())
+                .thenReturn(Collections.singletonList((Authorizable) adminUser).iterator());
+        Mockito.when(userManager.getAuthorizableByPath(GROUP2_PATH)).thenReturn(group2);
+
+    }
+
+    @Test
+    public void testResourceResolver()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource.getResourceResolver());
+        assertNotNull(authWrapper.getAuthorizable());
+        assertEquals(user, authWrapper.getAuthorizable());
+    }
+
+    @Test
+    public void testGetAuthorizable()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+        assertNotNull(authWrapper);
+        assertEquals(user, authWrapper.getAuthorizable());
+
+        authWrapper = new AuthorizableWrapperImpl(groupResource);
+        assertNotNull(authWrapper);
+        assertEquals(group, authWrapper.getAuthorizable());
+
+        try {
+            authWrapper = new AuthorizableWrapperImpl(contentResource);
+            fail();
+        } catch (RepositoryException re) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testGetDeclaredMembers()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(group2Resource);
+
+        Iterator<Authorizable> members = authWrapper.getDeclaredMembers();
+        assertTrue(members.hasNext());
+        assertEquals(1, SlingCMSTestHelper.toStream(members).count());
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.getDeclaredMembers().hasNext());
+
+        authWrapper = new AuthorizableWrapperImpl(adminResource);
+        assertFalse(authWrapper.getDeclaredMembers().hasNext());
+    }
+
+    @Test
+    public void testGetMembers()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(groupResource);
+
+        Iterator<Authorizable> members = authWrapper.getMembers();
+        assertTrue(members.hasNext());
+        assertEquals(1, SlingCMSTestHelper.toStream(members).count());
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.getMembers().hasNext());
+
+        authWrapper = new AuthorizableWrapperImpl(userResource);
+        assertFalse(authWrapper.getMembers().hasNext());
+    }
+
+    @Test
+    public void testGetGroupNames()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+
+        assertEquals(Collections.singletonList("456"),
+                SlingCMSTestHelper.toStream(authWrapper.getGroupNames()).collect(Collectors.toList()));
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.getGroupNames().hasNext());
+    }
+
+    @Test
+    public void testGetId() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+
+        assertEquals("123", authWrapper.getId());
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertNull(authWrapper.getId());
+    }
+
+    @Test
+    public void testIsAdmin()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+
+        assertFalse(authWrapper.isAdministrator());
+
+        authWrapper = new AuthorizableWrapperImpl(adminResource);
+        assertTrue(authWrapper.isAdministrator());
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.isAdministrator());
+    }
+
+    @Test
+    public void testIsMember()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+        assertTrue(authWrapper.isMember("456"));
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.isMember("456"));
+
+    }
+
+    @Test
+    public void testDeclaredMembership()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(adminResource);
+        assertTrue(SlingCMSTestHelper.toStream(authWrapper.getDeclaredMembership()).anyMatch(g -> g == group2));
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.getDeclaredMembership().hasNext());
+    }
+
+    @Test
+    public void testMembership()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+        assertTrue(SlingCMSTestHelper.toStream(authWrapper.getMembership()).anyMatch(g -> g == group));
+
+        authWrapper = new AuthorizableWrapperImpl(throwyResource);
+        assertFalse(authWrapper.getMembership().hasNext());
+    }
+
+    @Test
+    public void testNonMember()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        AuthorizableWrapper authWrapper = new AuthorizableWrapperImpl(userResource);
+        assertFalse(SlingCMSTestHelper.toStream(authWrapper.getMembership()).anyMatch(g -> g == group2));
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperationTest.java
new file mode 100644
index 0000000..312b2b8
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/ChangePasswordOperationTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class ChangePasswordOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+
+    private User user;
+
+    public static final String USER_PATH = "/home/users/test";
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        user = (User) SlingCMSTestHelper.AUTH_REGISTRY.get(USER_PATH);
+
+    }
+
+    @Test
+    public void testChangePassword() throws RepositoryException {
+        ChangePasswordOperation operation = new ChangePasswordOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource(USER_PATH);
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(CreateUserOperation.PN_PASSWORD, "test1").build());
+
+        operation.run(context.request(), response, new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        Mockito.verify(user).changePassword("test1", "test2");
+
+    }
+    
+    @Test
+    public void testGroup() throws RepositoryException {
+        ChangePasswordOperation operation = new ChangePasswordOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/groups/sling-cms/authors");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(CreateUserOperation.PN_PASSWORD, "test1").build());
+
+        operation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperationTest.java
new file mode 100644
index 0000000..32c9f6f
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateGroupOperationTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.security.Principal;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class CreateGroupOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+
+    private Group group;
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        group = null;
+        UserManager userManager = CommonUtils.getUserManager(context.resourceResolver());
+        Mockito.when(userManager.createGroup(Mockito.anyString(), Mockito.any(), Mockito.anyString()))
+                .thenAnswer((ans) -> {
+                    Group group = Mockito.mock(Group.class);
+                    Mockito.when(group.getPath()).thenReturn("/home/groups/tests");
+                    this.group = group;
+                    return group;
+                });
+    }
+
+    @Test
+    public void testCreate() throws RepositoryException {
+        CreateGroupOperation createGroupOperation = new CreateGroupOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(SlingPostConstants.RP_NODE_NAME, "test5").build());
+
+        createGroupOperation.run(context.request(), response,
+                new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        assertNotNull(group);
+        assertEquals("/home/groups/tests", group.getPath());
+
+    }
+
+    @Test
+    public void testExisting() throws RepositoryException {
+        CreateGroupOperation createGroupOperation = new CreateGroupOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(SlingPostConstants.RP_NODE_NAME, "test5").build());
+
+        UserManager userManager = CommonUtils.getUserManager(context.resourceResolver());
+        Mockito.when(userManager.getAuthorizable(Mockito.any(Principal.class))).thenReturn(Mockito.mock(Group.class));
+
+        createGroupOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateUserOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateUserOperationTest.java
new file mode 100644
index 0000000..5af3fdd
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/CreateUserOperationTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.security.Principal;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class CreateUserOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+
+    private User user;
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        user = null;
+        UserManager userManager = CommonUtils.getUserManager(context.resourceResolver());
+        Mockito.when(
+                userManager.createUser(Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.anyString()))
+                .thenAnswer((ans) -> {
+                    User user = Mockito.mock(User.class);
+                    Mockito.when(user.getPath()).thenReturn("/home/users/tests");
+                    this.user = user;
+                    return user;
+                });
+    }
+
+    @Test
+    public void testCreate() throws RepositoryException {
+        CreateUserOperation createUserOperation = new CreateUserOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(SlingPostConstants.RP_NODE_NAME, "tests").put(CreateUserOperation.PN_PASSWORD, "test5").build());
+
+        createUserOperation.run(context.request(), response,
+                new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        assertNotNull(user);
+        assertEquals("/home/users/tests", user.getPath());
+
+    }
+
+    @Test
+    public void testExisting() throws RepositoryException {
+        CreateUserOperation createUserOperation = new CreateUserOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(SlingPostConstants.RP_NODE_NAME, "test5").build());
+
+        UserManager userManager = CommonUtils.getUserManager(context.resourceResolver());
+        Mockito.when(userManager.getAuthorizable(Mockito.any(Principal.class))).thenReturn(Mockito.mock(Group.class));
+
+        createUserOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembersOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembersOperationTest.java
new file mode 100644
index 0000000..518d525
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembersOperationTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class MembersOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+    private ArrayList<String> added;
+    private ArrayList<String> removed;
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        Group group = (Group) SlingCMSTestHelper.AUTH_REGISTRY.get("/home/groups/sling-cms/authors");
+
+        added = new ArrayList<>();
+        removed = new ArrayList<>();
+
+        Mockito.when(group.addMember(Mockito.any())).then((ans) -> {
+            added.add(ans.getArgument(0, Authorizable.class).getPath());
+            return true;
+        });
+
+        Mockito.when(group.removeMember(Mockito.any())).then((ans) -> {
+            removed.add(ans.getArgument(0, Authorizable.class).getPath());
+            return true;
+        });
+    }
+
+    @Test
+    public void testModifyOperation() throws RepositoryException {
+        MembersOperation membersOperation = new MembersOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/groups/sling-cms/authors");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(":members", new String[] { "/home/users/test2", "/home/users/test3" }).build());
+
+        membersOperation.run(context.request(), response, null);
+
+        assertNull(response.getError());
+
+        assertEquals("/home/groups/sling-cms/authors", response.getPath());
+
+        assertTrue(added.size() == 1);
+        assertEquals("/home/users/test2", added.get(0));
+
+        assertTrue(removed.size() == 1);
+        assertEquals("/home/users/test", removed.get(0));
+    }
+    
+
+    @Test
+    public void testNotGroup() throws RepositoryException {
+        MembersOperation membersOperation = new MembersOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users/test2");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(":members", new String[] { "/home/users/test2", "/home/users/test3" }).build());
+
+        membersOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+    
+
+    @Test
+    public void testInvalidPath() throws RepositoryException {
+        MembersOperation membersOperation = new MembersOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/groups/sling-cms/authors");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(":members", new String[] { "/home/users/test2", "/home/users/test4" }).build());
+
+        membersOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembershipOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembershipOperationTest.java
new file mode 100644
index 0000000..a48188f
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/MembershipOperationTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class MembershipOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+    private ArrayList<String> added;
+    private ArrayList<String> removed;
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        Group group = (Group) SlingCMSTestHelper.AUTH_REGISTRY.get("/home/groups/sling-cms/authors");
+
+        added = new ArrayList<>();
+        removed = new ArrayList<>();
+
+        Mockito.when(group.addMember(Mockito.any())).then((ans) -> {
+            added.add(ans.getArgument(0, Authorizable.class).getPath());
+            return true;
+        });
+
+        Mockito.when(group.removeMember(Mockito.any())).then((ans) -> {
+            removed.add(ans.getArgument(0, Authorizable.class).getPath());
+            return true;
+        });
+    }
+
+    @Test
+    public void testModifyOperation() throws RepositoryException {
+        MembershipOperation membersOperation = new MembershipOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users/test2");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(MembershipOperation.PN_MEMBERSHIP, new String[] { "/home/groups/sling-cms/authors" }).build());
+
+        membersOperation.run(context.request(), response,
+                new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        assertEquals("/home/users/test2", response.getPath());
+
+        assertTrue(added.size() == 1);
+        assertEquals("/home/users/test2", added.get(0));
+
+        assertTrue(removed.size() == 0);
+
+    }
+
+    @Test
+    public void testInvalidPath() throws RepositoryException {
+        MembershipOperation membersOperation = new MembershipOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/content");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(":members", new String[] { "/home/groups/sling-cms/authors" }).build());
+
+        membersOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+    @Test
+    public void testInvalidGroup() throws RepositoryException {
+        MembershipOperation membersOperation = new MembershipOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users/test");
+        context.request().setParameterMap(ImmutableMap.<String, Object>builder()
+                .put(MembershipOperation.PN_MEMBERSHIP, new String[] { "/home/groups/sling-cms/authors32" }).build());
+
+        membersOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperationTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperationTest.java
new file mode 100644
index 0000000..0489b74
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/operations/UpdateStatusOperationTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.sling.cms.core.internal.operations;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.security.Principal;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
+import org.apache.sling.cms.core.internal.CommonUtils;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.SlingPostProcessor;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.google.common.collect.ImmutableMap;
+
+public class UpdateStatusOperationTest {
+
+    @Rule
+    public SlingContext context = new SlingContext();
+
+    private User user;
+
+    @Before
+    public void init() throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        SlingCMSTestHelper.initAuthContext(context);
+
+        user = (User) SlingCMSTestHelper.AUTH_REGISTRY.get("/home/users/test");
+    }
+
+    @Test
+    public void testEnable() throws RepositoryException {
+        UpdateStatusOperation operation = new UpdateStatusOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/users/test");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(UpdateStatusOperation.PN_REASON, "").build());
+
+        operation.run(context.request(), response, new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        Mockito.verify(user).disable(null);
+
+    }
+
+    @Test
+    public void testDisable() throws RepositoryException {
+        UpdateStatusOperation operation = new UpdateStatusOperation();
+        PostResponse response = new JSONResponse();
+
+        String reason = "A valid reason";
+        context.currentResource("/home/users/test");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(UpdateStatusOperation.PN_REASON, reason).build());
+
+        operation.run(context.request(), response, new SlingPostProcessor[] { Mockito.mock(SlingPostProcessor.class) });
+
+        assertNull(response.getError());
+
+        Mockito.verify(user).disable(reason);
+
+    }
+
+    @Test
+    public void testGroup() throws RepositoryException {
+        CreateUserOperation createUserOperation = new CreateUserOperation();
+        PostResponse response = new JSONResponse();
+
+        context.currentResource("/home/groups/sling-cms/authors");
+        context.request().setParameterMap(
+                ImmutableMap.<String, Object>builder().put(UpdateStatusOperation.PN_REASON, "").build());
+
+        UserManager userManager = CommonUtils.getUserManager(context.resourceResolver());
+        Mockito.when(userManager.getAuthorizable(Mockito.any(Principal.class))).thenReturn(Mockito.mock(Group.class));
+
+        createUserOperation.run(context.request(), response, null);
+
+        assertNotNull(response.getError());
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/servlets/DownloadFileServletTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/servlets/DownloadFileServletTest.java
index 839f227..01279dd 100644
--- a/core/src/test/java/org/apache/sling/cms/core/internal/servlets/DownloadFileServletTest.java
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/servlets/DownloadFileServletTest.java
@@ -23,7 +23,7 @@ import java.io.IOException;
 
 import javax.servlet.ServletException;
 
-import org.apache.sling.cms.core.helpers.SlingCMSContextHelper;
+import org.apache.sling.cms.core.helpers.SlingCMSTestHelper;
 import org.apache.sling.testing.mock.sling.junit.SlingContext;
 import org.junit.Before;
 import org.junit.Rule;
@@ -36,7 +36,7 @@ public class DownloadFileServletTest {
 
     @Before
     public void init() {
-        SlingCMSContextHelper.initContext(context);
+        SlingCMSTestHelper.initContext(context);
 
     }
 
diff --git a/core/src/test/resources/auth.json b/core/src/test/resources/auth.json
new file mode 100644
index 0000000..47d8eab
--- /dev/null
+++ b/core/src/test/resources/auth.json
@@ -0,0 +1,142 @@
+{
+    "jcr:primaryType": "rep:AuthorizableFolder",
+    "users": {
+        "jcr:primaryType": "rep:AuthorizableFolder",
+        "user1": {
+            "jcr:primaryType": "rep:User",
+            "rep:principalName": "user1",
+            "rep:authorizableId": "user1"
+        },
+        "F": {
+            "jcr:primaryType": "rep:AuthorizableFolder",
+            "FnzSMsZTdPghKjb4jwNML": {
+                "jcr:primaryType": "rep:User",
+                "rep:principalName": "admin",
+                "rep:authorizableId": "admin"
+            }
+        },
+        "w": {
+            "jcr:primaryType": "rep:AuthorizableFolder",
+            "wUbBUsoC475uXtLGfEwpG": {
+                "jcr:primaryType": "rep:User",
+                "rep:principalName": "anonymous",
+                "rep:authorizableId": "anonymous"
+            }
+        },
+        "test": {
+            "jcr:primaryType": "rep:User",
+            "rep:principalName": "test",
+            "rep:authorizableId": "test"
+        },
+        "test2": {
+            "jcr:primaryType": "rep:User",
+            "rep:principalName": "test2",
+            "rep:authorizableId": "test2"
+        },
+        "test3": {
+            "jcr:primaryType": "rep:User",
+            "rep:principalName": "test3",
+            "rep:authorizableId": "test3"
+        }
+    },
+    "groups": {
+        "jcr:primaryType": "rep:AuthorizableFolder",
+        "sling-cms": {
+            "jcr:primaryType": "rep:AuthorizableFolder",
+            "authors": {
+                "jcr:primaryType": "rep:Group",
+                "jcr:mixinTypes": [
+                    "rep:AccessControllable"
+                ],
+                "rep:principalName": "authors",
+                "rep:authorizableId": "authors",
+                "rep:policy": {
+                    "jcr:primaryType": "rep:ACL",
+                    "allow": {
+                        "jcr:primaryType": "rep:GrantACE",
+                        "rep:principalName": "authors",
+                        "rep:privileges": [
+                            "jcr:read"
+                        ]
+                    }
+                },
+                "members": [
+                    "/home/users/test",
+                    "/home/users/test3"
+                ]
+            },
+            "job-users": {
+                "jcr:primaryType": "rep:Group",
+                "jcr:mixinTypes": [
+                    "rep:AccessControllable"
+                ],
+                "rep:principalName": "job-users",
+                "rep:authorizableId": "job-users",
+                "rep:policy": {
+                    "jcr:primaryType": "rep:ACL",
+                    "allow": {
+                        "jcr:primaryType": "rep:GrantACE",
+                        "rep:principalName": "job-users",
+                        "rep:privileges": [
+                            "jcr:read"
+                        ]
+                    }
+                }
+            },
+            "taxonomy-users": {
+                "jcr:primaryType": "rep:Group",
+                "jcr:mixinTypes": [
+                    "rep:AccessControllable"
+                ],
+                "rep:principalName": "taxonomy-users",
+                "rep:authorizableId": "taxonomy-users",
+                "rep:policy": {
+                    "jcr:primaryType": "rep:ACL",
+                    "allow": {
+                        "jcr:primaryType": "rep:GrantACE",
+                        "rep:principalName": "taxonomy-users",
+                        "rep:privileges": [
+                            "jcr:read"
+                        ]
+                    }
+                }
+            },
+            "administrators": {
+                "jcr:primaryType": "rep:Group",
+                "jcr:mixinTypes": [
+                    "rep:AccessControllable"
+                ],
+                "rep:principalName": "administrators",
+                "rep:authorizableId": "administrators",
+                "rep:policy": {
+                    "jcr:primaryType": "rep:ACL",
+                    "allow": {
+                        "jcr:primaryType": "rep:GrantACE",
+                        "rep:principalName": "administrators",
+                        "rep:privileges": [
+                            "jcr:read"
+                        ]
+                    }
+                }
+            },
+            "ugc-users": {
+                "jcr:primaryType": "rep:Group",
+                "jcr:mixinTypes": [
+                    "rep:AccessControllable"
+                ],
+                "rep:principalName": "ugc-users",
+                "rep:authorizableId": "ugc-users",
+                "rep:policy": {
+                    "jcr:primaryType": "rep:ACL",
+                    "allow": {
+                        "jcr:primaryType": "rep:GrantACE",
+                        "rep:principalName": "ugc-users",
+                        "rep:privileges": [
+                            "jcr:read"
+                        ]
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/docs/admin-tools.md b/docs/admin-tools.md
index 24ffa8d..46661fa 100644
--- a/docs/admin-tools.md
+++ b/docs/admin-tools.md
@@ -60,4 +60,4 @@ This console is accessible from *Tools > System Console* or at [http://localhost
 
 ![users and Groups](img/users-groups.png)
 
-This tool is accessible from *Tools > Users & Groups* or at [http://localhost:8080/bin/users.html](http://localhost:8080/bin/users.html). It allows administrators to create and manage users and groups within Sling CMS. Permissions are managed in the Node Browser
\ No newline at end of file
+This tool is accessible from *Tools > Users & Groups* or at [http://localhost:8080/cms/auth/list.html/home](http://localhost:8080/cms/auth/list.html/home). It allows administrators to create and manage users and groups within Sling CMS. Permissions are managed in the Node Browser
\ No newline at end of file
diff --git a/docs/img/users-groups.png b/docs/img/users-groups.png
index b47296f..72c66ae 100644
Binary files a/docs/img/users-groups.png and b/docs/img/users-groups.png differ
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/cms/staticnav/staticnav.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/cms/staticnav/staticnav.jsp
index 0a19bcf..80c1bfd 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/cms/staticnav/staticnav.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/cms/staticnav/staticnav.jsp
@@ -32,7 +32,7 @@
             </c:if>
         </c:forEach>
     </c:forEach>
-    <sling:adaptTo var="currentUser" adaptable="${slingRequest.resourceResolver}" adaptTo="org.apache.sling.cms.CurrentUser" />
+    <sling:adaptTo var="currentUser" adaptable="${slingRequest.resourceResolver}" adaptTo="org.apache.sling.cms.AuthorizableWrapper" />
     <ul id="${fn:replace(properties.title,' ','-')}-nav" class="menu-list ${hidden}">
         <c:forEach var="item" items="${sling:listChildren(sling:getRelativeResource(resource,'links'))}">
             <c:set var="selected" value="" />
@@ -44,11 +44,11 @@
                     <c:set var="selected" value="is-selected" />
                 </c:if>
             </c:forEach>
-            <c:set var="enabled" value="${true}" />
-            <c:if test="${not empty item.valueMap.enabledGroups && currentUser.id != 'admin'}">
+            <c:set var="enabled" value="${currentUser.administrator || empty item.valueMap.enabledGroups}" />
+            <c:if test="${not empty item.valueMap.enabledGroups && !currentUser.administrator}">
                 <c:set var="enabled" value="${false}" />
                 <c:forEach var="group" items="${item.valueMap.enabledGroups}">
-                    <c:forEach var="userGroup" items="${currentUser.groups}">
+                    <c:forEach var="userGroup" items="${currentUser.groupNames}">
                         <c:if test="${group == userGroup}">
                             <c:set var="enabled" value="${true}" />
                         </c:if>
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members.json b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members.json
new file mode 100644
index 0000000..8fb7b25
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members.json
@@ -0,0 +1,6 @@
+{
+    "jcr:primaryType": "sling:Component",
+    "sling:resourceSuperType" : "sling-cms/components/editor/fields/labelfield",
+    "componentType": "SlingCMS-FieldConfig",
+    "jcr:title": "Sling CMS - Members"
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/options.jsp
similarity index 62%
copy from ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
copy to ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/options.jsp
index 6ade9a5..2541bef 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/options.jsp
@@ -16,12 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */ --%>
- <%@include file="/libs/sling-cms/global.jsp"%>
-<option value="">Select Locale</option>
-<c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
-</c:forEach>
\ No newline at end of file
+<%@include file="/libs/sling-cms/global.jsp"%>
+<datalist id="labelfield-${fn:replace(resource.name,':','-')}">
+    <c:set var="query" value="SELECT * FROM [rep:Authorizable] WHERE ISDESCENDANTNODE([/home]) ORDER BY [rep:principalName]" />
+    <c:forEach var="auth" items="${sling:findResources(resourceResolver,query,'JCR-SQL2')}">
+        <option value="${sling:encode(auth.path,'HTML_ATTR')}">${sling:encode(auth.valueMap['rep:principalName'],'HTML')}</option>
+    </c:forEach>
+</datalist>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/values.jsp
similarity index 64%
copy from ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
copy to ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/values.jsp
index 6ade9a5..ddce705 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/members/values.jsp
@@ -17,11 +17,13 @@
  * under the License.
  */ --%>
  <%@include file="/libs/sling-cms/global.jsp"%>
-<option value="">Select Locale</option>
-<c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
+<sling:adaptTo var="auth" adaptable="${slingRequest.requestPathInfo.suffixResource}" adaptTo="org.apache.sling.cms.AuthorizableWrapper" />
+<c:forEach var="member" items="${auth.declaredMembers}">
+    <a class="button labelfield__item">
+        <input type="hidden" name="${properties.name}" value="${member.path}" />
+        <span class="labelfield__title">
+            ${sling:encode(member,'HTML')}
+        </span>
+        <span class="jam jam-close"></span>
+    </a>
 </c:forEach>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership.json b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership.json
new file mode 100644
index 0000000..f9a1868
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership.json
@@ -0,0 +1,6 @@
+{
+    "jcr:primaryType": "sling:Component",
+    "sling:resourceSuperType" : "sling-cms/components/editor/fields/labelfield",
+    "componentType": "SlingCMS-FieldConfig",
+    "jcr:title": "Sling CMS - Membership"
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/options.jsp
similarity index 62%
copy from ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
copy to ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/options.jsp
index 6ade9a5..3a709b3 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/options.jsp
@@ -16,12 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */ --%>
- <%@include file="/libs/sling-cms/global.jsp"%>
-<option value="">Select Locale</option>
-<c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
-</c:forEach>
\ No newline at end of file
+<%@include file="/libs/sling-cms/global.jsp"%>
+<datalist id="labelfield-${fn:replace(resource.name,':','-')}">
+    <c:set var="query" value="SELECT * FROM [rep:Group] WHERE ISDESCENDANTNODE([/home/groups]) ORDER BY [rep:principalName]" />
+    <c:forEach var="group" items="${sling:findResources(resourceResolver,query,'JCR-SQL2')}">
+        <option value="${sling:encode(group.path,'HTML_ATTR')}">${sling:encode(group.valueMap['rep:principalName'],'HTML')}</option>
+    </c:forEach>
+</datalist>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/values.jsp
similarity index 64%
copy from ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
copy to ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/values.jsp
index 6ade9a5..7ccba05 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/membership/values.jsp
@@ -17,11 +17,13 @@
  * under the License.
  */ --%>
  <%@include file="/libs/sling-cms/global.jsp"%>
-<option value="">Select Locale</option>
-<c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
+<sling:adaptTo var="auth" adaptable="${slingRequest.requestPathInfo.suffixResource}" adaptTo="org.apache.sling.cms.AuthorizableWrapper" />
+<c:forEach var="group" items="${auth.declaredMembership}">
+    <a class="button labelfield__item">
+        <input type="hidden" name="${properties.name}" value="${group.path}" />
+        <span class="labelfield__title">
+            ${sling:encode(group,'HTML')}
+        </span>
+        <span class="jam jam-close"></span>
+    </a>
 </c:forEach>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/status/status.jsp
similarity index 51%
copy from ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
copy to ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/status/status.jsp
index 6ade9a5..1d68d6b 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/fields/auth/status/status.jsp
@@ -16,12 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */ --%>
- <%@include file="/libs/sling-cms/global.jsp"%>
-<option value="">Select Locale</option>
-<c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
-</c:forEach>
\ No newline at end of file
+<%@include file="/libs/sling-cms/global.jsp"%>
+<c:set var="user" value="${sling:adaptTo(slingRequest.requestPathInfo.suffixResource,'org.apache.sling.cms.AuthorizableWrapper').authorizable }" />
+<c:choose>
+    <c:when test="${user.disabled}">
+        <dl>
+            <dt>Status</dt>
+            <dd>Disabled</dd>
+            <dt>Reason</dt>
+            <dd><sling:encode value="${user.disabledReason}" mode="HTML" /></dd>
+        </dl>
+        <input type="hidden" name=":reason" value="" />
+    </c:when>
+    <c:otherwise>
+        <dl>
+            <dt>Status</dt>
+            <dd>Enabled</dd>
+            <dt><label for=":reason">Disable Reason</label></dt>
+            <dd><input type="text" class="input" name=":reason" value="" /></dd>
+        </dl>
+    </c:otherwise>
+</c:choose>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
index 6ade9a5..8e813cc 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/editor/scripts/localeOptions.jsp
@@ -19,9 +19,9 @@
  <%@include file="/libs/sling-cms/global.jsp"%>
 <option value="">Select Locale</option>
 <c:forEach var="locale" items="${sling:adaptTo(slingRequest,'org.apache.sling.cms.core.models.LocaleList').locales}">
-	<c:if test="${not empty locale.language}">
-		<option value="${locale}" ${locale == editProperties['jcr:language'] ? 'selected' : ''}>
-			${locale.displayLanguage} ${locale.displayCountry} (${locale})
-		</option>
-	</c:if>
+    <c:if test="${not empty locale.language}">
+        <option value="${locale}" ${locale == editProperties[properties.name] ? 'selected' : ''}>
+            ${locale.displayLanguage} ${locale.displayCountry} (${locale})
+        </option>
+    </c:if>
 </c:forEach>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/components/pages/base/nav.jsp b/ui/src/main/resources/jcr_root/libs/sling-cms/components/pages/base/nav.jsp
index eb42fc1..e9b3f8a 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/components/pages/base/nav.jsp
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/components/pages/base/nav.jsp
@@ -37,7 +37,34 @@
     </div>
     <div class="navbar-menu" id="top-navbar-menu">
         <div class="navbar-end">
-            <a class="navbar-item " href="/system/sling/logout" title="Logout of Apache Sling CMS"><span>${resourceResolver.userID} </span><i class="jam jam-log-out"></i></a>
+            <div class="navbar-item has-dropdown is-hoverable">
+                <sling:adaptTo adaptable="${resourceResolver}" adaptTo="org.apache.sling.cms.AuthorizableWrapper" var="auth" />
+                <sling:getResource path="${auth.authorizable.path}/profile" var="profile" />
+                <a class="navbar-link">
+                    <sling:encode value="${profile.valueMap.name}" default="${resourceResolver.userID}" mode="HTML" />
+                </a>
+                <div class="navbar-dropdown">
+                    <a class="navbar-item Fetch-Modal" data-title="User Profile" data-path=".Main-Content form" href="/cms/auth/user/profile.html${auth.authorizable.path}">
+                        <i class="jam jam-id-card">
+                            <span class="is-vhidden">Profile</span>
+                        </i>&nbsp;
+                        Profile
+                    </a>
+                    <a class="navbar-item" href="https://github.com/apache/sling-org-apache-sling-app-cms" target="_blank">
+                        <i class="jam jam-help">
+                            <span class="is-vhidden">Help</span>
+                        </i>&nbsp;
+                        Help
+                    </a>
+                    <hr class="navbar-divider">
+                    <a class="navbar-item" href="/system/sling/logout">
+                        <i class="jam jam-log-out">
+                            <span class="is-vhidden">Logout</span>
+                        </i>&nbsp;
+                        Logout
+                    </a>
+                </div>
+            </div>
         </div>
     </div>
 </nav>
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/create.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/create.json
new file mode 100644
index 0000000..9685745
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/create.json
@@ -0,0 +1,35 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Create Group",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "actionSuffix": "/*",
+                "button": "Create Group",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "name": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Name",
+                        "name": ":name",
+                        "required": true
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "creategroup"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/members.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/members.json
new file mode 100644
index 0000000..19a5d22
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/group/members.json
@@ -0,0 +1,32 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Members",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "button": "Update Members",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "membership": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/auth/members",
+                        "name": ":members"
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "members"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/list.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/list.json
new file mode 100644
index 0000000..6c79a66
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/list.json
@@ -0,0 +1,229 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/base",
+        "jcr:title": "Users / Groups",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "contentactions": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/cms/contentactions",
+                "actions": {
+                    "user": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "label": "User",
+                        "prefix": "/cms/auth/user/create.html"
+                    },
+                    "group": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "label": "Group",
+                        "prefix": "/cms/auth/group/create.html"
+                    },
+                    "repFolder": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "label": "Folder",
+                        "prefix": "/cms/auth/newfolder.html"
+                    }
+                }
+            },
+            "contentbreadcrumb": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/cms/contentbreadcrumb",
+                "depth": 0
+            },
+            "contenttable": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/cms/contenttable",
+                "columns": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "name": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "title": "Name"
+                    },
+                    "actions": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "title": "Actions"
+                    }
+                },
+                "types": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "rep:AuthorizableFolder": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "columns": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "name": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/name",
+                                "link": true,
+                                "prefix": "/cms/auth/list.html"
+                            },
+                            "actions": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/actions",
+                                "movecopy": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Move / Copy Folder",
+                                    "icon": "move-alt",
+                                    "prefix": "/cms/shared/movecopy.html"
+                                },
+                                "delete": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Delete Folder",
+                                    "icon": "trash",
+                                    "prefix": "/cms/shared/delete.html"
+                                }
+                            }
+                        }
+                    },
+                    "rep:Group": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "columns": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "name": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/text",
+                                "property": "rep:principalName"
+                            },
+                            "actions": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/actions",
+                                "membership": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Group Membership",
+                                    "icon": "user-circle",
+                                    "prefix": "/cms/auth/membership.html"
+                                },
+                                "members": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Group Members",
+                                    "icon": "users",
+                                    "prefix": "/cms/auth/group/members.html"
+                                },
+                                "movecopy": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Move / Copy Group",
+                                    "icon": "move-alt",
+                                    "prefix": "/cms/shared/movecopy.html"
+                                },
+                                "delete": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Delete Group",
+                                    "icon": "trash",
+                                    "prefix": "/cms/shared/delete.html"
+                                }
+                            }
+                        }
+                    },
+                    "rep:SystemUser": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "columns": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "name": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/text",
+                                "property": "rep:principalName"
+                            },
+                            "actions": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/actions",
+                                "edit": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Edit User",
+                                    "icon": "pencil-f",
+                                    "prefix": "/cms/auth/user/edit.html"
+                                },
+                                "membership": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Group Membership",
+                                    "icon": "user-circle",
+                                    "prefix": "/cms/auth/membership.html"
+                                },
+                                "movecopy": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Move / Copy User",
+                                    "icon": "move-alt",
+                                    "prefix": "/cms/shared/movecopy.html"
+                                },
+                                "delete": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Delete User",
+                                    "icon": "trash",
+                                    "prefix": "/cms/shared/delete.html"
+                                }
+                            }
+                        }
+                    },
+                    "rep:User": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "columns": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "name": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/text",
+                                "property": "rep:principalName"
+                            },
+                            "actions": {
+                                "jcr:primaryType": "nt:unstructured",
+                                "sling:resourceType": "sling-cms/components/cms/columns/actions",
+                                "profile": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "User Profile",
+                                    "icon": "id-card",
+                                    "prefix": "/cms/auth/user/profile.html"
+                                },
+                                "password": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Change Password",
+                                    "icon": "key",
+                                    "prefix": "/cms/auth/user/password.html"
+                                },
+                                "status": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Status",
+                                    "icon": "stop-sign",
+                                    "prefix": "/cms/auth/user/status.html"
+                                },
+                                "membership": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Group Membership",
+                                    "icon": "user-circle",
+                                    "prefix": "/cms/auth/membership.html"
+                                },
+                                "movecopy": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Move / Copy User",
+                                    "icon": "move-alt",
+                                    "prefix": "/cms/shared/movecopy.html"
+                                },
+                                "delete": {
+                                    "jcr:primaryType": "nt:unstructured",
+                                    "modal": true,
+                                    "title": "Delete User",
+                                    "icon": "trash",
+                                    "prefix": "/cms/shared/delete.html"
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/membership.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/membership.json
new file mode 100644
index 0000000..138aa87
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/membership.json
@@ -0,0 +1,32 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Membership",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "button": "Update Membership",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "membership": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/auth/membership",
+                        "name": ":membership"
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "membership"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/newfolder.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/newfolder.json
new file mode 100644
index 0000000..617a1f8
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/newfolder.json
@@ -0,0 +1,36 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Create Authorizable Folder",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "actionSuffix": "/*",
+                "button": "Create Folder",
+                "successPrepend": "/cms/auth/list.html",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "name": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Name",
+                        "name": ":name",
+                        "required": true
+                    },
+                    "primaryType": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": "jcr:primaryType",
+                        "value": "rep:AuthorizableFolder"
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/create.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/create.json
new file mode 100644
index 0000000..3fb2e6f
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/create.json
@@ -0,0 +1,43 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Create User",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "actionSuffix": "/*",
+                "button": "Create User",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "name": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Name",
+                        "name": ":name",
+                        "required": true
+                    },
+                    "password": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Password",
+                        "name": ":password",
+                        "required": true,
+                        "type": "password"
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "createuser"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/password.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/password.json
new file mode 100644
index 0000000..54c3f4c
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/password.json
@@ -0,0 +1,35 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "Change Password",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "button": "Change Password",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "password": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "New Password",
+                        "name": ":password",
+                        "type": "password",
+                        "requred": true
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "changepassword"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/profile.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/profile.json
new file mode 100644
index 0000000..2680887
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/profile.json
@@ -0,0 +1,41 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "User Profile",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "button": "Update Profile",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "name": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Name",
+                        "name": "profile/name"
+                    },
+                    "email": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/text",
+                        "label": "Email",
+                        "type": "email",
+                        "name": "profile/email"
+                    },
+                    "locale": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/select",
+                        "label": "Locale",
+                        "name": "profile/locale",
+                        "optionsScript": "/libs/sling-cms/components/editor/scripts/localeOptions.jsp"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/status.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/status.json
new file mode 100644
index 0000000..e16d044
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/auth/user/status.json
@@ -0,0 +1,31 @@
+{
+    "jcr:primaryType": "sling:Page",
+    "jcr:content": {
+        "sling:resourceType": "sling-cms/components/pages/modal",
+        "jcr:title": "User Status",
+        "jcr:primaryType": "nt:unstructured",
+        "container": {
+            "jcr:primaryType": "nt:unstructured",
+            "sling:resourceType": "sling-cms/components/general/container",
+            "slingform": {
+                "jcr:primaryType": "nt:unstructured",
+                "sling:resourceType": "sling-cms/components/editor/slingform",
+                "button": "Update Status",
+                "fields": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "sling:resourceType": "sling-cms/components/general/container",
+                    "status": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/auth/status"
+                    },
+                    "operation": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "sling:resourceType": "sling-cms/components/editor/fields/hidden",
+                        "name": ":operation",
+                        "value": "updatestatus"
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/content/start.json b/ui/src/main/resources/jcr_root/libs/sling-cms/content/start.json
index b6fea7f..43fe601 100644
--- a/ui/src/main/resources/jcr_root/libs/sling-cms/content/start.json
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/content/start.json
@@ -141,7 +141,10 @@
                     },
                     "usersgroups": {
                         "jcr:primaryType": "nt:unstructured",
-                        "link": "/bin/users.html",
+                        "enabledGroups": [
+                            "administrators"
+                        ],
+                        "link": "/cms/auth/list.html/home",
                         "text": "Users &amp; Groups"
                     }
                 }
diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.ResourceEditorAssociation-auth.config b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.ResourceEditorAssociation-auth.config
new file mode 100644
index 0000000..9188e1b
--- /dev/null
+++ b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.cms.core.internal.ResourceEditorAssociation-auth.config
@@ -0,0 +1,20 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+#
+pathPattern="\\/home.*"
+editor="/cms/auth/list.html"
\ No newline at end of file