keycloak-aplcache
Changes
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js 61(+61 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html 1(+0 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html 85(+85 -0)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java
index cd5292f..4414f24 100755
--- a/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/GroupRepresentation.java
@@ -14,6 +14,7 @@ import java.util.Map;
public class GroupRepresentation {
protected String id;
protected String name;
+ protected String path;
protected Map<String, List<String>> attributes;
protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles;
@@ -35,6 +36,14 @@ public class GroupRepresentation {
this.name = name;
}
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
public List<String> getRealmRoles() {
return realmRoles;
}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 2fd5382..c148a02 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -447,6 +447,21 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'UserRoleMappingCtrl'
})
+ .when('/realms/:realm/users/:user/groups', {
+ templateUrl : resourceUrl + '/partials/user-group-membership.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ user : function(UserLoader) {
+ return UserLoader();
+ },
+ groups : function(GroupListLoader) {
+ return GroupListLoader();
+ }
+ },
+ controller : 'UserGroupMembershipCtrl'
+ })
.when('/realms/:realm/users/:user/sessions', {
templateUrl : resourceUrl + '/partials/user-sessions.html',
resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 4f45d63..f42f8aa 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -1090,3 +1090,64 @@ module.controller('UserFederationMapperCreateCtrl', function($scope, realm, prov
});
+module.controller('UserGroupMembershipCtrl', function($scope, $route, realm, groups, user, UserGroupMembership, UserGroupMapping, Notifications, $location, Dialog) {
+ $scope.realm = realm;
+ $scope.user = user;
+ $scope.groupList = groups;
+ $scope.selectedGroup = null;
+ $scope.tree = [];
+
+ UserGroupMembership.query({realm: realm.realm, userId: user.id}, function(data) {
+ $scope.groupMemberships = data;
+
+ });
+
+ $scope.joinGroup = function() {
+ if (!$scope.tree.currentNode) {
+ Notifications.error('Please select a group to add');
+ return;
+ };
+ UserGroupMapping.update({realm: realm.realm, userId: user.id, groupId: $scope.tree.currentNode.id}, function() {
+ Notifications.success('Added group membership');
+ $route.reload();
+ });
+
+ };
+
+ $scope.leaveGroup = function() {
+ UserGroupMapping.remove({realm: realm.realm, userId: user.id, groupId: $scope.selectedGroup.id}, function() {
+ Notifications.success('Removed group membership');
+ $route.reload();
+ });
+
+ };
+
+ var isLeaf = function(node) {
+ return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+ };
+
+ $scope.getGroupClass = function(node) {
+ if (node.id == "realm") {
+ return 'pficon pficon-users';
+ }
+ if (isLeaf(node)) {
+ return 'normal';
+ }
+ if (node.subGroups.length && node.collapsed) return 'collapsed';
+ if (node.subGroups.length && !node.collapsed) return 'expanded';
+ return 'collapsed';
+
+ }
+
+ $scope.getSelectedClass = function(node) {
+ if (node.selected) {
+ return 'selected';
+ } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+ return 'cut';
+ }
+ return undefined;
+ }
+
+});
+
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index b9e4705..c2d8c32 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -1512,6 +1512,26 @@ module.factory('GroupCompositeClientRoleMapping', function($resource) {
});
});
+module.factory('UserGroupMembership', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/users/:userId/groups', {
+ realm : '@realm',
+ userId : '@userId'
+ });
+});
+
+module.factory('UserGroupMapping', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/users/:userId/groups/:groupId', {
+ realm : '@realm',
+ userId : '@userId',
+ groupId : '@groupId'
+ }, {
+ update : {
+ method : 'PUT'
+ }
+ });
+});
+
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
index 7904e56..d81bea7 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/role-mappings.html
@@ -94,7 +94,6 @@
</div>
</div>
</div>
- </div>
</form>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html
new file mode 100755
index 0000000..a8e2c13
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-group-membership.html
@@ -0,0 +1,85 @@
+ <div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/users">Users</a></li>
+ <li>{{user.username}}</li>
+ </ol>
+
+ <kc-tabs-user></kc-tabs-user>
+
+ <form class="form-horizontal" name="realmForm" novalidate>
+ <div class="form-group" kc-read-only="!access.manageUsers">
+ <label class="col-md-1 control-label" class="control-label"></label>
+
+ <div class="col-md-8" >
+ <div class="row">
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <label class="control-label">Group Membership</label>
+ <kc-tooltip>Groups user is a member of. Select a listed group and click the Leave button to leave the group.</kc-tooltip>
+
+ <div class="pull-right" data-ng-show="access.manageUsers">
+ <button id="leaveGroups" class="btn btn-default" ng-click="leaveGroup()">Leave</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <select id="groupMembership" class="form-control" size=5
+ ng-model="selectedGroup"
+ ng-options="r.path for r in groupMemberships">
+ <option style="display:none" value="">select a type</option>
+ </select>
+
+
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-md-5">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+
+ <div class="form-inline">
+ <label class="control-label">Available Groups</label>
+ <kc-tooltip>Groups a user can join. Select a group and click the join button.</kc-tooltip>
+
+ <div class="pull-right" data-ng-show="access.manageUsers">
+ <button id="joinGroup" class="btn btn-default" ng-click="joinGroup()">Join</button>
+ </div>
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td> <div
+ tree-id="tree"
+ angular-treeview="true"
+ tree-model="groupList"
+ node-id="id"
+ node-label="name"
+ node-children="subGroups" >
+ </div>
+
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+ </div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
old mode 100644
new mode 100755
index edc3a66..7508c83
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-user.html
@@ -10,6 +10,7 @@
<li ng-class="{active: path[4] == 'user-attributes'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-attributes">Attributes</a></li>
<li ng-class="{active: path[4] == 'user-credentials'}" data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/user-credentials">Credentials</a></li>
<li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/role-mappings">Role Mappings</a></li>
+ <li ng-class="{active: path[4] == 'groups'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/groups">Groups</a></li>
<li ng-class="{active: path[4] == 'consents'}"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/consents">Consents</a></li>
<li ng-class="{active: path[4] == 'sessions'}" ><a href="#/realms/{{realm.realm}}/users/{{user.id}}/sessions">Sessions</a></li>
<li ng-class="{active: path[4] == 'federated-identity' || path[1] == 'federated-identity'}" data-ng-show="user.federatedIdentities != null"><a href="#/realms/{{realm.realm}}/users/{{user.id}}/federated-identity">Identity Provider Links</a></li>
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index c62d96e..59c640c 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -60,10 +60,25 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class ModelToRepresentation {
+ public static void buildGroupPath(StringBuilder sb, GroupModel group) {
+ if (group.getParent() != null) {
+ buildGroupPath(sb, group.getParent());
+ }
+ sb.append('/').append(group.getName());
+ }
+
+ public static String buildGroupPath(GroupModel group) {
+ StringBuilder sb = new StringBuilder();
+ buildGroupPath(sb, group);
+ return sb.toString();
+ }
+
+
public static GroupRepresentation toRepresentation(GroupModel group, boolean full) {
GroupRepresentation rep = new GroupRepresentation();
rep.setId(group.getId());
rep.setName(group.getName());
+ rep.setPath(buildGroupPath(group));
if (!full) return rep;
// Role mappings
Set<RoleModel> roles = group.getRoleMappings();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 9ba114c..a85c9d2 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -17,6 +17,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.FederatedIdentityModel;
+import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
@@ -36,6 +37,7 @@ import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ClientMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserConsentRepresentation;
@@ -676,7 +678,7 @@ public class UsersResource {
}
- /**
+ /**
* Set up a temporary password for the user
*
* User will have to reset the temporary password next time they log in.
@@ -911,4 +913,57 @@ public class UsersResource {
return clientSession;
}
+ @GET
+ @Path("{id}/groups")
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<GroupRepresentation> groupMembership(@PathParam("id") String id) {
+ auth.requireView();
+
+ UserModel user = session.users().getUserById(id, realm);
+ if (user == null) {
+ throw new NotFoundException("User not found");
+ }
+ List<GroupRepresentation> memberships = new LinkedList<>();
+ for (GroupModel group : user.getGroups()) {
+ memberships.add(ModelToRepresentation.toRepresentation(group, false));
+ }
+ return memberships;
+ }
+
+ @DELETE
+ @Path("{id}/groups/{groupId}")
+ @NoCache
+ public void removeMembership(@PathParam("id") String id, @PathParam("groupId") String groupId) {
+ auth.requireManage();
+
+ UserModel user = session.users().getUserById(id, realm);
+ if (user == null) {
+ throw new NotFoundException("User not found");
+ }
+ GroupModel group = session.realms().getGroupById(groupId, realm);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ if (user.isMemberOf(group)) user.leaveGroup(group);
+ }
+
+ @PUT
+ @Path("{id}/groups/{groupId}")
+ @NoCache
+ public void joinGroup(@PathParam("id") String id, @PathParam("groupId") String groupId) {
+ auth.requireManage();
+
+ UserModel user = session.users().getUserById(id, realm);
+ if (user == null) {
+ throw new NotFoundException("User not found");
+ }
+ GroupModel group = session.realms().getGroupById(groupId, realm);
+ if (group == null) {
+ throw new NotFoundException("Group not found");
+ }
+ if (!user.isMemberOf(group)) user.joinGroup(group);
+ }
+
+
}