This commit is contained in:
Bill Burke 2015-11-11 18:03:53 -05:00
parent 151c56a304
commit e25157655b
45 changed files with 1882 additions and 302 deletions

View file

@ -12,9 +12,9 @@ import java.util.Map;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class GroupRepresentation { public class GroupRepresentation {
private String id; protected String id;
private String name; protected String name;
protected Map<String, Object> attributes; protected Map<String, List<String>> attributes;
protected List<String> realmRoles; protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles; protected Map<String, List<String>> clientRoles;
protected List<GroupRepresentation> subGroups; protected List<GroupRepresentation> subGroups;
@ -51,17 +51,12 @@ public class GroupRepresentation {
this.clientRoles = clientRoles; this.clientRoles = clientRoles;
} }
public Map<String, Object> getAttributes() {
public Map<String, List<String>> getAttributes() {
return attributes; return attributes;
} }
// This method can be removed once we can remove backwards compatibility with Keycloak 1.3 (then getAttributes() can be changed to return Map<String, List<String>> ) public void setAttributes(Map<String, List<String>> attributes) {
@JsonIgnore
public Map<String, List<String>> getAttributesAsListValues() {
return (Map) attributes;
}
public void setAttributes(Map<String, Object> attributes) {
this.attributes = attributes; this.attributes = attributes;
} }

View file

@ -1,45 +0,0 @@
package org.keycloak.representations.idm;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserRoleMappingRepresentation {
protected String self; // link
protected String username;
protected Set<String> roles;
public String getSelf() {
return self;
}
public void setSelf(String self) {
this.self = self;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public UserRoleMappingRepresentation role(String role) {
if (this.roles == null) this.roles = new HashSet<String>();
this.roles.add(role);
return this;
}
}

View file

@ -25,6 +25,7 @@
<script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script> <script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script>
<script src="${resourceUrl}/lib/angular/angular-translate.js"></script> <script src="${resourceUrl}/lib/angular/angular-translate.js"></script>
<script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script> <script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script>
<script src="${resourceUrl}/lib/angular/treeview/angular.treeview.js"></script>
<script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script> <script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script>
<script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script> <script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script>
@ -37,6 +38,7 @@
<script src="${resourceUrl}/js/controllers/realm.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/realm.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/clients.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/clients.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/users.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/users.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/groups.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script> <script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/services.js" type="text/javascript"></script> <script src="${resourceUrl}/js/services.js" type="text/javascript"></script>
</head> </head>

View file

@ -7,7 +7,7 @@ var configUrl = consoleBaseUrl + "/config";
var auth = {}; var auth = {};
var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'pascalprecht.translate', 'ngCookies', 'ngSanitize']); var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'ui.bootstrap', 'ui.select2', 'angularFileUpload', 'angularTreeview', 'pascalprecht.translate', 'ngCookies', 'ngSanitize']);
var resourceRequests = 0; var resourceRequests = 0;
var loadingTimer = -1; var loadingTimer = -1;
@ -574,6 +574,73 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'RoleListCtrl' controller : 'RoleListCtrl'
}) })
.when('/realms/:realm/groups', {
templateUrl : resourceUrl + '/partials/group-list.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
groups : function(GroupListLoader) {
return GroupListLoader();
}
},
controller : 'GroupListCtrl'
})
.when('/create/group/:realm/parent/:parentId', {
templateUrl : resourceUrl + '/partials/create-group.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
parentId : function($route) {
return $route.current.params.parentId;
}
},
controller : 'GroupCreateCtrl'
})
.when('/realms/:realm/groups/:group', {
templateUrl : resourceUrl + '/partials/group-detail.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
group : function(GroupLoader) {
return GroupLoader();
}
},
controller : 'GroupDetailCtrl'
})
.when('/realms/:realm/groups/:group/attributes', {
templateUrl : resourceUrl + '/partials/group-attributes.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
group : function(GroupLoader) {
return GroupLoader();
}
},
controller : 'GroupDetailCtrl'
})
.when('/realms/:realm/groups/:group/role-mappings', {
templateUrl : resourceUrl + '/partials/group-role-mappings.html',
resolve : {
realm : function(RealmLoader) {
return RealmLoader();
},
group : function(GroupLoader) {
return GroupLoader();
},
clients : function(ClientListLoader) {
return ClientListLoader();
},
client : function() {
return {};
}
},
controller : 'GroupRoleMappingCtrl'
})
.when('/create/role/:realm/clients/:client', { .when('/create/role/:realm/clients/:client', {
templateUrl : resourceUrl + '/partials/client-role-detail.html', templateUrl : resourceUrl + '/partials/client-role-detail.html',
@ -1856,6 +1923,15 @@ module.directive('kcTabsUser', function () {
} }
}); });
module.directive('kcTabsGroup', function () {
return {
scope: true,
restrict: 'E',
replace: true,
templateUrl: resourceUrl + '/templates/kc-tabs-group.html'
}
});
module.directive('kcTabsClient', function () { module.directive('kcTabsClient', function () {
return { return {
scope: true, scope: true,

View file

@ -0,0 +1,321 @@
module.controller('GroupListCtrl', function($scope, $route, realm, groups, Groups, Group, GroupChildren, Notifications, $location, Dialog) {
$scope.realm = realm;
$scope.groupList = [
{"id" : "realm", "name": "Groups",
"subGroups" : groups}
];
$scope.tree = [];
$scope.edit = function(selected) {
$location.url("/realms/" + realm.realm + "/groups/" + selected.id);
}
$scope.cut = function(selected) {
$scope.cutNode = selected;
}
$scope.isDisabled = function() {
if (!$scope.tree.currentNode) return true;
return $scope.tree.currentNode.id == 'realm';
}
$scope.paste = function(selected) {
if (selected == null) return;
if ($scope.cutNode == null) return;
if (selected.id == 'realm') {
Groups.save({realm: realm.realm}, {id:$scope.cutNode.id}, function() {
$route.reload();
Notifications.success("Group moved.");
});
} else {
GroupChildren.save({realm: realm.realm, groupId: selected.id}, {id:$scope.cutNode.id}, function() {
$route.reload();
Notifications.success("Group moved.");
});
}
}
$scope.remove = function(selected) {
if (selected == null) return;
Dialog.confirmDelete(selected.name, 'group', function() {
Group.remove({ realm: realm.realm, groupId : selected.id }, function() {
$route.reload();
Notifications.success("The group has been deleted.");
});
});
}
$scope.createGroup = function(selected) {
var parent = 'realm';
if (selected) {
parent = selected.id;
}
$location.url("/create/group/" + realm.realm + '/parent/' + parent);
}
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;
}
});
module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, Groups, Group, GroupChildren, Notifications, $location) {
$scope.realm = realm;
$scope.group = {};
$scope.save = function() {
console.log('save!!!');
if (parentId == 'realm') {
console.log('realm')
Groups.save({realm: realm.realm, groupId: parentId}, $scope.group, function(data, headers) {
var l = headers().location;
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.realm + "/groups/" + id);
Notifications.success("Group Created.");
})
} else {
GroupChildren.save({realm: realm.realm, groupId: parentId}, $scope.group, function(data, headers) {
var l = headers().location;
var id = l.substring(l.lastIndexOf("/") + 1);
$location.url("/realms/" + realm.realm + "/groups/" + id);
Notifications.success("Group Created.");
})
}
}
$scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/groups");
};
});
module.controller('GroupTabCtrl', function(Dialog, $scope, Current, Group, Notifications, $location) {
$scope.removeGroup = function() {
Dialog.confirmDelete($scope.group.name, 'group', function() {
Group.remove({
realm : Current.realm.realm,
groupId : $scope.group.id
}, function() {
$location.url("/realms/" + Current.realm.realm + "/groups");
Notifications.success("The group has been deleted.");
});
});
};
});
module.controller('GroupDetailCtrl', function(Dialog, $scope, realm, group, Group, Notifications, $location) {
$scope.realm = realm;
if (!group.attributes) {
group.attributes = {}
}
convertAttributeValuesToString(group);
$scope.group = angular.copy(group);
$scope.changed = false; // $scope.create;
$scope.$watch('group', function() {
if (!angular.equals($scope.group, group)) {
$scope.changed = true;
}
}, true);
$scope.save = function() {
convertAttributeValuesToLists();
Group.update({
realm: realm.realm,
groupId: $scope.group.id
}, $scope.group, function () {
$scope.changed = false;
convertAttributeValuesToString($scope.group);
group = angular.copy($scope.group);
Notifications.success("Your changes have been saved to the group.");
});
};
function convertAttributeValuesToLists() {
var attrs = $scope.group.attributes;
for (var attribute in attrs) {
if (typeof attrs[attribute] === "string") {
var attrVals = attrs[attribute].split("##");
attrs[attribute] = attrVals;
}
}
}
function convertAttributeValuesToString(group) {
var attrs = group.attributes;
for (var attribute in attrs) {
if (typeof attrs[attribute] === "object") {
var attrVals = attrs[attribute].join("##");
attrs[attribute] = attrVals;
}
}
}
$scope.reset = function() {
$scope.group = angular.copy(group);
$scope.changed = false;
};
$scope.cancel = function() {
$location.url("/realms/" + realm.realm + "/groups");
};
$scope.addAttribute = function() {
$scope.group.attributes[$scope.newAttribute.key] = $scope.newAttribute.value;
delete $scope.newAttribute;
}
$scope.removeAttribute = function(key) {
delete $scope.group.attributes[key];
}
});
module.controller('GroupRoleMappingCtrl', function($scope, $http, realm, group, clients, client, Notifications, GroupRealmRoleMapping,
GroupClientRoleMapping, GroupAvailableRealmRoleMapping, GroupAvailableClientRoleMapping,
GroupCompositeRealmRoleMapping, GroupCompositeClientRoleMapping) {
$scope.realm = realm;
$scope.group = group;
$scope.selectedRealmRoles = [];
$scope.selectedRealmMappings = [];
$scope.realmMappings = [];
$scope.clients = clients;
$scope.client = client;
$scope.clientRoles = [];
$scope.clientComposite = [];
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
$scope.clientMappings = [];
$scope.dummymodel = [];
$scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.addRealmRole = function() {
var roles = $scope.selectedRealmRoles;
$scope.selectedRealmRoles = [];
$http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
roles).success(function() {
$scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.selectedRealmMappings = [];
$scope.selectRealmRoles = [];
if ($scope.targetClient) {
console.log('load available');
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
}
Notifications.success("Role mappings updated.");
});
};
$scope.deleteRealmRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/realm',
{data : $scope.selectedRealmMappings, headers : {"content-type" : "application/json"}}).success(function() {
$scope.realmMappings = GroupRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.selectedRealmMappings = [];
$scope.selectRealmRoles = [];
if ($scope.targetClient) {
console.log('load available');
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
}
Notifications.success("Role mappings updated.");
});
};
$scope.addClientRole = function() {
$http.post(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
$scope.selectedClientRoles).success(function() {
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
Notifications.success("Role mappings updated.");
});
};
$scope.deleteClientRole = function() {
$http.delete(authUrl + '/admin/realms/' + realm.realm + '/groups/' + group.id + '/role-mappings/clients/' + $scope.targetClient.id,
{data : $scope.selectedClientMappings, headers : {"content-type" : "application/json"}}).success(function() {
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
$scope.realmComposite = GroupCompositeRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
$scope.realmRoles = GroupAvailableRealmRoleMapping.query({realm : realm.realm, groupId : group.id});
Notifications.success("Role mappings updated.");
});
};
$scope.changeClient = function() {
if ($scope.targetClient) {
$scope.clientComposite = GroupCompositeClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientRoles = GroupAvailableClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
$scope.clientMappings = GroupClientRoleMapping.query({realm : realm.realm, groupId : group.id, client : $scope.targetClient.id});
} else {
$scope.clientRoles = null;
$scope.clientMappings = null;
$scope.clientComposite = null;
}
$scope.selectedClientRoles = [];
$scope.selectedClientMappings = [];
};
});

View file

@ -1994,3 +1994,7 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, conf

View file

@ -458,6 +458,24 @@ module.factory('AuthenticationConfigLoader', function(Loader, AuthenticationConf
}); });
}); });
module.factory('GroupListLoader', function(Loader, Groups, $route, $q) {
return Loader.query(Groups, function() {
return {
realm : $route.current.params.realm
}
});
});
module.factory('GroupLoader', function(Loader, Group, $route, $q) {
return Loader.get(Group, function() {
return {
realm : $route.current.params.realm,
groupId : $route.current.params.group
}
});
});

View file

@ -1442,6 +1442,78 @@ module.service('SelectRoleDialog', function($modal) {
return dialog return dialog
}); });
module.factory('Group', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId', {
realm : '@realm',
userId : '@groupId'
}, {
update : {
method : 'PUT'
}
});
});
module.factory('GroupChildren', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/children', {
realm : '@realm',
groupId : '@groupId'
});
});
module.factory('Groups', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups', {
realm : '@realm'
});
});
module.factory('GroupRealmRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/realm', {
realm : '@realm',
groupId : '@groupId'
});
});
module.factory('GroupCompositeRealmRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/realm/composite', {
realm : '@realm',
groupId : '@groupId'
});
});
module.factory('GroupAvailableRealmRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/realm/available', {
realm : '@realm',
groupId : '@groupId'
});
});
module.factory('GroupClientRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/clients/:client', {
realm : '@realm',
groupId : '@groupId',
client : "@client"
});
});
module.factory('GroupAvailableClientRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/clients/:client/available', {
realm : '@realm',
groupId : '@groupId',
client : "@client"
});
});
module.factory('GroupCompositeClientRoleMapping', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/groups/:groupId/role-mappings/clients/:client/composite', {
realm : '@realm',
groupId : '@groupId',
client : "@client"
});
});

View file

@ -0,0 +1,25 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<div>
<h1>Create Group</h1>
</div>
<form class="form-horizontal" name="clientForm" novalidate>
<fieldset class="border-top">
<div class="form-group">
<label class="col-md-2 control-label"for="name">Name <span class="required">*</span></label>
<div class="col-md-6">
<input class="form-control" type="text" id="name" name="name" data-ng-model="group.name" autofocus
required >
</div>
</div>
</fieldset>
<div class="form-group">
<div class="col-md-10 col-md-offset-2">
<button kc-save>Save</button>
<button kc-cancel data-ng-click="cancel()">Cancel</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,45 @@
<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}}/groups">Groups</a></li>
<li>{{group.name}}</li>
</ol>
<kc-tabs-group></kc-tabs-group>
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageUsers">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="(key, value) in group.attributes">
<td>{{key}}</td>
<td><input ng-model="group.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" data-ng-click="removeAttribute(key)">Delete</button>
</td>
</tr>
<tr>
<td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
<td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" data-ng-click="addAttribute()">Add</button>
</td>
</tr>
</tbody>
</table>
<div class="form-group" data-ng-show="access.manageUsers">
<div class="col-md-12">
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed">Cancel</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,28 @@
<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}}/groups">Groups</a></li>
<li>{{group.name}}</li>
</ol>
<kc-tabs-group></kc-tabs-group>
<form class="form-horizontal" name="clientForm" novalidate>
<fieldset class="border-top">
<div class="form-group">
<label class="col-md-2 control-label"for="name">Name <span class="required">*</span></label>
<div class="col-md-6">
<input class="form-control" type="text" id="name" name="name" data-ng-model="group.name" autofocus
required >
</div>
</div>
</fieldset>
<div class="form-group" data-ng-show="access.manageUsers">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">Save</button>
<button kc-reset data-ng-disabled="!changed" data-ng-click="cancel()">Cancel</button>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -0,0 +1,40 @@
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
<h1>
<span>User Groups</span>
<kc-tooltip>User groups</kc-tooltip>
</h1>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
<div class="form-inline">
<div class="pull-right" data-ng-show="access.manageUsers">
<button id="createGroup" class="btn btn-default" ng-click="createGroup(tree.currentNode)">New</button>
<button id="editGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="edit(tree.currentNode)">Edit</button>
<button id="cutGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="cut(tree.currentNode)">Cut</button>
<button id="pasteGroup" ng-disabled="!cutNode" class="btn btn-default" ng-click="paste(tree.currentNode)">Paste</button>
<button id="removeGroup" ng-disabled="isDisabled()" class="btn btn-default" ng-click="remove(tree.currentNode)">Delete</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>
<kc-menu></kc-menu>

View file

@ -0,0 +1,101 @@
<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}}/groups">Groups</a></li>
<li>{{group.name}}</li>
</ol>
<kc-tabs-group></kc-tabs-group>
<form class="form-horizontal" name="realmForm" novalidate>
<div class="form-group" kc-read-only="!access.manageUsers">
<label class="col-md-2 control-label" class="control-label">Realm Roles</label>
<div class="col-md-10">
<div class="row">
<div class="col-md-3">
<label class="control-label" for="available">Available Roles</label>
<select id="available" class="form-control" multiple size="5"
ng-multiple="true"
ng-model="selectedRealmRoles"
ng-options="r.name for r in realmRoles">
</select>
<button ng-disabled="selectedRealmRoles.length == 0" ng-disabled="c.length == 0" class="btn btn-default" type="submit" ng-click="addRealmRole()">
Add selected <i class="fa fa-angle-right"></i>
</button>
<kc-tooltip>Realm roles that can be assigned to the group.</kc-tooltip>
</div>
<div class="col-md-3">
<label class="control-label" for="assigned">Assigned Roles</label>
<kc-tooltip>Realm roles mapped to the group</kc-tooltip>
<select id="assigned" class="form-control" multiple size=5
ng-multiple="true"
ng-model="selectedRealmMappings"
ng-options="r.name for r in realmMappings">
</select>
<button ng-disabled="selectedRealmMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteRealmRole()">
<i class="fa fa-angle-double-left"></i> Remove selected
</button>
</div>
<div class="col-md-3">
<label class="control-label" for="realm-composite">Effective Roles</label>
<kc-tooltip>All realm role mappings. Some roles here might be inherited from a mapped composite role.</kc-tooltip>
<select id="realm-composite" class="form-control" multiple size=5
disabled="true"
ng-model="dummymodel"
ng-options="r.name for r in realmComposite">
</select>
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" class="control-label">
<span>Client Roles</span>
<select class="form-control" id="clients" name="clients" ng-change="changeClient()" ng-model="targetClient" ng-options="a.clientId for a in clients" ng-disabled="false"></select>
</label>
<div class="col-md-10" kc-read-only="!access.manageUsers">
<div class="row" data-ng-hide="targetClient">
<div class="col-md-4"><span class="text-muted">Select client to view roles for client</span></div>
</div>
<div class="row" data-ng-show="targetClient">
<div class="col-md-3">
<label class="control-label" for="available-client">Available Roles</label>
<kc-tooltip>Assignable roles from this client.</kc-tooltip>
<select id="available-client" class="form-control" multiple size="5"
ng-multiple="true"
ng-model="selectedClientRoles"
ng-options="r.name for r in clientRoles">
</select>
<button ng-disabled="selectedClientRoles.length == 0" class="btn btn-default" type="submit" ng-click="addClientRole()">
Add selected <i class="fa fa-angle-double-right"></i>
</button>
</div>
<div class="col-md-3">
<label class="control-label" for="assigned-client">Assigned Roles</label>
<kc-tooltip>Role mappings for this client.</kc-tooltip>
<select id="assigned-client" class="form-control" multiple size=5
ng-multiple="true"
ng-model="selectedClientMappings"
ng-options="r.name for r in clientMappings">
</select>
<button ng-disabled="selectedClientMappings.length == 0" class="btn btn-default" type="submit" ng-click="deleteClientRole()">
<i class="fa fa-angle-double-left"></i> Remove selected
</button>
</div>
<div class="col-md-3">
<label class="control-label" for="client-composite">Effective Roles <span tooltip-placement="right" tooltip-trigger="mouseover mouseout" tooltip="Role mappings for this client. Some roles here might be inherited from a mapped composite role." class="fa fa-info-circle"></span></label>
<select id="client-composite" class="form-control" multiple size=5
disabled="true"
ng-model="dummymodel"
ng-options="r.name for r in clientComposite">
</select>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<kc-menu></kc-menu>

View file

@ -40,7 +40,8 @@
<div class="nav-category" data-ng-show="current.realm"> <div class="nav-category" data-ng-show="current.realm">
<h2>Manage</h2> <h2>Manage</h2>
<ul class="nav nav-pills nav-stacked"> <ul class="nav nav-pills nav-stacked">
<li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-users"></span> Users</a></li> <li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'groups' || path[1] == 'group') && 'active'"><a href="#/realms/{{realm.realm}}/groups"><span class="pficon pficon-users"></span> Groups</a></li>
<li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-user"></span> Users</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> Sessions</a></li> <li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> Sessions</a></li>
<li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events' || path[2] == 'events-settings') && 'active'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> Events</a></li> <li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events' || path[2] == 'events-settings') && 'active'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> Events</a></li>
</ul> </ul>

View file

@ -0,0 +1,13 @@
<div data-ng-controller="GroupTabCtrl">
<h1>
{{group.name|capitalize}}
<i id="removeGroup" class="pficon pficon-delete clickable" data-ng-show="access.manageUsers" data-ng-click="removeGroup()"></i>
</h1>
<ul class="nav nav-tabs">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/groups/{{group.id}}">Settings</a></li>
<li ng-class="{active: path[4] == 'attributes'}"><a href="#/realms/{{realm.realm}}/groups/{{group.id}}/attributes">Attributes</a></li>
<li ng-class="{active: path[4] == 'role-mappings'}" ><a href="#/realms/{{realm.realm}}/groups/{{group.id}}/role-mappings">Role Mappings</a></li>
<li ng-class="{active: path[4] == 'members'}"><a href="#/realms/{{realm.realm}}/groups/{{group.id}}/members">Members</a></li>
</ul>
</div>

View file

@ -1,3 +1,3 @@
parent=base parent=base
import=common/keycloak import=common/keycloak
styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css styles=lib/patternfly/css/patternfly.css lib/select2-3.4.1/select2.css css/styles.css lib/angular/treeview/css/angular.treeview.css

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Steve
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,122 @@
Angular Treeview
================
Pure [AngularJS](http://www.angularjs.org) based tree menu directive.
[![ScreenShot](https://github.com/eu81273/angular.treeview/raw/master/img/preview.png)](http://jsfiddle.net/eu81273/8LWUc/)
## Installation
Copy the script and css into your project and add a script and link tag to your page.
```html
<script type="text/javascript" src="/angular.treeview.js"></script>
<link rel="stylesheet" type="text/css" href="css/angular.treeview.css">
```
Add a dependency to your application module.
```javascript
angular.module('myApp', ['angularTreeview']);
```
Add a tree to your application. See [Usage](#usage).
## Usage
Attributes of angular treeview are below.
- angular-treeview: the treeview directive.
- tree-id : each tree's unique id.
- tree-model : the tree model on $scope.
- node-id : each node's id.
- node-label : each node's label.
- node-children: each node's children.
Here is a simple example.
```html
<div
data-angular-treeview="true"
data-tree-id="abc"
data-tree-model="treedata"
data-node-id="id"
data-node-label="label"
data-node-children="children" >
</div>
```
Example model:
```javascript
$scope.treedata =
[
{ "label" : "User", "id" : "role1", "children" : [
{ "label" : "subUser1", "id" : "role11", "children" : [] },
{ "label" : "subUser2", "id" : "role12", "children" : [
{ "label" : "subUser2-1", "id" : "role121", "children" : [
{ "label" : "subUser2-1-1", "id" : "role1211", "children" : [] },
{ "label" : "subUser2-1-2", "id" : "role1212", "children" : [] }
]}
]}
]},
{ "label" : "Admin", "id" : "role2", "children" : [] },
{ "label" : "Guest", "id" : "role3", "children" : [] }
];
```
## Selection
If tree node is selected, then that selected tree node is saved to $scope."TREE ID".currentNode. By using $watch, the controller can recognize the tree selection.
```javascript
$scope.$watch( 'abc.currentNode', function( newObj, oldObj ) {
if( $scope.abc && angular.isObject($scope.abc.currentNode) ) {
console.log( 'Node Selected!!' );
console.log( $scope.abc.currentNode );
}
}, false);
```
## Examples
#### Basic example
[![ScreenShot](https://github.com/eu81273/angular.treeview/raw/master/img/jsfiddle01.png)](http://jsfiddle.net/eu81273/8LWUc/)
[jsFiddle - http://jsfiddle.net/eu81273/8LWUc/](http://jsfiddle.net/eu81273/8LWUc/)
#### Multiple treeview example
[![ScreenShot](https://github.com/eu81273/angular.treeview/raw/master/img/jsfiddle02.png)](http://jsfiddle.net/eu81273/b9Pnw/)
[jsFiddle - http://jsfiddle.net/eu81273/b9Pnw/](http://jsfiddle.net/eu81273/b9Pnw/)
## Browser Compatibility
Same with AngularJS. Safari, Chrome, Firefox, Opera, IE8, IE9 and mobile browsers (Android, Chrome Mobile, iOS Safari).
## Changelogs
#### version 0.1.6
- Fixed the bug that 'null' string appears before each 'div' generated. (Thanks to Iaac)
#### version 0.1.5
- support multiple treeview. (Thanks to Axel Pesme)
#### version 0.1.4
- prevented memory leaks.
#### version 0.1.3
- removed unnecessary codes.
#### version 0.1.2
- removed some jQuery dependency. (Issue #2)
## License
The MIT License.
Copyright ⓒ 2013 AHN JAE-HA.
See [LICENSE](https://github.com/eu81273/angular.treeview/blob/master/LICENSE)

View file

@ -0,0 +1,97 @@
/*
@license Angular Treeview version 0.1.6
2013 AHN JAE-HA http://github.com/eu81273/angular.treeview
License: MIT
[TREE attribute]
angular-treeview: the treeview directive
tree-id : each tree's unique id.
tree-model : the tree model on $scope.
node-id : each node's id
node-label : each node's label
node-children: each node's children
<div
data-angular-treeview="true"
data-tree-id="tree"
data-tree-model="roleList"
data-node-id="roleId"
data-node-label="roleName"
data-node-children="children" >
</div>
*/
(function ( angular ) {
'use strict';
angular.module( 'angularTreeview', [] ).directive( 'treeModel', ['$compile', function( $compile ) {
return {
restrict: 'A',
link: function ( scope, element, attrs ) {
//tree id
var treeId = attrs.treeId;
//tree model
var treeModel = attrs.treeModel;
//node id
var nodeId = attrs.nodeId || 'id';
//node label
var nodeLabel = attrs.nodeLabel || 'label';
//children
var nodeChildren = attrs.nodeChildren || 'children';
//tree template
var template =
'<ul>' +
'<li data-ng-repeat="node in ' + treeModel + '">' +
'<i ng-class="getGroupClass(node)" data-ng-click="' + treeId + '.selectNodeHead(node)"></i>' +
'<span data-ng-class="getSelectedClass(node)" ng-dblclick="edit(node)" data-ng-click="' + treeId + '.selectNodeLabel(node)">{{node.' + nodeLabel + '}}</span>' +
'<div data-ng-hide="node.collapsed" data-tree-id="' + treeId + '" data-tree-model="node.' + nodeChildren + '" data-node-id=' + nodeId + ' data-node-label=' + nodeLabel + ' data-node-children=' + nodeChildren + '></div>' +
'</li>' +
'</ul>';
//check tree id, tree model
if( treeId && treeModel ) {
//root node
if( attrs.angularTreeview ) {
//create tree object if not exists
scope[treeId] = scope[treeId] || {};
//if node head clicks,
scope[treeId].selectNodeHead = scope[treeId].selectNodeHead || function( selectedNode ){
//Collapse or Expand
selectedNode.collapsed = !selectedNode.collapsed;
scope[treeId].selectNodeLabel(selectedNode);
};
//if node label clicks,
scope[treeId].selectNodeLabel = scope[treeId].selectNodeLabel || function( selectedNode ){
//remove highlight from previous node
if( scope[treeId].currentNode && scope[treeId].currentNode.selected ) {
scope[treeId].currentNode.selected = undefined;
}
//set highlight to selected node
selectedNode.selected = 'selected';
//set currentNode
scope[treeId].currentNode = selectedNode;
};
}
//Rendering template.
element.html('').append( $compile( template )( scope ) );
}
}
};
}]);
})( angular );

View file

@ -0,0 +1,9 @@
/*
@license Angular Treeview version 0.1.6
2013 AHN JAE-HA http://github.com/eu81273/angular.treeview
License: MIT
*/
(function(f){f.module("angularTreeview",[]).directive("treeModel",function($compile){return{restrict:"A",link:function(b,h,c){var a=c.treeId,g=c.treeModel,e=c.nodeLabel||"label",d=c.nodeChildren||"children",e='<ul><li data-ng-repeat="node in '+g+'"><i class="collapsed" data-ng-show="node.'+d+'.length && node.collapsed" data-ng-click="'+a+'.selectNodeHead(node)"></i><i class="expanded" data-ng-show="node.'+d+'.length && !node.collapsed" data-ng-click="'+a+'.selectNodeHead(node)"></i><i class="normal" data-ng-hide="node.'+
d+'.length"></i> <span data-ng-class="node.selected" data-ng-click="'+a+'.selectNodeLabel(node)">{{node.'+e+'}}</span><div data-ng-hide="node.collapsed" data-tree-id="'+a+'" data-tree-model="node.'+d+'" data-node-id='+(c.nodeId||"id")+" data-node-label="+e+" data-node-children="+d+"></div></li></ul>";a&&g&&(c.angularTreeview&&(b[a]=b[a]||{},b[a].selectNodeHead=b[a].selectNodeHead||function(a){a.collapsed=!a.collapsed},b[a].selectNodeLabel=b[a].selectNodeLabel||function(c){b[a].currentNode&&b[a].currentNode.selected&&
(b[a].currentNode.selected=void 0);c.selected="selected";b[a].currentNode=c}),h.html('').append($compile(e)(b)))}}})})(angular);

View file

@ -0,0 +1,99 @@
div[angular-treeview] {
/* prevent user selection */
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
/* default */
font-family: Tahoma;
font-size:13px;
color: #555;
text-decoration: none;
}
div[tree-model] ul {
margin: 0;
padding: 0;
list-style: none;
border: none;
overflow: hidden;
}
div[tree-model] li {
position: relative;
padding: 0 0 0 20px;
line-height: 20px;
}
div[tree-model] li .expanded {
padding: 1px 10px;
background-image: url("../img/folder.png");
background-repeat: no-repeat;
}
div[tree-model] li .collapsed {
padding: 1px 10px;
background-image: url("../img/folder-closed.png");
background-repeat: no-repeat;
}
div[tree-model] li .normal {
padding: 1px 10px;
background-image: url("../img/file.png");
background-repeat: no-repeat;
}
div[tree-model] li i, div[tree-model] li span {
cursor: pointer;
}
div[tree-model] li .selected {
background-color: #aaddff;
font-weight: bold;
padding: 1px 5px;
}
div[tree-model] li .cut {
font-weight: bold;
color: gray
}
/*
.angular-ui-tree-handle {
cursor: grab;
text-decoration: none;
font-weight: bold;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
min-height: 20px;
line-height: 20px;
}
*/
.angular-ui-tree-handle {
/* background: #f8faff; */
/*
color: #7c9eb2; */
border: 1px solid #dae2ea;
padding: 10px 10px;
cursor: pointer;
}
.expanded-folder {
padding: 1px 10px;
background-image: url("../img/folder.png");
background-repeat: no-repeat;
cursor: pointer;
}
.collapsed-folder {
padding: 1px 10px;
background-image: url("../img/folder-closed.png");
background-repeat: no-repeat;
cursor: pointer;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

View file

@ -8,7 +8,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface GroupModel { public interface GroupModel extends RoleMapperModel {
String getId(); String getId();
String getName(); String getName();
@ -41,18 +41,12 @@ public interface GroupModel {
Map<String, List<String>> getAttributes(); Map<String, List<String>> getAttributes();
Set<RoleModel> getRealmRoleMappings();
Set<RoleModel> getClientRoleMappings(ClientModel app);
boolean hasRole(RoleModel role);
void grantRole(RoleModel role);
Set<RoleModel> getRoleMappings();
void deleteRoleMapping(RoleModel role);
GroupModel getParent(); GroupModel getParent();
String getParentId();
Set<GroupModel> getSubGroups(); Set<GroupModel> getSubGroups();
/** /**
* You must also call joinGroup on the parent group. * You must also call addChild on the parent group, addChild on RealmModel if there is no parent group
* *
* @param group * @param group
*/ */

View file

@ -329,11 +329,18 @@ public interface RealmModel extends RoleContainerModel {
String getDefaultLocale(); String getDefaultLocale();
void setDefaultLocale(String locale); void setDefaultLocale(String locale);
GroupModel createGroup(String name);
/**
* Move Group to top realm level. Basically just sets group parent to null. You need to call this though
* to make sure caches are set properly
*
* @param subGroup
*/
void addTopLevelGroup(GroupModel subGroup);
GroupModel getGroupById(String id); GroupModel getGroupById(String id);
List<GroupModel> getGroups(); List<GroupModel> getGroups();
List<GroupModel> getTopLevelGroups(); List<GroupModel> getTopLevelGroups();
boolean removeGroup(GroupModel group); boolean removeGroup(GroupModel group);
void moveGroup(GroupModel group, GroupModel toParent);
} }

View file

@ -0,0 +1,21 @@
package org.keycloak.models;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface RoleMapperModel {
Set<RoleModel> getRealmRoleMappings();
Set<RoleModel> getClientRoleMappings(ClientModel app);
boolean hasRole(RoleModel role);
void grantRole(RoleModel role);
Set<RoleModel> getRoleMappings();
void deleteRoleMapping(RoleModel role);
}

View file

@ -8,7 +8,7 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public interface UserModel { public interface UserModel extends RoleMapperModel {
String USERNAME = "username"; String USERNAME = "username";
String LAST_NAME = "lastName"; String LAST_NAME = "lastName";
String FIRST_NAME = "firstName"; String FIRST_NAME = "firstName";
@ -94,13 +94,6 @@ public interface UserModel {
void updateCredentialDirectly(UserCredentialValueModel cred); void updateCredentialDirectly(UserCredentialValueModel cred);
Set<RoleModel> getRealmRoleMappings();
Set<RoleModel> getClientRoleMappings(ClientModel app);
boolean hasRole(RoleModel role);
void grantRole(RoleModel role);
Set<RoleModel> getRoleMappings();
void deleteRoleMapping(RoleModel role);
Set<GroupModel> getGroups(); Set<GroupModel> getGroups();
void joinGroup(GroupModel group); void joinGroup(GroupModel group);
void leaveGroup(GroupModel group); void leaveGroup(GroupModel group);

View file

@ -6,6 +6,7 @@ import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -30,6 +31,7 @@ import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation;
@ -58,6 +60,59 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ModelToRepresentation { public class ModelToRepresentation {
public static GroupRepresentation toRepresentation(GroupModel group, boolean full) {
GroupRepresentation rep = new GroupRepresentation();
rep.setId(group.getId());
rep.setName(group.getName());
if (!full) return rep;
// Role mappings
Set<RoleModel> roles = group.getRoleMappings();
List<String> realmRoleNames = new ArrayList<>();
Map<String, List<String>> clientRoleNames = new HashMap<>();
for (RoleModel role : roles) {
if (role.getContainer() instanceof RealmModel) {
realmRoleNames.add(role.getName());
} else {
ClientModel client = (ClientModel)role.getContainer();
String clientId = client.getClientId();
List<String> currentClientRoles = clientRoleNames.get(clientId);
if (currentClientRoles == null) {
currentClientRoles = new ArrayList<>();
clientRoleNames.put(clientId, currentClientRoles);
}
currentClientRoles.add(role.getName());
}
}
rep.setRealmRoles(realmRoleNames);
rep.setClientRoles(clientRoleNames);
Map<String, List<String>> attributes = group.getAttributes();
rep.setAttributes(attributes);
return rep;
}
public static List<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full) {
List<GroupRepresentation> hierarchy = new LinkedList<>();
List<GroupModel> groups = realm.getTopLevelGroups();
if (groups == null) return hierarchy;
for (GroupModel group : groups) {
GroupRepresentation rep = toGroupHierarchy(group, full);
hierarchy.add(rep);
}
return hierarchy;
}
public static GroupRepresentation toGroupHierarchy(GroupModel group, boolean full) {
GroupRepresentation rep = toRepresentation(group, full);
List<GroupRepresentation> subGroups = new LinkedList<>();
for (GroupModel subGroup : group.getSubGroups()) {
subGroups.add(toGroupHierarchy(subGroup, full));
}
rep.setSubGroups(subGroups);
return rep;
}
public static UserRepresentation toRepresentation(UserModel user) { public static UserRepresentation toRepresentation(UserModel user) {
UserRepresentation rep = new UserRepresentation(); UserRepresentation rep = new UserRepresentation();
rep.setId(user.getId()); rep.setId(user.getId());

View file

@ -177,6 +177,11 @@ public class GroupAdapter implements GroupModel {
return realm.getGroupById(group.getParentId()); return realm.getGroupById(group.getParentId());
} }
@Override
public String getParentId() {
return group.getParentId();
}
@Override @Override
public Set<GroupModel> getSubGroups() { public Set<GroupModel> getSubGroups() {
Set<GroupModel> subGroups = new HashSet<>(); Set<GroupModel> subGroups = new HashSet<>();

View file

@ -610,6 +610,15 @@ public class RealmAdapter implements RealmModel {
return null; return null;
} }
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
if (group.getParentId() != null) {
group.getParent().removeChild(group);
}
group.setParent(toParent);
if (toParent != null) toParent.addChild(group);
else addTopLevelGroup(group);
}
@Override @Override
public List<GroupModel> getGroups() { public List<GroupModel> getGroups() {
List<GroupModel> list = new LinkedList<>(); List<GroupModel> list = new LinkedList<>();
@ -1819,4 +1828,13 @@ public class RealmAdapter implements RealmModel {
return mapper; return mapper;
} }
@Override
public GroupModel createGroup(String name) {
return null;
} }
@Override
public void addTopLevelGroup(GroupModel subGroup) {
}
}

View file

@ -197,6 +197,12 @@ public class GroupAdapter implements GroupModel {
return keycloakSession.realms().getGroupById(cached.getParentId(), realm); return keycloakSession.realms().getGroupById(cached.getParentId(), realm);
} }
@Override
public String getParentId() {
if (updated != null) return updated.getParentId();
return cached.getParentId();
}
@Override @Override
public Set<GroupModel> getSubGroups() { public Set<GroupModel> getSubGroups() {
if (updated != null) return updated.getSubGroups(); if (updated != null) return updated.getSubGroups();
@ -214,6 +220,8 @@ public class GroupAdapter implements GroupModel {
return subGroups; return subGroups;
} }
@Override @Override
public void setParent(GroupModel group) { public void setParent(GroupModel group) {
getDelegateForUpdate(); getDelegateForUpdate();

View file

@ -1272,7 +1272,7 @@ public class RealmAdapter implements RealmModel {
@Override @Override
public List<GroupModel> getGroups() { public List<GroupModel> getGroups() {
if (updated != null) return updated.getGroups(); if (updated != null) return updated.getGroups();
if (cached.getGroups().isEmpty()) return null; if (cached.getGroups().isEmpty()) return Collections.EMPTY_LIST;
List<GroupModel> list = new LinkedList<>(); List<GroupModel> list = new LinkedList<>();
for (String id : cached.getGroups()) { for (String id : cached.getGroups()) {
GroupModel group = cacheSession.getGroupById(id, this); GroupModel group = cacheSession.getGroupById(id, this);
@ -1300,4 +1300,23 @@ public class RealmAdapter implements RealmModel {
getDelegateForUpdate(); getDelegateForUpdate();
return updated.removeGroup(group); return updated.removeGroup(group);
} }
@Override
public GroupModel createGroup(String name) {
getDelegateForUpdate();
return updated.createGroup(name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
getDelegateForUpdate();
updated.addTopLevelGroup(subGroup);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
getDelegateForUpdate();
updated.moveGroup(group, toParent);
}
} }

View file

@ -30,7 +30,7 @@ public class CachedGroup implements Serializable {
this.id = group.getId(); this.id = group.getId();
this.realm = realm.getId(); this.realm = realm.getId();
this.name = group.getName(); this.name = group.getName();
if (group.getParent() != null) this.parentId = group.getParent().getId(); this.parentId = group.getParentId();
this.attributes.putAll(group.getAttributes()); this.attributes.putAll(group.getAttributes());
for (RoleModel role : group.getRoleMappings()) { for (RoleModel role : group.getRoleMappings()) {

View file

@ -87,6 +87,13 @@ public class GroupAdapter implements GroupModel {
return realm.getGroupById(parent.getId()); return realm.getGroupById(parent.getId());
} }
@Override
public String getParentId() {
GroupEntity parent = group.getParent();
if (parent == null) return null;
return parent.getId();
}
public static GroupEntity toEntity(GroupModel model, EntityManager em) { public static GroupEntity toEntity(GroupModel model, EntityManager em) {
if (model instanceof GroupAdapter) { if (model instanceof GroupAdapter) {
return ((GroupAdapter)model).getGroup(); return ((GroupAdapter)model).getGroup();
@ -95,21 +102,22 @@ public class GroupAdapter implements GroupModel {
} }
@Override @Override
public void setParent(GroupModel group) { public void setParent(GroupModel parent) {
GroupEntity parent = toEntity(group, em); if (parent == null) group.setParent(null);
group.setParent(group); else {
GroupEntity parentEntity = toEntity(parent, em);
group.setParent(parentEntity);
}
} }
@Override @Override
public void addChild(GroupModel subGroup) { public void addChild(GroupModel subGroup) {
subGroup.setParent(this); subGroup.setParent(this);
} }
@Override @Override
public void removeChild(GroupModel subGroup) { public void removeChild(GroupModel subGroup) {
subGroup.setParent(null); subGroup.setParent(null);
} }
@Override @Override

View file

@ -131,7 +131,7 @@ public class JpaRealmProvider implements RealmProvider {
public GroupModel getGroupById(String id, RealmModel realm) { public GroupModel getGroupById(String id, RealmModel realm) {
GroupEntity groupEntity = em.find(GroupEntity.class, id); GroupEntity groupEntity = em.find(GroupEntity.class, id);
if (groupEntity == null) return null; if (groupEntity == null) return null;
if (groupEntity.getRealm().getId().equals(realm.getId())) return null; if (!groupEntity.getRealm().getId().equals(realm.getId())) return null;
return new GroupAdapter(realm, em, groupEntity); return new GroupAdapter(realm, em, groupEntity);
} }

View file

@ -1947,12 +1947,19 @@ public class RealmAdapter implements RealmModel {
return null; return null;
} }
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
if (group.getParentId() != null) {
group.getParent().removeChild(group);
}
group.setParent(toParent);
if (toParent != null) toParent.addChild(group);
else addTopLevelGroup(group);
}
@Override @Override
public GroupModel getGroupById(String id) { public GroupModel getGroupById(String id) {
GroupEntity groupEntity = em.find(GroupEntity.class, id); return session.realms().getGroupById(id, this);
if (groupEntity == null) return null;
if (groupEntity.getRealm().getId().equals(getId())) return null;
return new GroupAdapter(this, em, groupEntity);
} }
@Override @Override
@ -1994,12 +2001,29 @@ public class RealmAdapter implements RealmModel {
session.users().preRemove(this, group); session.users().preRemove(this, group);
moveGroup(group, null);
realm.getGroups().remove(groupEntity); realm.getGroups().remove(groupEntity);
em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", group).executeUpdate(); em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", groupEntity).executeUpdate();
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", group).executeUpdate(); em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
em.remove(groupEntity); em.remove(groupEntity);
return true; return true;
} }
@Override
public GroupModel createGroup(String name) {
GroupEntity groupEntity = new GroupEntity();
groupEntity.setId(KeycloakModelUtils.generateId());
groupEntity.setName(name);
groupEntity.setRealm(realm);
em.persist(groupEntity);
return new GroupAdapter(this, em, groupEntity);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
subGroup.setParent(null);
}
} }

View file

@ -191,6 +191,11 @@ public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> impleme
return realm.getGroupById(group.getParentId()); return realm.getGroupById(group.getParentId());
} }
@Override
public String getParentId() {
return group.getParentId();
}
@Override @Override
public Set<GroupModel> getSubGroups() { public Set<GroupModel> getSubGroups() {
Set<GroupModel> subGroups = new HashSet<>(); Set<GroupModel> subGroups = new HashSet<>();

View file

@ -609,6 +609,34 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
return model.getRoleById(id, this); return model.getRoleById(id, this);
} }
@Override
public GroupModel createGroup(String name) {
MongoGroupEntity group = new MongoGroupEntity();
group.setId(KeycloakModelUtils.generateId());
group.setName(name);
group.setRealmId(getId());
getMongoStore().insertEntity(group, invocationContext);
return new GroupAdapter(session, this, group, invocationContext);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
subGroup.setParent(null);
}
@Override
public void moveGroup(GroupModel group, GroupModel toParent) {
if (group.getParentId() != null) {
group.getParent().removeChild(group);
}
group.setParent(toParent);
if (toParent != null) toParent.addChild(group);
else addTopLevelGroup(group);
}
@Override @Override
public GroupModel getGroupById(String id) { public GroupModel getGroupById(String id) {
return model.getGroupById(id, this); return model.getGroupById(id, this);
@ -646,7 +674,11 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override @Override
public boolean removeGroup(GroupModel group) { public boolean removeGroup(GroupModel group) {
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
session.users().preRemove(this, group); session.users().preRemove(this, group);
moveGroup(group, null);
return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext); return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext);
} }

View file

@ -6,6 +6,7 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleMapperModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
@ -29,17 +30,17 @@ import java.util.Set;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class UserClientRoleMappingsResource { public class ClientRoleMappingsResource {
protected static final Logger logger = Logger.getLogger(UserClientRoleMappingsResource.class); protected static final Logger logger = Logger.getLogger(ClientRoleMappingsResource.class);
protected RealmModel realm; protected RealmModel realm;
protected RealmAuth auth; protected RealmAuth auth;
protected UserModel user; protected RoleMapperModel user;
protected ClientModel client; protected ClientModel client;
protected AdminEventBuilder adminEvent; protected AdminEventBuilder adminEvent;
private UriInfo uriInfo; private UriInfo uriInfo;
public UserClientRoleMappingsResource(UriInfo uriInfo, RealmModel realm, RealmAuth auth, UserModel user, ClientModel client, AdminEventBuilder adminEvent) { public ClientRoleMappingsResource(UriInfo uriInfo, RealmModel realm, RealmAuth auth, RoleMapperModel user, ClientModel client, AdminEventBuilder adminEvent) {
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
this.realm = realm; this.realm = realm;
this.auth = auth; this.auth = auth;
@ -105,10 +106,10 @@ public class UserClientRoleMappingsResource {
return getAvailableRoles(user, available); return getAvailableRoles(user, available);
} }
public static List<RoleRepresentation> getAvailableRoles(UserModel user, Set<RoleModel> available) { public static List<RoleRepresentation> getAvailableRoles(RoleMapperModel mapper, Set<RoleModel> available) {
Set<RoleModel> roles = new HashSet<RoleModel>(); Set<RoleModel> roles = new HashSet<RoleModel>();
for (RoleModel roleModel : available) { for (RoleModel roleModel : available) {
if (user.hasRole(roleModel)) continue; if (mapper.hasRole(roleModel)) continue;
roles.add(roleModel); roles.add(roleModel);
} }

View file

@ -0,0 +1,261 @@
package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Bill Burke
*/
public class GroupResource {
private static Logger logger = Logger.getLogger(GroupResource.class);
private final RealmModel realm;
private final KeycloakSession session;
private final RealmAuth auth;
private final AdminEventBuilder adminEvent;
public GroupResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
}
@Context private UriInfo uriInfo;
public GroupResource(RealmAuth auth, RealmModel realm, KeycloakSession session, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
}
/**
* Get group hierarchy. Only name and ids are returned.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<GroupRepresentation> getGroups() {
this.auth.requireView();
return ModelToRepresentation.toGroupHierarchy(realm, false);
}
/**
* Set or create child as a top level group. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Path("{id}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response addRealmGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
GroupModel parentModel = realm.getGroupById(parentId);
Response.ResponseBuilder builder = Response.status(204);
if (parentModel == null) {
throw new NotFoundException("Could not find parent by id");
}
GroupModel child = null;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
updateGroup(rep, child);
URI uri = uriInfo.getBaseUriBuilder()
.path(uriInfo.getMatchedURIs().get(1))
.path(child.getId()).build();
builder.status(201).location(uri);
}
child.setParent(parentModel);
GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
}
/**
* Does not expand hierarchy. Subgroups will not be set.
*
* @param id
* @return
*/
@GET
@Path("{id}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation getGroupById(@PathParam("id") String id) {
this.auth.requireView();
GroupModel group = realm.getGroupById(id);
if (group == null) {
throw new NotFoundException("Could not find group by id");
}
return ModelToRepresentation.toRepresentation(group, true);
}
/**
* Update group
*
* @param rep
*/
@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateGroup(@PathParam("id") String id, GroupRepresentation rep) {
GroupModel model = realm.getGroupById(id);
if (model == null) {
throw new NotFoundException("Could not find group by id");
}
updateGroup(rep, model);
}
@DELETE
@Path("{id}")
public void deleteGroup(@PathParam("id") String id) {
GroupModel model = realm.getGroupById(id);
if (model == null) {
throw new NotFoundException("Could not find group by id");
}
realm.removeGroup(model);
}
/**
* Set or create child. This will just set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Path("{id}/children")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response addGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
GroupModel parentModel = realm.getGroupById(parentId);
Response.ResponseBuilder builder = Response.status(204);
if (parentModel == null) {
throw new NotFoundException("Could not find parent by id");
}
GroupModel child = null;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
updateGroup(rep, child);
URI uri = uriInfo.getBaseUriBuilder()
.path(uriInfo.getMatchedURIs().get(1))
.path(child.getId()).build();
builder.status(201).location(uri);
}
realm.moveGroup(child, parentModel);
GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
}
/**
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addTopLevelGroup(GroupRepresentation rep) {
GroupModel child = null;
Response.ResponseBuilder builder = Response.status(204);
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
updateGroup(rep, child);
URI uri = uriInfo.getAbsolutePathBuilder()
.path(child.getId()).build();
builder.status(201).location(uri);
}
realm.moveGroup(child, null);
return builder.build();
}
public void updateGroup(GroupRepresentation rep, GroupModel model) {
if (rep.getName() != null) model.setName(rep.getName());
if (rep.getAttributes() != null) {
Set<String> attrsToRemove = new HashSet<>(model.getAttributes().keySet());
attrsToRemove.removeAll(rep.getAttributes().keySet());
for (Map.Entry<String, List<String>> attr : rep.getAttributes().entrySet()) {
model.setAttribute(attr.getKey(), attr.getValue());
}
for (String attr : attrsToRemove) {
model.removeAttribute(attr);
}
}
}
@Path("{id}/role-mappings")
public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
GroupModel group = session.realms().getGroupById(id, realm);
if (group == null) {
throw new NotFoundException("Group not found");
}
auth.init(RealmAuth.Resource.USER);
RoleMapperResource resource = new RoleMapperResource(realm, auth, group, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
}

View file

@ -619,4 +619,11 @@ public class RealmAdminResource {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent); return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
} }
@Path("groups")
public GroupResource getGroups() {
GroupResource resource = new GroupResource(realm, session, this.auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
} }

View file

@ -0,0 +1,271 @@
package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.common.ClientConnection;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelReadOnlyException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.representations.idm.ClientMappingsRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Base resource for managing users
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleMapperResource {
protected static final Logger logger = Logger.getLogger(RoleMapperResource.class);
protected RealmModel realm;
private RealmAuth auth;
private RoleMapperModel roleMapper;
private AdminEventBuilder adminEvent;
@Context
protected ClientConnection clientConnection;
@Context
protected UriInfo uriInfo;
@Context
protected KeycloakSession session;
@Context
protected HttpHeaders headers;
@Context
protected BruteForceProtector protector;
public RoleMapperResource(RealmModel realm, RealmAuth auth, RoleMapperModel roleMapper, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent;
this.roleMapper = roleMapper;
}
/**
* Get role mappings
*
* @return
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public MappingsRepresentation getRoleMappings() {
auth.requireView();
MappingsRepresentation all = new MappingsRepresentation();
Set<RoleModel> realmMappings = roleMapper.getRoleMappings();
RealmManager manager = new RealmManager(session);
if (realmMappings.size() > 0) {
List<RoleRepresentation> realmRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
realmRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
all.setRealmMappings(realmRep);
}
List<ClientModel> clients = realm.getClients();
if (clients.size() > 0) {
Map<String, ClientMappingsRepresentation> appMappings = new HashMap<String, ClientMappingsRepresentation>();
for (ClientModel client : clients) {
Set<RoleModel> roleMappings = roleMapper.getClientRoleMappings(client);
if (roleMappings.size() > 0) {
ClientMappingsRepresentation mappings = new ClientMappingsRepresentation();
mappings.setId(client.getId());
mappings.setClient(client.getClientId());
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
mappings.setMappings(roles);
for (RoleModel role : roleMappings) {
roles.add(ModelToRepresentation.toRepresentation(role));
}
appMappings.put(client.getClientId(), mappings);
all.setClientMappings(appMappings);
}
}
}
return all;
}
/**
* Get realm-level role mappings
*
* @return
*/
@Path("realm")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getRealmRoleMappings() {
auth.requireView();
Set<RoleModel> realmMappings = roleMapper.getRealmRoleMappings();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
return realmMappingsRep;
}
/**
* Get effective realm-level role mappings
*
* This will recurse all composite roles to get the result.
*
* @return
*/
@Path("realm/composite")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getCompositeRealmRoleMappings() {
auth.requireView();
Set<RoleModel> roles = realm.getRoles();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
if (roleMapper.hasRole(roleModel)) {
realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
}
return realmMappingsRep;
}
/**
* Get realm-level roles that can be mapped
*
* @return
*/
@Path("realm/available")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getAvailableRealmRoleMappings() {
auth.requireView();
Set<RoleModel> available = realm.getRoles();
return ClientRoleMappingsResource.getAvailableRoles(roleMapper, available);
}
/**
* Add realm-level role mappings to the user
*
* @param roles Roles to add
*/
@Path("realm")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void addRealmRoleMappings(List<RoleRepresentation> roles) {
auth.requireManage();
logger.debugv("** addRealmRoleMappings: {0}", roles);
for (RoleRepresentation role : roles) {
RoleModel roleModel = realm.getRole(role.getName());
if (roleModel == null || !roleModel.getId().equals(role.getId())) {
throw new NotFoundException("Role not found");
}
roleMapper.grantRole(roleModel);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, role.getId()).representation(roles).success();
}
}
/**
* Delete realm-level role mappings
*
* @param roles
*/
@Path("realm")
@DELETE
@Consumes(MediaType.APPLICATION_JSON)
public void deleteRealmRoleMappings(List<RoleRepresentation> roles) {
auth.requireManage();
logger.debug("deleteRealmRoleMappings");
if (roles == null) {
Set<RoleModel> roleModels = roleMapper.getRealmRoleMappings();
for (RoleModel roleModel : roleModels) {
roleMapper.deleteRoleMapping(roleModel);
}
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(roles).success();
} else {
for (RoleRepresentation role : roles) {
RoleModel roleModel = realm.getRole(role.getName());
if (roleModel == null || !roleModel.getId().equals(role.getId())) {
throw new NotFoundException("Role not found");
}
roleMapper.deleteRoleMapping(roleModel);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo, role.getId()).representation(roles).success();
}
}
}
@Path("clients/{client}")
public ClientRoleMappingsResource getUserClientRoleMappingsResource(@PathParam("client") String client) {
ClientModel clientModel = realm.getClientById(client);
if (clientModel == null) {
throw new NotFoundException("Client not found");
}
return new ClientRoleMappingsResource(uriInfo, realm, auth, roleMapper, clientModel, adminEvent);
}
}

View file

@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
@ -660,214 +661,18 @@ public class UsersResource {
return results; return results;
} }
/**
* Get role mappings for the user
*
* @param id User id
* @return
*/
@Path("{id}/role-mappings") @Path("{id}/role-mappings")
@GET public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public MappingsRepresentation getRoleMappings(@PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm); UserModel user = session.users().getUserById(id, realm);
if (user == null) { if (user == null) {
throw new NotFoundException("User not found"); throw new NotFoundException("User not found");
} }
auth.init(RealmAuth.Resource.USER);
MappingsRepresentation all = new MappingsRepresentation(); RoleMapperResource resource = new RoleMapperResource(realm, auth, user, adminEvent);
Set<RoleModel> realmMappings = user.getRoleMappings(); ResteasyProviderFactory.getInstance().injectProperties(resource);
RealmManager manager = new RealmManager(session); return resource;
if (realmMappings.size() > 0) {
List<RoleRepresentation> realmRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
realmRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
all.setRealmMappings(realmRep);
}
List<ClientModel> clients = realm.getClients();
if (clients.size() > 0) {
Map<String, ClientMappingsRepresentation> appMappings = new HashMap<String, ClientMappingsRepresentation>();
for (ClientModel client : clients) {
Set<RoleModel> roleMappings = user.getClientRoleMappings(client);
if (roleMappings.size() > 0) {
ClientMappingsRepresentation mappings = new ClientMappingsRepresentation();
mappings.setId(client.getId());
mappings.setClient(client.getClientId());
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
mappings.setMappings(roles);
for (RoleModel role : roleMappings) {
roles.add(ModelToRepresentation.toRepresentation(role));
}
appMappings.put(client.getClientId(), mappings);
all.setClientMappings(appMappings);
}
}
}
return all;
}
/**
* Get realm-level role mappings for the user
*
* @param id User id
* @return
*/
@Path("{id}/role-mappings/realm")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getRealmRoleMappings(@PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
Set<RoleModel> realmMappings = user.getRealmRoleMappings();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : realmMappings) {
realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
return realmMappingsRep;
}
/**
* Get effective realm-level role mappings for the user
*
* This will recurse all composite roles to get the result.
*
* @param id User id
* @return
*/
@Path("{id}/role-mappings/realm/composite")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getCompositeRealmRoleMappings(@PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
Set<RoleModel> roles = realm.getRoles();
List<RoleRepresentation> realmMappingsRep = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roles) {
if (user.hasRole(roleModel)) {
realmMappingsRep.add(ModelToRepresentation.toRepresentation(roleModel));
}
}
return realmMappingsRep;
}
/**
* Get realm-level roles that can be mapped to this user
*
* @param id User id
* @return
*/
@Path("{id}/role-mappings/realm/available")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public List<RoleRepresentation> getAvailableRealmRoleMappings(@PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
Set<RoleModel> available = realm.getRoles();
return UserClientRoleMappingsResource.getAvailableRoles(user, available);
}
/**
* Add realm-level role mappings to the user
*
* @param id User id
* @param roles Roles to add
*/
@Path("{id}/role-mappings/realm")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public void addRealmRoleMappings(@PathParam("id") String id, List<RoleRepresentation> roles) {
auth.requireManage();
logger.debugv("** addRealmRoleMappings: {0}", roles);
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
for (RoleRepresentation role : roles) {
RoleModel roleModel = realm.getRole(role.getName());
if (roleModel == null || !roleModel.getId().equals(role.getId())) {
throw new NotFoundException("Role not found");
}
user.grantRole(roleModel);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, role.getId()).representation(roles).success();
}
}
/**
* Delete realm-level role mappings
*
* @param id User id
* @param roles
*/
@Path("{id}/role-mappings/realm")
@DELETE
@Consumes(MediaType.APPLICATION_JSON)
public void deleteRealmRoleMappings(@PathParam("id") String id, List<RoleRepresentation> roles) {
auth.requireManage();
logger.debug("deleteRealmRoleMappings");
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
if (roles == null) {
Set<RoleModel> roleModels = user.getRealmRoleMappings();
for (RoleModel roleModel : roleModels) {
user.deleteRoleMapping(roleModel);
}
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(roles).success();
} else {
for (RoleRepresentation role : roles) {
RoleModel roleModel = realm.getRole(role.getName());
if (roleModel == null || !roleModel.getId().equals(role.getId())) {
throw new NotFoundException("Role not found");
}
user.deleteRoleMapping(roleModel);
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo, role.getId()).representation(roles).success();
}
}
}
@Path("{id}/role-mappings/clients/{client}")
public UserClientRoleMappingsResource getUserClientRoleMappingsResource(@PathParam("id") String id, @PathParam("client") String client) {
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
ClientModel clientModel = realm.getClientById(client);
if (clientModel == null) {
throw new NotFoundException("Client not found");
}
return new UserClientRoleMappingsResource(uriInfo, realm, auth, user, clientModel, adminEvent);
} }

View file

@ -161,6 +161,9 @@ public class KeycloakServer {
if (!System.getProperties().containsKey("keycloak.theme.dir")) { if (!System.getProperties().containsKey("keycloak.theme.dir")) {
System.setProperty("keycloak.theme.dir", file(dir.getAbsolutePath(), "forms", "common-themes", "src", "main", "resources", "theme").getAbsolutePath()); System.setProperty("keycloak.theme.dir", file(dir.getAbsolutePath(), "forms", "common-themes", "src", "main", "resources", "theme").getAbsolutePath());
} else {
String foo = System.getProperty("keycloak.theme.dir");
System.out.println(foo);
} }
if (!System.getProperties().containsKey("keycloak.theme.cacheTemplates")) { if (!System.getProperties().containsKey("keycloak.theme.cacheTemplates")) {