Merge pull request #1788 from patriot1burke/master

groups data model
This commit is contained in:
Bill Burke 2015-11-11 19:38:01 -05:00
commit 6cc76b2e3d
88 changed files with 4314 additions and 268 deletions

View file

@ -1,6 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd"> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="mposolda@redhat.com" id="1.7.0"> <changeSet author="bburke@redhat.com" id="1.7.0">
<createTable tableName="KEYCLOAK_GROUP">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)"/>
<column name="PARENT_GROUP" type="VARCHAR(36)"/>
<column name="REALM_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="GROUP_ROLE_MAPPING">
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="GROUP_ATTRIBUTE">
<column name="ID" type="VARCHAR(36)" defaultValue="sybase-needs-something-here">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_GROUP_MEMBERSHIP">
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addColumn tableName="IDENTITY_PROVIDER"> <addColumn tableName="IDENTITY_PROVIDER">
<column name="FIRST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)"> <column name="FIRST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)">
@ -9,5 +45,19 @@
</addColumn> </addColumn>
<dropColumn tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LGN_MD"/> <dropColumn tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LGN_MD"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GROUP" tableName="KEYCLOAK_GROUP"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="KEYCLOAK_GROUP" constraintName="FK_GROUP_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GROUP_ATTRIBUTE_PK" tableName="GROUP_ATTRIBUTE"/>
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="GROUP_ATTRIBUTE" constraintName="FK_GROUP_ATTRIBUTE_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addPrimaryKey columnNames="GROUP_ID, USER_ID" constraintName="CONSTRAINT_USER_GROUP" tableName="USER_GROUP_MEMBERSHIP"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_GROUP_MEMBERSHIP" constraintName="FK_USER_GROUP_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addPrimaryKey columnNames="ROLE_ID, GROUP_ID" constraintName="CONSTRAINT_GROUP_ROLE" tableName="GROUP_ROLE_MAPPING"/>
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
</changeSet> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -31,6 +31,10 @@
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class> <class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
<class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class> <class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
<class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class> <class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
<class>org.keycloak.models.jpa.entities.GroupEntity</class>
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
<!-- JpaAuditProviders --> <!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class> <class>org.keycloak.events.jpa.EventEntity</class>

View file

@ -0,0 +1,76 @@
package org.keycloak.representations.idm;
import org.codehaus.jackson.annotate.JsonIgnore;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupRepresentation {
protected String id;
protected String name;
protected Map<String, List<String>> attributes;
protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles;
protected List<GroupRepresentation> subGroups;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getRealmRoles() {
return realmRoles;
}
public void setRealmRoles(List<String> realmRoles) {
this.realmRoles = realmRoles;
}
public Map<String, List<String>> getClientRoles() {
return clientRoles;
}
public void setClientRoles(Map<String, List<String>> clientRoles) {
this.clientRoles = clientRoles;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
public GroupRepresentation singleAttribute(String name, String value) {
if (this.attributes == null) attributes = new HashMap<>();
attributes.put(name, Arrays.asList(value));
return this;
}
public List<GroupRepresentation> getSubGroups() {
return subGroups;
}
public void setSubGroups(List<GroupRepresentation> subGroups) {
this.subGroups = subGroups;
}
}

View file

@ -47,6 +47,7 @@ public class RealmRepresentation {
protected String certificate; protected String certificate;
protected String codeSecret; protected String codeSecret;
protected RolesRepresentation roles; protected RolesRepresentation roles;
protected List<GroupRepresentation> groups;
protected List<String> defaultRoles; protected List<String> defaultRoles;
@Deprecated @Deprecated
protected Set<String> requiredCredentials; protected Set<String> requiredCredentials;
@ -775,4 +776,12 @@ public class RealmRepresentation {
public void setClientAuthenticationFlow(String clientAuthenticationFlow) { public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
this.clientAuthenticationFlow = clientAuthenticationFlow; this.clientAuthenticationFlow = clientAuthenticationFlow;
} }
public List<GroupRepresentation> getGroups() {
return groups;
}
public void setGroups(List<GroupRepresentation> groups) {
this.groups = groups;
}
} }

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

@ -1,6 +1,7 @@
package org.keycloak.examples.federation.properties; package org.keycloak.examples.federation.properties;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
@ -106,6 +107,12 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
// complete we dont'care if a role is removed
}
/** /**
* See if the user is still in the properties file * See if the user is still in the properties file
* *

View file

@ -63,4 +63,6 @@ public class ClasspathPropertiesFederationProvider extends BasePropertiesFederat
throw new IllegalStateException("Remove not supported"); throw new IllegalStateException("Remove not supported");
} }
} }

View file

@ -12,6 +12,7 @@ import org.jboss.logging.Logger;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
@ -105,6 +106,11 @@ public class KerberosFederationProvider implements UserFederationProvider {
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
@Override @Override
public boolean isValid(RealmModel realm, UserModel local) { public boolean isValid(RealmModel realm, UserModel local) {
// KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now

View file

@ -12,6 +12,7 @@ import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -319,6 +320,11 @@ public class LDAPFederationProvider implements UserFederationProvider {
// TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper? // TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper?
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
public boolean validPassword(RealmModel realm, UserModel user, String password) { public boolean validPassword(RealmModel realm, UserModel user, String password) {
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) { if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) {
// Use Kerberos JAAS (Krb5LoginModule) // Use Kerberos JAAS (Krb5LoginModule)

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;
@ -583,6 +583,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',
@ -1871,6 +1938,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

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

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

@ -57,7 +57,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
} }
public boolean isActive() { public boolean isActive() {
return this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore(); return token != null && this.token.isActive() && this.token.getIssuedAt() > deployment.getNotBefore();
} }
public KeycloakDeployment getDeployment() { public KeycloakDeployment getDeployment() {
@ -111,6 +111,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
log.debug("Token Verification succeeded!"); log.debug("Token Verification succeeded!");
} catch (VerificationException e) { } catch (VerificationException e) {
log.error("failed verification of token"); log.error("failed verification of token");
return false;
} }
if (response.getNotBeforePolicy() > deployment.getNotBefore()) { if (response.getNotBeforePolicy() > deployment.getNotBefore()) {
deployment.setNotBefore(response.getNotBeforePolicy()); deployment.setNotBefore(response.getNotBeforePolicy());

View file

@ -11,7 +11,7 @@ public interface MigrationModel {
/** /**
* Must have the form of major.minor.micro as the version is parsed and numbers are compared * Must have the form of major.minor.micro as the version is parsed and numbers are compared
*/ */
public static final String LATEST_VERSION = "1.6.0"; public static final String LATEST_VERSION = "1.7.0";
String getStoredVersion(); String getStoredVersion();
void setStoredVersion(String version); void setStoredVersion(String version);

View file

@ -0,0 +1,68 @@
package org.keycloak.models;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface GroupModel extends RoleMapperModel {
String getId();
String getName();
void setName(String name);
/**
* Set single value of specified attribute. Remove all other existing values
*
* @param name
* @param value
*/
void setSingleAttribute(String name, String value);
void setAttribute(String name, List<String> values);
void removeAttribute(String name);
/**
* @param name
* @return null if there is not any value of specified attribute or first value otherwise. Don't throw exception if there are more values of the attribute
*/
String getFirstAttribute(String name);
/**
* @param name
* @return list of all attribute values or empty list if there are not any values. Never return null
*/
List<String> getAttribute(String name);
Map<String, List<String>> getAttributes();
GroupModel getParent();
String getParentId();
Set<GroupModel> getSubGroups();
/**
* You must also call addChild on the parent group, addChild on RealmModel if there is no parent group
*
* @param group
*/
void setParent(GroupModel group);
/**
* Automatically calls setParent() on the subGroup
*
* @param subGroup
*/
void addChild(GroupModel subGroup);
/**
* Automatically calls setParent() on the subGroup
*
* @param subGroup
*/
void removeChild(GroupModel subGroup);
}

View file

@ -328,4 +328,19 @@ public interface RealmModel extends RoleContainerModel {
void setSupportedLocales(Set<String> locales); void setSupportedLocales(Set<String> locales);
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);
List<GroupModel> getGroups();
List<GroupModel> getTopLevelGroups();
boolean removeGroup(GroupModel group);
void moveGroup(GroupModel group, GroupModel toParent);
} }

View file

@ -20,8 +20,11 @@ public interface RealmProvider extends Provider {
RoleModel getRoleById(String id, RealmModel realm); RoleModel getRoleById(String id, RealmModel realm);
ClientModel getClientById(String id, RealmModel realm); ClientModel getClientById(String id, RealmModel realm);
GroupModel getGroupById(String id, RealmModel realm);
List<RealmModel> getRealms(); List<RealmModel> getRealms();
boolean removeRealm(String id); boolean removeRealm(String id);
void close(); void close();
} }

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

@ -165,6 +165,16 @@ public class UserFederationManager implements UserProvider {
return user; return user;
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return session.userStorage().getGroupMembers(realm, group, firstResult, maxResults);
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return getGroupMembers(realm, group, -1, -1);
}
@Override @Override
public UserModel getUserByUsername(String username, RealmModel realm) { public UserModel getUserByUsername(String username, RealmModel realm) {
UserModel user = session.userStorage().getUserByUsername(username.toLowerCase(), realm); UserModel user = session.userStorage().getUserByUsername(username.toLowerCase(), realm);
@ -353,6 +363,16 @@ public class UserFederationManager implements UserProvider {
session.userStorage().preRemove(realm, model); session.userStorage().preRemove(realm, model);
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
for (UserFederationProviderModel federation : realm.getUserFederationProviders()) {
UserFederationProvider fed = getFederationProvider(federation);
fed.preRemove(realm, group);
}
session.userStorage().preRemove(realm, group);
}
@Override @Override
public void preRemove(RealmModel realm, RoleModel role) { public void preRemove(RealmModel realm, RoleModel role) {
for (UserFederationProviderModel federation : realm.getUserFederationProviders()) { for (UserFederationProviderModel federation : realm.getUserFederationProviders()) {

View file

@ -119,6 +119,14 @@ public interface UserFederationProvider extends Provider {
*/ */
void preRemove(RealmModel realm, RoleModel role); void preRemove(RealmModel realm, RoleModel role);
/**
* called before a role is removed.
*
* @param realm
* @param group
*/
void preRemove(RealmModel realm, GroupModel group);
/** /**
* Is the Keycloak UserModel still valid and/or existing in federated storage? Keycloak may call this method * Is the Keycloak UserModel still valid and/or existing in federated storage? Keycloak may call this method
* in various user operations. The local storage may be deleted if this method returns false. * in various user operations. The local storage may be deleted if this method returns false.

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,12 +94,10 @@ public interface UserModel {
void updateCredentialDirectly(UserCredentialValueModel cred); void updateCredentialDirectly(UserCredentialValueModel cred);
Set<RoleModel> getRealmRoleMappings(); Set<GroupModel> getGroups();
Set<RoleModel> getClientRoleMappings(ClientModel app); void joinGroup(GroupModel group);
boolean hasRole(RoleModel role); void leaveGroup(GroupModel group);
void grantRole(RoleModel role); boolean isMemberOf(GroupModel group);
Set<RoleModel> getRoleMappings();
void deleteRoleMapping(RoleModel role);
String getFederationLink(); String getFederationLink();
void setFederationLink(String link); void setFederationLink(String link);

View file

@ -25,12 +25,16 @@ public interface UserProvider extends Provider {
UserModel getUserById(String id, RealmModel realm); UserModel getUserById(String id, RealmModel realm);
UserModel getUserByUsername(String username, RealmModel realm); UserModel getUserByUsername(String username, RealmModel realm);
UserModel getUserByEmail(String email, RealmModel realm); UserModel getUserByEmail(String email, RealmModel realm);
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm); UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm);
UserModel getUserByServiceAccountClient(ClientModel client); UserModel getUserByServiceAccountClient(ClientModel client);
List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts); List<UserModel> getUsers(RealmModel realm, boolean includeServiceAccounts);
// Service account is included for counts // Service account is included for counts
int getUsersCount(RealmModel realm); int getUsersCount(RealmModel realm);
List<UserModel> getGroupMembers(RealmModel realm, GroupModel group);
List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts); List<UserModel> getUsers(RealmModel realm, int firstResult, int maxResults, boolean includeServiceAccounts);
List<UserModel> searchForUser(String search, RealmModel realm); List<UserModel> searchForUser(String search, RealmModel realm);
List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults); List<UserModel> searchForUser(String search, RealmModel realm, int firstResult, int maxResults);
@ -50,6 +54,7 @@ public interface UserProvider extends Provider {
void preRemove(RealmModel realm, UserFederationProviderModel link); void preRemove(RealmModel realm, UserFederationProviderModel link);
void preRemove(RealmModel realm, RoleModel role); void preRemove(RealmModel realm, RoleModel role);
void preRemove(RealmModel realm, GroupModel group);
void preRemove(RealmModel realm, ClientModel client); void preRemove(RealmModel realm, ClientModel client);
void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper); void preRemove(ClientModel realm, ProtocolMapperModel protocolMapper);

View file

@ -0,0 +1,60 @@
package org.keycloak.models.entities;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bburke@redhat.com">Bill Burke/a>
*/
public class GroupEntity extends AbstractIdentifiableEntity {
private String name;
private String realmId;
private List<String> roleIds;
private String parentId;
private Map<String, List<String>> attributes;
public String getRealmId() {
return realmId;
}
public void setRealmId(String realmId) {
this.realmId = realmId;
}
public List<String> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<String> roleIds) {
this.roleIds = roleIds;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getParentId() {
return parentId;
}
public void setParentId(String parentId) {
this.parentId = parentId;
}
}

View file

@ -21,6 +21,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private String realmId; private String realmId;
private List<String> roleIds; private List<String> roleIds;
private List<String> groupIds;
private Map<String, List<String>> attributes; private Map<String, List<String>> attributes;
private List<String> requiredActions; private List<String> requiredActions;
@ -157,5 +158,13 @@ public class UserEntity extends AbstractIdentifiableEntity {
public void setServiceAccountClientLink(String serviceAccountClientLink) { public void setServiceAccountClientLink(String serviceAccountClientLink) {
this.serviceAccountClientLink = serviceAccountClientLink; this.serviceAccountClientLink = serviceAccountClientLink;
} }
public List<String> getGroupIds() {
return groupIds;
}
public void setGroupIds(List<String> groupIds) {
this.groupIds = groupIds;
}
} }

View file

@ -3,6 +3,7 @@ package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter; import org.bouncycastle.openssl.PEMWriter;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -279,6 +280,24 @@ public final class KeycloakModelUtils {
return false; return false;
} }
/**
*
* @param groups
* @param targetGroup
* @return true if targetGroup is in groups (directly or indirectly via parent child relationship)
*/
public static boolean isMember(Set<GroupModel> groups, GroupModel targetGroup) {
if (groups.contains(targetGroup)) return true;
for (GroupModel mapping : groups) {
GroupModel child = mapping;
while(child.getParent() != null) {
if (child.getParent().equals(targetGroup)) return true;
child = child.getParent();
}
}
return false;
}
// USER FEDERATION RELATED STUFF // USER FEDERATION RELATED STUFF
/** /**

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

@ -1,6 +1,7 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
@ -255,4 +256,26 @@ public class UserModelDelegate implements UserModel {
public void setCreatedTimestamp(Long timestamp){ public void setCreatedTimestamp(Long timestamp){
delegate.setCreatedTimestamp(timestamp); delegate.setCreatedTimestamp(timestamp);
} }
@Override
public Set<GroupModel> getGroups() {
return delegate.getGroups();
}
@Override
public void joinGroup(GroupModel group) {
delegate.joinGroup(group);
}
@Override
public void leaveGroup(GroupModel group) {
delegate.leaveGroup(group);
}
@Override
public boolean isMemberOf(GroupModel group) {
return delegate.isMemberOf(group);
}
} }

View file

@ -20,6 +20,7 @@ import org.keycloak.connections.file.FileConnectionProvider;
import org.keycloak.connections.file.InMemoryModel; import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -78,6 +79,11 @@ public class FileRealmProvider implements RealmProvider {
return realm; return realm;
} }
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
return null;
}
@Override @Override
public RealmModel getRealm(String id) { public RealmModel getRealm(String id) {
RealmModel model = inMemoryModel.getRealm(id); RealmModel model = inMemoryModel.getRealm(id);

View file

@ -21,6 +21,7 @@ import org.keycloak.connections.file.InMemoryModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException; import org.keycloak.models.ModelException;
@ -80,6 +81,21 @@ public class FileUserProvider implements UserProvider {
return inMemoryModel.getUser(realm.getId(), userId); return inMemoryModel.getUser(realm.getId(), userId);
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return null;
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return null;
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
@Override @Override
public UserModel getUserByUsername(String username, RealmModel realm) { public UserModel getUserByUsername(String username, RealmModel realm) {
for (UserModel user : inMemoryModel.getUsers(realm.getId())) { for (UserModel user : inMemoryModel.getUsers(realm.getId())) {

View file

@ -0,0 +1,213 @@
package org.keycloak.models.file.adapter;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.entities.GroupEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupAdapter implements GroupModel {
private final GroupEntity group;
private RealmModel realm;
private KeycloakSession session;
public GroupAdapter(KeycloakSession session, RealmModel realm, GroupEntity group) {
this.group = group;
this.realm = realm;
this.session = session;
}
@Override
public String getId() {
return group.getId();
}
@Override
public String getName() {
return group.getName();
}
@Override
public void setName(String name) {
group.setName(name);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof GroupModel)) return false;
GroupModel that = (GroupModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
@Override
public void setSingleAttribute(String name, String value) {
if (group.getAttributes() == null) {
group.setAttributes(new HashMap<String, List<String>>());
}
List<String> attrValues = new ArrayList<>();
attrValues.add(value);
group.getAttributes().put(name, attrValues);
}
@Override
public void setAttribute(String name, List<String> values) {
if (group.getAttributes() == null) {
group.setAttributes(new HashMap<String, List<String>>());
}
group.getAttributes().put(name, values);
}
@Override
public void removeAttribute(String name) {
if (group.getAttributes() == null) return;
group.getAttributes().remove(name);
}
@Override
public String getFirstAttribute(String name) {
if (group.getAttributes()==null) return null;
List<String> attrValues = group.getAttributes().get(name);
return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
}
@Override
public List<String> getAttribute(String name) {
if (group.getAttributes()==null) return Collections.<String>emptyList();
List<String> attrValues = group.getAttributes().get(name);
return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
}
@Override
public Map<String, List<String>> getAttributes() {
return group.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) group.getAttributes());
}
@Override
public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings();
return KeycloakModelUtils.hasRole(roles, role);
}
@Override
public void grantRole(RoleModel role) {
if (group.getRoleIds() == null) {
group.setRoleIds(new LinkedList<String>());
}
if (group.getRoleIds().contains(role.getId())) {
return;
}
group.getRoleIds().add(role.getId());
}
@Override
public Set<RoleModel> getRoleMappings() {
if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET;
Set<RoleModel> roles = new HashSet<>();
for (String id : group.getRoleIds()) {
roles.add(realm.getRoleById(id));
}
return roles;
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> allRoles = getRoleMappings();
// Filter to retrieve just realm roles
Set<RoleModel> realmRoles = new HashSet<RoleModel>();
for (RoleModel role : allRoles) {
if (role.getContainer() instanceof RealmModel) {
realmRoles.add(role);
}
}
return realmRoles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
if (group == null || role == null) return;
if (group.getRoleIds() == null) return;
group.getRoleIds().remove(role.getId());
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> result = new HashSet<RoleModel>();
Set<RoleModel> roles = getRoleMappings();
for (RoleModel role : roles) {
if (app.equals(role.getContainer())) {
result.add(role);
}
}
return result;
}
@Override
public GroupModel getParent() {
if (group.getParentId() == null) return null;
return realm.getGroupById(group.getParentId());
}
@Override
public String getParentId() {
return group.getParentId();
}
@Override
public Set<GroupModel> getSubGroups() {
Set<GroupModel> subGroups = new HashSet<>();
for (GroupModel groupModel : realm.getGroups()) {
if (groupModel.getParent().equals(this)) {
subGroups.add(groupModel);
}
}
return subGroups;
}
@Override
public void setParent(GroupModel group) {
this.group.setParentId(group.getId());
}
@Override
public void addChild(GroupModel subGroup) {
subGroup.setParent(this);
}
@Override
public void removeChild(GroupModel subGroup) {
subGroup.setParent(null);
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
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.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -88,6 +89,7 @@ public class RealmAdapter implements RealmModel {
private final Map<String, ClientModel> allApps = new HashMap<String, ClientModel>(); private final Map<String, ClientModel> allApps = new HashMap<String, ClientModel>();
private ClientModel masterAdminApp = null; private ClientModel masterAdminApp = null;
private final Map<String, RoleAdapter> allRoles = new HashMap<String, RoleAdapter>(); private final Map<String, RoleAdapter> allRoles = new HashMap<String, RoleAdapter>();
private final Map<String, GroupAdapter> allGroups = new HashMap<String, GroupAdapter>();
private final Map<String, IdentityProviderModel> allIdProviders = new HashMap<String, IdentityProviderModel>(); private final Map<String, IdentityProviderModel> allIdProviders = new HashMap<String, IdentityProviderModel>();
public RealmAdapter(KeycloakSession session, RealmEntity realm, InMemoryModel inMemoryModel) { public RealmAdapter(KeycloakSession session, RealmEntity realm, InMemoryModel inMemoryModel) {
@ -601,6 +603,45 @@ public class RealmAdapter implements RealmModel {
return null; return null;
} }
@Override
public GroupModel getGroupById(String id) {
GroupModel found = allGroups.get(id);
if (found != null) return found;
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
public List<GroupModel> getGroups() {
List<GroupModel> list = new LinkedList<>();
for (GroupAdapter group : allGroups.values()) {
list.add(group);
}
return list;
}
@Override
public List<GroupModel> getTopLevelGroups() {
List<GroupModel> list = new LinkedList<>();
for (GroupAdapter group : allGroups.values()) {
if (group.getParent() == null) list.add(group);
}
return list;
}
@Override
public boolean removeGroup(GroupModel group) {
return allGroups.remove(group.getId()) != null;
}
@Override @Override
public List<String> getDefaultRoles() { public List<String> getDefaultRoles() {
return realm.getDefaultRoles(); return realm.getDefaultRoles();
@ -1787,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

@ -21,6 +21,7 @@ import org.keycloak.models.ClientModel;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
@ -59,6 +60,7 @@ public class UserAdapter implements UserModel, Comparable {
private final RealmModel realm; private final RealmModel realm;
private final Set<RoleModel> allRoles = new HashSet<RoleModel>(); private final Set<RoleModel> allRoles = new HashSet<RoleModel>();
private final Set<GroupModel> allGroups = new HashSet<GroupModel>();
public UserAdapter(RealmModel realm, UserEntity userEntity, InMemoryModel inMemoryModel) { public UserAdapter(RealmModel realm, UserEntity userEntity, InMemoryModel inMemoryModel) {
this.user = userEntity; this.user = userEntity;
@ -467,6 +469,29 @@ public class UserAdapter implements UserModel, Comparable {
credentialEntity.setPeriod(credModel.getPeriod()); credentialEntity.setPeriod(credModel.getPeriod());
} }
@Override
public Set<GroupModel> getGroups() {
return Collections.unmodifiableSet(allGroups);
}
@Override
public void joinGroup(GroupModel group) {
allGroups.add(group);
}
@Override
public void leaveGroup(GroupModel group) {
if (user == null || group == null) return;
allGroups.remove(group);
}
@Override
public boolean isMemberOf(GroupModel group) {
return KeycloakModelUtils.isMember(getGroups(), group);
}
@Override @Override
public boolean hasRole(RoleModel role) { public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings(); Set<RoleModel> roles = getRoleMappings();

View file

@ -21,9 +21,11 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
protected Set<String> realmInvalidations = new HashSet<String>(); protected Set<String> realmInvalidations = new HashSet<String>();
protected Set<String> appInvalidations = new HashSet<String>(); protected Set<String> appInvalidations = new HashSet<String>();
protected Set<String> roleInvalidations = new HashSet<String>(); protected Set<String> roleInvalidations = new HashSet<String>();
protected Set<String> groupInvalidations = new HashSet<String>();
protected Map<String, RealmModel> managedRealms = new HashMap<String, RealmModel>(); protected Map<String, RealmModel> managedRealms = new HashMap<String, RealmModel>();
protected Map<String, ClientModel> managedApplications = new HashMap<String, ClientModel>(); protected Map<String, ClientModel> managedApplications = new HashMap<String, ClientModel>();
protected Map<String, RoleModel> managedRoles = new HashMap<String, RoleModel>(); protected Map<String, RoleModel> managedRoles = new HashMap<String, RoleModel>();
protected Map<String, GroupModel> managedGroups = new HashMap<String, GroupModel>();
protected boolean clearAll; protected boolean clearAll;
@ -73,6 +75,12 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
roleInvalidations.add(id); roleInvalidations.add(id);
} }
@Override
public void registerGroupInvalidation(String id) {
groupInvalidations.add(id);
}
protected void runInvalidations() { protected void runInvalidations() {
for (String id : realmInvalidations) { for (String id : realmInvalidations) {
cache.invalidateCachedRealmById(id); cache.invalidateCachedRealmById(id);
@ -80,6 +88,9 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
for (String id : roleInvalidations) { for (String id : roleInvalidations) {
cache.invalidateRoleById(id); cache.invalidateRoleById(id);
} }
for (String id : groupInvalidations) {
cache.invalidateGroupById(id);
}
for (String id : appInvalidations) { for (String id : appInvalidations) {
cache.invalidateCachedApplicationById(id); cache.invalidateCachedApplicationById(id);
} }
@ -254,6 +265,31 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
return adapter; return adapter;
} }
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
if (!cache.isEnabled()) return getDelegate().getGroupById(id, realm);
CachedGroup cached = cache.getGroup(id);
if (cached != null && !cached.getRealm().equals(realm.getId())) {
cached = null;
}
if (cached == null) {
GroupModel model = getDelegate().getGroupById(id, realm);
if (model == null) return null;
if (groupInvalidations.contains(id)) return model;
cached = new CachedGroup(realm, model);
cache.addCachedGroup(cached);
} else if (groupInvalidations.contains(id)) {
return getDelegate().getGroupById(id, realm);
} else if (managedGroups.containsKey(id)) {
return managedGroups.get(id);
}
GroupAdapter adapter = new GroupAdapter(cached, this, session, realm);
managedGroups.put(id, adapter);
return adapter;
}
@Override @Override
public ClientModel getClientById(String id, RealmModel realm) { public ClientModel getClientById(String id, RealmModel realm) {
if (!cache.isEnabled()) return getDelegate().getClientById(id, realm); if (!cache.isEnabled()) return getDelegate().getClientById(id, realm);

View file

@ -197,6 +197,16 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
return getDelegate().getUserByFederatedIdentity(socialLink, realm); return getDelegate().getUserByFederatedIdentity(socialLink, realm);
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
return getDelegate().getGroupMembers(realm, group, firstResult, maxResults);
}
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return getDelegate().getGroupMembers(realm, group);
}
@Override @Override
public UserModel getUserByServiceAccountClient(ClientModel client) { public UserModel getUserByServiceAccountClient(ClientModel client) {
return getDelegate().getUserByServiceAccountClient(client); return getDelegate().getUserByServiceAccountClient(client);
@ -319,6 +329,11 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
public void preRemove(RealmModel realm, RoleModel role) { public void preRemove(RealmModel realm, RoleModel role) {
getDelegate().preRemove(realm, role); getDelegate().preRemove(realm, role);
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
getDelegate().preRemove(realm, group);
}
@Override @Override
public void preRemove(RealmModel realm, UserFederationProviderModel link) { public void preRemove(RealmModel realm, UserFederationProviderModel link) {

View file

@ -0,0 +1,244 @@
package org.keycloak.models.cache.infinispan;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedUser;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupAdapter implements GroupModel {
protected GroupModel updated;
protected CachedGroup cached;
protected CacheRealmProvider cacheSession;
protected KeycloakSession keycloakSession;
protected RealmModel realm;
public GroupAdapter(CachedGroup cached, CacheRealmProvider cacheSession, KeycloakSession keycloakSession, RealmModel realm) {
this.cached = cached;
this.cacheSession = cacheSession;
this.keycloakSession = keycloakSession;
this.realm = realm;
}
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerGroupInvalidation(getId());
updated = cacheSession.getDelegate().getGroupById(getId(), realm);
if (updated == null) throw new IllegalStateException("Not found in database");
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof GroupModel)) return false;
GroupModel that = (GroupModel) o;
if (!cached.getId().equals(that.getId())) return false;
return true;
}
@Override
public int hashCode() {
return cached.getId().hashCode();
}
@Override
public String getId() {
if (updated != null) return updated.getId();
return cached.getId();
}
@Override
public String getName() {
if (updated != null) return updated.getName();
return cached.getName();
}
@Override
public void setName(String name) {
getDelegateForUpdate();
updated.setName(name);
}
@Override
public void setSingleAttribute(String name, String value) {
getDelegateForUpdate();
updated.setSingleAttribute(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
getDelegateForUpdate();
updated.setAttribute(name, values);
}
@Override
public void removeAttribute(String name) {
getDelegateForUpdate();
updated.removeAttribute(name);
}
@Override
public String getFirstAttribute(String name) {
if (updated != null) return updated.getFirstAttribute(name);
return cached.getAttributes().getFirst(name);
}
@Override
public List<String> getAttribute(String name) {
List<String> values = cached.getAttributes().get(name);
if (values == null) return null;
return values;
}
@Override
public Map<String, List<String>> getAttributes() {
return cached.getAttributes();
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
if (updated != null) return updated.getRealmRoleMappings();
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> realmMappings = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
if (((RealmModel) container).getId().equals(realm.getId())) {
realmMappings.add(role);
}
}
}
return realmMappings;
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
if (updated != null) return updated.getClientRoleMappings(app);
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> appMappings = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof ClientModel) {
if (((ClientModel) container).getId().equals(app.getId())) {
appMappings.add(role);
}
}
}
return appMappings;
}
@Override
public boolean hasRole(RoleModel role) {
if (updated != null) return updated.hasRole(role);
if (cached.getRoleMappings().contains(role.getId())) return true;
Set<RoleModel> mappings = getRoleMappings();
for (RoleModel mapping: mappings) {
if (mapping.hasRole(role)) return true;
}
return false;
}
@Override
public void grantRole(RoleModel role) {
getDelegateForUpdate();
updated.grantRole(role);
}
@Override
public Set<RoleModel> getRoleMappings() {
if (updated != null) return updated.getRoleMappings();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String id : cached.getRoleMappings()) {
RoleModel roleById = keycloakSession.realms().getRoleById(id, realm);
if (roleById == null) {
// chance that role was removed, so just delegate to persistence and get user invalidated
getDelegateForUpdate();
return updated.getRoleMappings();
}
roles.add(roleById);
}
return roles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
getDelegateForUpdate();
updated.deleteRoleMapping(role);
}
@Override
public GroupModel getParent() {
if (updated != null) return updated.getParent();
if (cached.getParentId() == null) return null;
return keycloakSession.realms().getGroupById(cached.getParentId(), realm);
}
@Override
public String getParentId() {
if (updated != null) return updated.getParentId();
return cached.getParentId();
}
@Override
public Set<GroupModel> getSubGroups() {
if (updated != null) return updated.getSubGroups();
Set<GroupModel> subGroups = new HashSet<>();
for (String id : cached.getSubGroups()) {
GroupModel subGroup = keycloakSession.realms().getGroupById(id, realm);
if (subGroup == null) {
// chance that role was removed, so just delegate to persistence and get user invalidated
getDelegateForUpdate();
return updated.getSubGroups();
}
subGroups.add(subGroup);
}
return subGroups;
}
@Override
public void setParent(GroupModel group) {
getDelegateForUpdate();
updated.setParent(group);
}
@Override
public void addChild(GroupModel subGroup) {
getDelegateForUpdate();
updated.addChild(subGroup);
}
@Override
public void removeChild(GroupModel subGroup) {
getDelegateForUpdate();
updated.removeChild(subGroup);
}
}

View file

@ -4,6 +4,7 @@ import org.infinispan.Cache;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.cache.RealmCache; import org.keycloak.models.cache.RealmCache;
import org.keycloak.models.cache.entities.CachedClient; import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole; import org.keycloak.models.cache.entities.CachedRole;
@ -101,12 +102,49 @@ public class InfinispanRealmCache implements RealmCache {
cache.remove(id); cache.remove(id);
} }
@Override
public CachedGroup getGroup(String id) {
if (!enabled) return null;
return get(id, CachedGroup.class);
}
@Override
public void invalidateGroup(CachedGroup role) {
logger.tracev("Removing group {0}", role.getId());
cache.remove(role.getId());
}
@Override
public void addCachedGroup(CachedGroup role) {
if (!enabled) return;
logger.tracev("Adding group {0}", role.getId());
cache.put(role.getId(), role);
}
@Override
public void invalidateCachedGroupById(String id) {
logger.tracev("Removing group {0}", id);
cache.remove(id);
}
@Override
public void invalidateGroupById(String id) {
logger.tracev("Removing group {0}", id);
cache.remove(id);
}
@Override @Override
public CachedRole getRole(String id) { public CachedRole getRole(String id) {
if (!enabled) return null; if (!enabled) return null;
return get(id, CachedRole.class); return get(id, CachedRole.class);
} }
@Override @Override
public void invalidateRole(CachedRole role) { public void invalidateRole(CachedRole role) {
logger.tracev("Removing role {0}", role.getId()); logger.tracev("Removing role {0}", role.getId());

View file

@ -1262,4 +1262,61 @@ public class RealmAdapter implements RealmModel {
if (updated != null) return updated.getRequiredActionProviderByAlias(alias); if (updated != null) return updated.getRequiredActionProviderByAlias(alias);
return cached.getRequiredActionProvidersByAlias().get(alias); return cached.getRequiredActionProvidersByAlias().get(alias);
} }
@Override
public GroupModel getGroupById(String id) {
if (updated != null) return updated.getGroupById(id);
return cacheSession.getGroupById(id, this);
}
@Override
public List<GroupModel> getGroups() {
if (updated != null) return updated.getGroups();
if (cached.getGroups().isEmpty()) return Collections.EMPTY_LIST;
List<GroupModel> list = new LinkedList<>();
for (String id : cached.getGroups()) {
GroupModel group = cacheSession.getGroupById(id, this);
if (group == null) continue;
list.add(group);
}
return list;
}
@Override
public List<GroupModel> getTopLevelGroups() {
List<GroupModel> all = getGroups();
Iterator<GroupModel> it = all.iterator();
while (it.hasNext()) {
GroupModel group = it.next();
if (group.getParent() != null) {
it.remove();
}
}
return all;
}
@Override
public boolean removeGroup(GroupModel group) {
getDelegateForUpdate();
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

@ -317,6 +317,44 @@ public class UserAdapter implements UserModel {
updated.deleteRoleMapping(role); updated.deleteRoleMapping(role);
} }
@Override
public Set<GroupModel> getGroups() {
if (updated != null) return updated.getGroups();
Set<GroupModel> groups = new HashSet<GroupModel>();
for (String id : cached.getRoleMappings()) {
GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm);
if (groupModel == null) {
// chance that role was removed, so just delete to persistence and get user invalidated
getDelegateForUpdate();
return updated.getGroups();
}
groups.add(groupModel);
}
return groups;
}
@Override
public void joinGroup(GroupModel group) {
getDelegateForUpdate();
updated.joinGroup(group);
}
@Override
public void leaveGroup(GroupModel group) {
getDelegateForUpdate();
updated.leaveGroup(group);
}
@Override
public boolean isMemberOf(GroupModel group) {
if (updated != null) return updated.isMemberOf(group);
if (cached.getGroups().contains(group.getId())) return true;
Set<GroupModel> roles = getGroups();
return KeycloakModelUtils.isMember(roles, group);
}
@Override @Override
public void addConsent(UserConsentModel consent) { public void addConsent(UserConsentModel consent) {
getDelegateForUpdate(); getDelegateForUpdate();
@ -348,4 +386,5 @@ public class UserAdapter implements UserModel {
getDelegateForUpdate(); getDelegateForUpdate();
return updated.revokeConsentForClient(clientId); return updated.revokeConsentForClient(clientId);
} }
} }

View file

@ -17,4 +17,6 @@ public interface CacheRealmProvider extends RealmProvider {
void registerApplicationInvalidation(String id); void registerApplicationInvalidation(String id);
void registerRoleInvalidation(String id); void registerRoleInvalidation(String id);
void registerGroupInvalidation(String id);
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.models.cache; package org.keycloak.models.cache;
import org.keycloak.models.cache.entities.CachedClient; import org.keycloak.models.cache.entities.CachedClient;
import org.keycloak.models.cache.entities.CachedGroup;
import org.keycloak.models.cache.entities.CachedRealm; import org.keycloak.models.cache.entities.CachedRealm;
import org.keycloak.models.cache.entities.CachedRole; import org.keycloak.models.cache.entities.CachedRole;
@ -39,6 +40,16 @@ public interface RealmCache {
void invalidateRoleById(String id); void invalidateRoleById(String id);
CachedGroup getGroup(String id);
void invalidateGroup(CachedGroup role);
void addCachedGroup(CachedGroup role);
void invalidateCachedGroupById(String id);
void invalidateGroupById(String id);
boolean isEnabled(); boolean isEnabled();
void setEnabled(boolean enabled); void setEnabled(boolean enabled);

View file

@ -0,0 +1,74 @@
package org.keycloak.models.cache.entities;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CachedGroup implements Serializable {
private String id;
private String realm;
private String name;
private String parentId;
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> roleMappings = new HashSet<>();
private Set<String> subGroups = new HashSet<>();
public CachedGroup(RealmModel realm, GroupModel group) {
this.id = group.getId();
this.realm = realm.getId();
this.name = group.getName();
this.parentId = group.getParentId();
this.attributes.putAll(group.getAttributes());
for (RoleModel role : group.getRoleMappings()) {
roleMappings.add(role.getId());
}
Set<GroupModel> subGroups1 = group.getSubGroups();
if (subGroups1 != null) {
for (GroupModel subGroup : subGroups1) {
subGroups.add(subGroup.getId());
}
}
}
public String getId() {
return id;
}
public String getRealm() {
return realm;
}
public MultivaluedHashMap<String, String> getAttributes() {
return attributes;
}
public Set<String> getRoleMappings() {
return roleMappings;
}
public String getName() {
return name;
}
public String getParentId() {
return parentId;
}
public Set<String> getSubGroups() {
return subGroups;
}
}

View file

@ -5,6 +5,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
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.OTPPolicy; import org.keycloak.models.OTPPolicy;
@ -106,6 +107,7 @@ public class CachedRealm implements Serializable {
protected Set<String> adminEnabledEventOperations = new HashSet<String>(); protected Set<String> adminEnabledEventOperations = new HashSet<String>();
protected boolean adminEventsDetailsEnabled; protected boolean adminEventsDetailsEnabled;
private List<String> defaultRoles = new LinkedList<String>(); private List<String> defaultRoles = new LinkedList<String>();
private Set<String> groups = new HashSet<String>();
private Map<String, String> realmRoles = new HashMap<String, String>(); private Map<String, String> realmRoles = new HashMap<String, String>();
private Map<String, String> clients = new HashMap<String, String>(); private Map<String, String> clients = new HashMap<String, String>();
private boolean internationalizationEnabled; private boolean internationalizationEnabled;
@ -216,6 +218,9 @@ public class CachedRealm implements Serializable {
executionsById.put(execution.getId(), execution); executionsById.put(execution.getId(), execution);
} }
} }
for (GroupModel group : model.getGroups()) {
groups.add(group.getId());
}
for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) { for (AuthenticatorConfigModel authenticator : model.getAuthenticatorConfigs()) {
authenticatorConfigs.put(authenticator.getId(), authenticator); authenticatorConfigs.put(authenticator.getId(), authenticator);
} }
@ -507,4 +512,8 @@ public class CachedRealm implements Serializable {
public AuthenticationFlowModel getClientAuthenticationFlow() { public AuthenticationFlowModel getClientAuthenticationFlow() {
return clientAuthenticationFlow; return clientAuthenticationFlow;
} }
public Set<String> getGroups() {
return groups;
}
} }

View file

@ -1,5 +1,6 @@
package org.keycloak.models.cache.entities; package org.keycloak.models.cache.entities;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
@ -33,6 +34,7 @@ public class CachedUser implements Serializable {
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>(); private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> requiredActions = new HashSet<>(); private Set<String> requiredActions = new HashSet<>();
private Set<String> roleMappings = new HashSet<>(); private Set<String> roleMappings = new HashSet<>();
private Set<String> groups = new HashSet<>();
public CachedUser(RealmModel realm, UserModel user) { public CachedUser(RealmModel realm, UserModel user) {
this.id = user.getId(); this.id = user.getId();
@ -53,6 +55,12 @@ public class CachedUser implements Serializable {
for (RoleModel role : user.getRoleMappings()) { for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId()); roleMappings.add(role.getId());
} }
Set<GroupModel> groupMappings = user.getGroups();
if (groupMappings != null) {
for (GroupModel group : groupMappings) {
groups.add(group.getId());
}
}
} }
public String getId() { public String getId() {
@ -118,4 +126,8 @@ public class CachedUser implements Serializable {
public String getServiceAccountClientLink() { public String getServiceAccountClientLink() {
return serviceAccountClientLink; return serviceAccountClientLink;
} }
public Set<String> getGroups() {
return groups;
}
} }

View file

@ -0,0 +1,328 @@
package org.keycloak.models.jpa;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.jpa.entities.GroupAttributeEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.GroupRoleMappingEntity;
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserConsentEntity;
import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupAdapter implements GroupModel {
protected GroupEntity group;
protected EntityManager em;
protected RealmModel realm;
public GroupAdapter(RealmModel realm, EntityManager em, GroupEntity group) {
this.em = em;
this.group = group;
this.realm = realm;
}
public GroupEntity getGroup() {
return group;
}
@Override
public String getId() {
return group.getId();
}
@Override
public String getName() {
return group.getName();
}
@Override
public void setName(String name) {
group.setName(name);
}
@Override
public GroupModel getParent() {
GroupEntity parent = group.getParent();
if (parent == null) return null;
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) {
if (model instanceof GroupAdapter) {
return ((GroupAdapter)model).getGroup();
}
return em.getReference(GroupEntity.class, model.getId());
}
@Override
public void setParent(GroupModel parent) {
if (parent == null) group.setParent(null);
else {
GroupEntity parentEntity = toEntity(parent, em);
group.setParent(parentEntity);
}
}
@Override
public void addChild(GroupModel subGroup) {
subGroup.setParent(this);
}
@Override
public void removeChild(GroupModel subGroup) {
subGroup.setParent(null);
}
@Override
public Set<GroupModel> getSubGroups() {
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByParent", String.class);
query.setParameter("parent", group);
List<String> ids = query.getResultList();
Set<GroupModel> set = new HashSet<>();
for (String id : ids) {
GroupModel subGroup = realm.getGroupById(id);
if (subGroup == null) continue;
set.add(subGroup);
}
return set;
}
@Override
public void setSingleAttribute(String name, String value) {
boolean found = false;
List<GroupAttributeEntity> toRemove = new ArrayList<>();
for (GroupAttributeEntity attr : group.getAttributes()) {
if (attr.getName().equals(name)) {
if (!found) {
attr.setValue(value);
found = true;
} else {
toRemove.add(attr);
}
}
}
for (GroupAttributeEntity attr : toRemove) {
em.remove(attr);
group.getAttributes().remove(attr);
}
if (found) {
return;
}
persistAttributeValue(name, value);
}
@Override
public void setAttribute(String name, List<String> values) {
// Remove all existing
removeAttribute(name);
// Put all new
for (String value : values) {
persistAttributeValue(name, value);
}
}
private void persistAttributeValue(String name, String value) {
GroupAttributeEntity attr = new GroupAttributeEntity();
attr.setId(KeycloakModelUtils.generateId());
attr.setName(name);
attr.setValue(value);
attr.setGroup(group);
em.persist(attr);
group.getAttributes().add(attr);
}
@Override
public void removeAttribute(String name) {
Iterator<GroupAttributeEntity> it = group.getAttributes().iterator();
while (it.hasNext()) {
GroupAttributeEntity attr = it.next();
if (attr.getName().equals(name)) {
it.remove();
em.remove(attr);
}
}
}
@Override
public String getFirstAttribute(String name) {
for (GroupAttributeEntity attr : group.getAttributes()) {
if (attr.getName().equals(name)) {
return attr.getValue();
}
}
return null;
}
@Override
public List<String> getAttribute(String name) {
List<String> result = new ArrayList<>();
for (GroupAttributeEntity attr : group.getAttributes()) {
if (attr.getName().equals(name)) {
result.add(attr.getValue());
}
}
return result;
}
@Override
public Map<String, List<String>> getAttributes() {
MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
for (GroupAttributeEntity attr : group.getAttributes()) {
result.add(attr.getName(), attr.getValue());
}
return result;
}
@Override
public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings();
return KeycloakModelUtils.hasRole(roles, role);
}
protected TypedQuery<GroupRoleMappingEntity> getGroupRoleMappingEntityTypedQuery(RoleModel role) {
TypedQuery<GroupRoleMappingEntity> query = em.createNamedQuery("groupHasRole", GroupRoleMappingEntity.class);
query.setParameter("group", getGroup());
query.setParameter("roleId", role.getId());
return query;
}
@Override
public void grantRole(RoleModel role) {
if (hasRole(role)) return;
GroupRoleMappingEntity entity = new GroupRoleMappingEntity();
entity.setGroup(getGroup());
entity.setRoleId(role.getId());
em.persist(entity);
em.flush();
em.detach(entity);
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> realmRoles = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof RealmModel) {
realmRoles.add(role);
}
}
return realmRoles;
}
@Override
public Set<RoleModel> getRoleMappings() {
// we query ids only as the role might be cached and following the @ManyToOne will result in a load
// even if we're getting just the id.
TypedQuery<String> query = em.createNamedQuery("groupRoleMappingIds", String.class);
query.setParameter("group", getGroup());
List<String> ids = query.getResultList();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (String roleId : ids) {
RoleModel roleById = realm.getRoleById(roleId);
if (roleById == null) continue;
roles.add(roleById);
}
return roles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
if (group == null || role == null) return;
TypedQuery<GroupRoleMappingEntity> query = getGroupRoleMappingEntityTypedQuery(role);
List<GroupRoleMappingEntity> results = query.getResultList();
if (results.size() == 0) return;
for (GroupRoleMappingEntity entity : results) {
em.remove(entity);
}
em.flush();
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> roleMappings = getRoleMappings();
Set<RoleModel> roles = new HashSet<RoleModel>();
for (RoleModel role : roleMappings) {
RoleContainerModel container = role.getContainer();
if (container instanceof ClientModel) {
ClientModel appModel = (ClientModel)container;
if (appModel.getId().equals(app.getId())) {
roles.add(role);
}
}
}
return roles;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
UserModel that = (UserModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
}

View file

@ -2,11 +2,13 @@ package org.keycloak.models.jpa;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider; import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.jpa.entities.ClientEntity; import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.RealmEntity; import org.keycloak.models.jpa.entities.RealmEntity;
import org.keycloak.models.jpa.entities.RoleEntity; import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -101,6 +103,14 @@ public class JpaRealmProvider implements RealmProvider {
adapter.removeClient(a.getId()); adapter.removeClient(a.getId());
} }
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupsByRealm")
.setParameter("realm", realm).executeUpdate();
em.remove(realm); em.remove(realm);
return true; return true;
} }
@ -117,6 +127,14 @@ public class JpaRealmProvider implements RealmProvider {
return new RoleAdapter(realm, em, entity); return new RoleAdapter(realm, em, entity);
} }
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
GroupEntity groupEntity = em.find(GroupEntity.class, id);
if (groupEntity == null) return null;
if (!groupEntity.getRealm().getId().equals(realm.getId())) return null;
return new GroupAdapter(realm, em, groupEntity);
}
@Override @Override
public ClientModel getClientById(String id, RealmModel realm) { public ClientModel getClientById(String id, RealmModel realm) {
ClientEntity app = em.find(ClientEntity.class, id); ClientEntity app = em.find(ClientEntity.class, id);

View file

@ -3,6 +3,7 @@ package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -175,6 +176,8 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUsersByRealm") num = em.createNamedQuery("deleteUsersByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserGroupMembershipByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
} }
@Override @Override
@ -225,6 +228,25 @@ public class JpaUserProvider implements UserProvider {
.executeUpdate(); .executeUpdate();
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
TypedQuery<UserEntity> query = em.createNamedQuery("groupMembership", UserEntity.class);
query.setParameter("groupId", group.getId());
List<UserEntity> results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
for (UserEntity user : results) {
users.add(new UserAdapter(realm, em, user));
}
return users;
}
@Override
public void preRemove(RealmModel realm, GroupModel group) {
em.createNamedQuery("deleteUserGroupMembershipsByGroup").setParameter("groupId", group.getId()).executeUpdate();
}
@Override @Override
public UserModel getUserById(String id, RealmModel realm) { public UserModel getUserById(String id, RealmModel realm) {
TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserById", UserEntity.class); TypedQuery<UserEntity> query = em.createNamedQuery("getRealmUserById", UserEntity.class);
@ -324,6 +346,25 @@ public class JpaUserProvider implements UserProvider {
return users; return users;
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
TypedQuery<UserEntity> query = em.createNamedQuery("groupMembership", UserEntity.class);
query.setParameter("groupId", group.getId());
if (firstResult != -1) {
query.setFirstResult(firstResult);
}
if (maxResults != -1) {
query.setMaxResults(maxResults);
}
List<UserEntity> results = query.getResultList();
List<UserModel> users = new ArrayList<UserModel>();
for (UserEntity user : results) {
users.add(new UserAdapter(realm, em, user));
}
return users;
}
@Override @Override
public List<UserModel> searchForUser(String search, RealmModel realm) { public List<UserModel> searchForUser(String search, RealmModel realm) {
return searchForUser(search, realm, -1, -1); return searchForUser(search, realm, -1, -1);

View file

@ -6,6 +6,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
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.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -24,6 +25,7 @@ import org.keycloak.models.jpa.entities.AuthenticationExecutionEntity;
import org.keycloak.models.jpa.entities.AuthenticationFlowEntity; import org.keycloak.models.jpa.entities.AuthenticationFlowEntity;
import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity; import org.keycloak.models.jpa.entities.AuthenticatorConfigEntity;
import org.keycloak.models.jpa.entities.ClientEntity; import org.keycloak.models.jpa.entities.ClientEntity;
import org.keycloak.models.jpa.entities.GroupEntity;
import org.keycloak.models.jpa.entities.IdentityProviderEntity; import org.keycloak.models.jpa.entities.IdentityProviderEntity;
import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity; import org.keycloak.models.jpa.entities.IdentityProviderMapperEntity;
import org.keycloak.models.jpa.entities.RealmAttributeEntity; import org.keycloak.models.jpa.entities.RealmAttributeEntity;
@ -1944,4 +1946,84 @@ 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
public GroupModel getGroupById(String id) {
return session.realms().getGroupById(id, this);
}
@Override
public List<GroupModel> getGroups() {
List<GroupModel> list = new LinkedList<>();
Collection<GroupEntity> groups = realm.getGroups();
if (groups == null) return list;
for (GroupEntity entity : groups) {
list.add(new GroupAdapter(this, em, entity));
}
return list;
}
@Override
public List<GroupModel> getTopLevelGroups() {
List<GroupModel> all = getGroups();
Iterator<GroupModel> it = all.iterator();
while (it.hasNext()) {
GroupModel group = it.next();
if (group.getParent() != null) {
it.remove();
}
}
return all;
}
@Override
public boolean removeGroup(GroupModel group) {
if (group == null) {
return false;
}
GroupEntity groupEntity = GroupAdapter.toEntity(group, em);
if (!groupEntity.getRealm().getId().equals(getId())) {
return false;
}
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
session.users().preRemove(this, group);
moveGroup(group, null);
realm.getGroups().remove(groupEntity);
em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", groupEntity).executeUpdate();
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
em.remove(groupEntity);
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

@ -1,6 +1,7 @@
package org.keycloak.models.jpa; package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
@ -19,6 +20,7 @@ import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
import org.keycloak.models.jpa.entities.UserConsentRoleEntity; import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity; import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserEntity; import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.jpa.entities.UserGroupMembershipEntity;
import org.keycloak.models.jpa.entities.UserRequiredActionEntity; import org.keycloak.models.jpa.entities.UserRequiredActionEntity;
import org.keycloak.models.jpa.entities.UserRoleMappingEntity; import org.keycloak.models.jpa.entities.UserRoleMappingEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -485,6 +487,63 @@ public class UserAdapter implements UserModel {
em.flush(); em.flush();
} }
@Override
public Set<GroupModel> getGroups() {
// we query ids only as the group might be cached and following the @ManyToOne will result in a load
// even if we're getting just the id.
TypedQuery<String> query = em.createNamedQuery("userGroupIds", String.class);
query.setParameter("user", getUser());
List<String> ids = query.getResultList();
Set<GroupModel> groups = new HashSet<>();
for (String groupId : ids) {
GroupModel group = realm.getGroupById(groupId);
if (group == null) continue;
groups.add(group);
}
return groups;
}
@Override
public void joinGroup(GroupModel group) {
if (isMemberOf(group)) return;
UserGroupMembershipEntity entity = new UserGroupMembershipEntity();
entity.setUser(getUser());
entity.setGroupId(group.getId());
em.persist(entity);
em.flush();
em.detach(entity);
}
@Override
public void leaveGroup(GroupModel group) {
if (user == null || group == null) return;
TypedQuery<UserGroupMembershipEntity> query = getUserGroupMappingQuery(group);
List<UserGroupMembershipEntity> results = query.getResultList();
if (results.size() == 0) return;
for (UserGroupMembershipEntity entity : results) {
em.remove(entity);
}
em.flush();
}
@Override
public boolean isMemberOf(GroupModel group) {
Set<GroupModel> roles = getGroups();
return KeycloakModelUtils.isMember(roles, group);
}
protected TypedQuery<UserGroupMembershipEntity> getUserGroupMappingQuery(GroupModel group) {
TypedQuery<UserGroupMembershipEntity> query = em.createNamedQuery("userMemberOf", UserGroupMembershipEntity.class);
query.setParameter("user", getUser());
query.setParameter("groupId", group.getId());
return query;
}
@Override @Override
public boolean hasRole(RoleModel role) { public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings(); Set<RoleModel> roles = getRoleMappings();

View file

@ -0,0 +1,70 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="getGroupAttributesByNameAndValue", query="select attr from GroupAttributeEntity attr where attr.name = :name and attr.value = :value"),
@NamedQuery(name="deleteGroupAttributesByGroup", query="delete from GroupAttributeEntity attr where attr.group = :group"),
@NamedQuery(name="deleteGroupAttributesByRealm", query="delete from GroupAttributeEntity attr where attr.group IN (select u from GroupEntity u where u.realm=:realm)")
})
@Table(name="GROUP_ATTRIBUTE")
@Entity
public class GroupAttributeEntity {
@Id
@Column(name="ID", length = 36)
protected String id;
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "GROUP_ID")
protected GroupEntity group;
@Column(name = "NAME")
protected String name;
@Column(name = "VALUE")
protected String value;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public GroupEntity getGroup() {
return group;
}
public void setGroup(GroupEntity group) {
this.group = group;
}
}

View file

@ -0,0 +1,108 @@
package org.keycloak.models.jpa.entities;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.Collection;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="getAllGroupsByRealm", query="select u from GroupEntity u where u.realm = :realm order by u.name"),
@NamedQuery(name="getGroupById", query="select u from GroupEntity u where u.id = :id and u.realm = :realm"),
@NamedQuery(name="getGroupIdsByParent", query="select u.id from GroupEntity u where u.parent = :parent"),
@NamedQuery(name="getGroupCount", query="select count(u) from GroupEntity u where u.realm = :realm"),
@NamedQuery(name="deleteGroupsByRealm", query="delete from GroupEntity u where u.realm = :realm")
})
@Entity
@Table(name="KEYCLOAK_GROUP")
public class GroupEntity {
@Id
@Column(name="ID", length = 36)
protected String id;
@Column(name = "NAME")
protected String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PARENT_GROUP")
private GroupEntity parent;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "REALM_ID")
private RealmEntity realm;
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="group")
protected Collection<GroupAttributeEntity> attributes = new ArrayList<GroupAttributeEntity>();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Collection<GroupAttributeEntity> getAttributes() {
return attributes;
}
public void setAttributes(Collection<GroupAttributeEntity> attributes) {
this.attributes = attributes;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public RealmEntity getRealm() {
return realm;
}
public void setRealm(RealmEntity realm) {
this.realm = realm;
}
public GroupEntity getParent() {
return parent;
}
public void setParent(GroupEntity parent) {
this.parent = parent;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GroupEntity that = (GroupEntity) o;
if (!id.equals(that.id)) return false;
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
}

View file

@ -0,0 +1,100 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="groupHasRole", query="select m from GroupRoleMappingEntity m where m.group = :group and m.roleId = :roleId"),
@NamedQuery(name="groupRoleMappings", query="select m from GroupRoleMappingEntity m where m.group = :group"),
@NamedQuery(name="groupRoleMappingIds", query="select m.roleId from GroupRoleMappingEntity m where m.group = :group"),
@NamedQuery(name="deleteGroupRoleMappingsByRealm", query="delete from GroupRoleMappingEntity mapping where mapping.group IN (select u from GroupEntity u where u.realm=:realm)"),
@NamedQuery(name="deleteGroupRoleMappingsByRole", query="delete from GroupRoleMappingEntity m where m.roleId = :roleId"),
@NamedQuery(name="deleteGroupRoleMappingsByGroup", query="delete from GroupRoleMappingEntity m where m.group = :group")
})
@Table(name="GROUP_ROLE_MAPPING")
@Entity
@IdClass(GroupRoleMappingEntity.Key.class)
public class GroupRoleMappingEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="GROUP_ID")
protected GroupEntity group;
@Id
@Column(name = "ROLE_ID")
protected String roleId;
public GroupEntity getGroup() {
return group;
}
public void setGroup(GroupEntity group) {
this.group = group;
}
public String getRoleId() {
return roleId;
}
public void setRoleId(String roleId) {
this.roleId = roleId;
}
public static class Key implements Serializable {
protected GroupEntity group;
protected String roleId;
public Key() {
}
public Key(GroupEntity group, String roleId) {
this.group = group;
this.roleId = roleId;
}
public GroupEntity getGroup() {
return group;
}
public String getRoleId() {
return roleId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (!roleId.equals(key.roleId)) return false;
if (!group.equals(key.group)) return false;
return true;
}
@Override
public int hashCode() {
int result = group.hashCode();
result = 31 * result + roleId.hashCode();
return result;
}
}
}

View file

@ -133,6 +133,9 @@ public class RealmEntity {
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<RoleEntity> roles = new ArrayList<RoleEntity>(); Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<GroupEntity> groups = new ArrayList<GroupEntity>();
@ElementCollection @ElementCollection
@MapKeyColumn(name="NAME") @MapKeyColumn(name="NAME")
@Column(name="VALUE") @Column(name="VALUE")
@ -718,5 +721,21 @@ public class RealmEntity {
public void setClientAuthenticationFlow(String clientAuthenticationFlow) { public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
this.clientAuthenticationFlow = clientAuthenticationFlow; this.clientAuthenticationFlow = clientAuthenticationFlow;
} }
public Collection<GroupEntity> getGroups() {
return groups;
}
public void setGroups(Collection<GroupEntity> groups) {
this.groups = groups;
}
public void addGroup(GroupEntity group) {
if (groups == null) {
groups = new ArrayList<GroupEntity>();
}
groups.add(group);
}
} }

View file

@ -0,0 +1,102 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name="userMemberOf", query="select m from UserGroupMembershipEntity m where m.user = :user and m.groupId = :groupId"),
@NamedQuery(name="userGroupMembership", query="select m from UserGroupMembershipEntity m where m.user = :user"),
@NamedQuery(name="groupMembership", query="select g.user from UserGroupMembershipEntity g where g.groupId = :groupId"),
@NamedQuery(name="userGroupIds", query="select m.groupId from UserGroupMembershipEntity m where m.user = :user"),
@NamedQuery(name="deleteUserGroupMembershipByRealm", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteUserGroupMembershipsByRealmAndLink", query="delete from UserGroupMembershipEntity mapping where mapping.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
@NamedQuery(name="deleteUserGroupMembershipsByGroup", query="delete from UserGroupMembershipEntity m where m.groupId = :groupId"),
@NamedQuery(name="deleteUserGroupMembershipsByUser", query="delete from UserGroupMembershipEntity m where m.user = :user")
})
@Table(name="USER_GROUP_MEMBERSHIP")
@Entity
@IdClass(UserGroupMembershipEntity.Key.class)
public class UserGroupMembershipEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name="USER_ID")
protected UserEntity user;
@Id
@Column(name = "GROUP_ID")
protected String groupId;
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public static class Key implements Serializable {
protected UserEntity user;
protected String groupId;
public Key() {
}
public Key(UserEntity user, String groupId) {
this.user = user;
this.groupId = groupId;
}
public UserEntity getUser() {
return user;
}
public String getGroupId() {
return groupId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (!groupId.equals(key.groupId)) return false;
if (!user.equals(key.user)) return false;
return true;
}
@Override
public int hashCode() {
int result = user.hashCode();
result = 31 * result + groupId.hashCode();
return result;
}
}
}

View file

@ -0,0 +1,230 @@
package org.keycloak.models.mongo.keycloak.adapters;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GroupAdapter extends AbstractMongoAdapter<MongoGroupEntity> implements GroupModel {
private final MongoGroupEntity group;
private RealmModel realm;
private KeycloakSession session;
public GroupAdapter(KeycloakSession session, RealmModel realm, MongoGroupEntity group, MongoStoreInvocationContext invContext) {
super(invContext);
this.group = group;
this.realm = realm;
this.session = session;
}
@Override
public String getId() {
return group.getId();
}
@Override
public String getName() {
return group.getName();
}
@Override
public void setName(String name) {
group.setName(name);
updateGroup();
}
protected void updateGroup() {
super.updateMongoEntity();
}
@Override
public MongoGroupEntity getMongoEntity() {
return group;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof GroupModel)) return false;
GroupModel that = (GroupModel) o;
return that.getId().equals(getId());
}
@Override
public int hashCode() {
return getId().hashCode();
}
@Override
public void setSingleAttribute(String name, String value) {
if (group.getAttributes() == null) {
group.setAttributes(new HashMap<String, List<String>>());
}
List<String> attrValues = new ArrayList<>();
attrValues.add(value);
group.getAttributes().put(name, attrValues);
updateGroup();
}
@Override
public void setAttribute(String name, List<String> values) {
if (group.getAttributes() == null) {
group.setAttributes(new HashMap<String, List<String>>());
}
group.getAttributes().put(name, values);
updateGroup();
}
@Override
public void removeAttribute(String name) {
if (group.getAttributes() == null) return;
group.getAttributes().remove(name);
updateGroup();
}
@Override
public String getFirstAttribute(String name) {
if (group.getAttributes()==null) return null;
List<String> attrValues = group.getAttributes().get(name);
return (attrValues==null || attrValues.isEmpty()) ? null : attrValues.get(0);
}
@Override
public List<String> getAttribute(String name) {
if (group.getAttributes()==null) return Collections.<String>emptyList();
List<String> attrValues = group.getAttributes().get(name);
return (attrValues == null) ? Collections.<String>emptyList() : Collections.unmodifiableList(attrValues);
}
@Override
public Map<String, List<String>> getAttributes() {
return group.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) group.getAttributes());
}
@Override
public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings();
return KeycloakModelUtils.hasRole(roles, role);
}
@Override
public void grantRole(RoleModel role) {
getMongoStore().pushItemToList(group, "roleIds", role.getId(), true, invocationContext);
}
@Override
public Set<RoleModel> getRoleMappings() {
if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET;
Set<RoleModel> roles = new HashSet<>();
for (String id : group.getRoleIds()) {
roles.add(realm.getRoleById(id));
}
return roles;
}
@Override
public Set<RoleModel> getRealmRoleMappings() {
Set<RoleModel> allRoles = getRoleMappings();
// Filter to retrieve just realm roles
Set<RoleModel> realmRoles = new HashSet<RoleModel>();
for (RoleModel role : allRoles) {
if (role.getContainer() instanceof RealmModel) {
realmRoles.add(role);
}
}
return realmRoles;
}
@Override
public void deleteRoleMapping(RoleModel role) {
if (group == null || role == null) return;
getMongoStore().pullItemFromList(group, "roleIds", role.getId(), invocationContext);
}
@Override
public Set<RoleModel> getClientRoleMappings(ClientModel app) {
Set<RoleModel> result = new HashSet<RoleModel>();
Set<RoleModel> roles = getRoleMappings();
for (RoleModel role : roles) {
if (app.equals(role.getContainer())) {
result.add(role);
}
}
return result;
}
@Override
public GroupModel getParent() {
if (group.getParentId() == null) return null;
return realm.getGroupById(group.getParentId());
}
@Override
public String getParentId() {
return group.getParentId();
}
@Override
public Set<GroupModel> getSubGroups() {
Set<GroupModel> subGroups = new HashSet<>();
for (GroupModel groupModel : realm.getGroups()) {
if (groupModel.getParent().equals(this)) {
subGroups.add(groupModel);
}
}
return subGroups;
}
@Override
public void setParent(GroupModel group) {
this.group.setParentId(group.getId());
updateGroup();
}
@Override
public void addChild(GroupModel subGroup) {
subGroup.setParent(this);
updateGroup();
}
@Override
public void removeChild(GroupModel subGroup) {
subGroup.setParent(null);
updateGroup();
}
}

View file

@ -7,11 +7,13 @@ import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.migration.MigrationModel; import org.keycloak.migration.MigrationModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider; import org.keycloak.models.RealmProvider;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity; import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
@ -121,6 +123,14 @@ public class MongoRealmProvider implements RealmProvider {
return new RoleAdapter(session, realm, role, null, invocationContext); return new RoleAdapter(session, realm, role, null, invocationContext);
} }
@Override
public GroupModel getGroupById(String id, RealmModel realm) {
MongoGroupEntity group = getMongoStore().loadEntity(MongoGroupEntity.class, id, invocationContext);
if (group == null) return null;
if (group.getRealmId() != null && !group.getRealmId().equals(realm.getId())) return null;
return new GroupAdapter(session, realm, group, invocationContext);
}
@Override @Override
public ClientModel getClientById(String id, RealmModel realm) { public ClientModel getClientById(String id, RealmModel realm) {
MongoClientEntity appData = getMongoStore().loadEntity(MongoClientEntity.class, id, invocationContext); MongoClientEntity appData = getMongoStore().loadEntity(MongoClientEntity.class, id, invocationContext);

View file

@ -9,6 +9,7 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -90,10 +91,26 @@ public class MongoUserProvider implements UserProvider {
} }
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) {
QueryBuilder queryBuilder = new QueryBuilder()
.and("realmId").is(realm.getId());
queryBuilder.and("groupIds").is(group.getId());
DBObject sort = new BasicDBObject("username", 1);
List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, queryBuilder.get(), sort, firstResult, maxResults, invocationContext);
return convertUserEntities(realm, users);
}
protected MongoStore getMongoStore() { protected MongoStore getMongoStore() {
return invocationContext.getMongoStore(); return invocationContext.getMongoStore();
} }
@Override
public List<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
return getGroupMembers(realm, group, -1, -1);
}
@Override @Override
public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) { public UserModel getUserByFederatedIdentity(FederatedIdentityModel socialLink, RealmModel realm) {
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()
@ -424,6 +441,17 @@ public class MongoUserProvider implements UserProvider {
getMongoStore().updateEntities(MongoUserConsentEntity.class, query, pull, invocationContext); getMongoStore().updateEntities(MongoUserConsentEntity.class, query, pull, invocationContext);
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
// Remove this role from all users, which has it
DBObject query = new QueryBuilder()
.and("groupIds").is(group.getId())
.get();
DBObject pull = new BasicDBObject("$pull", query);
getMongoStore().updateEntities(MongoUserEntity.class, query, pull, invocationContext);
}
@Override @Override
public void preRemove(RealmModel realm, RoleModel role) { public void preRemove(RealmModel realm, RoleModel role) {
// Remove this role from all users, which has it // Remove this role from all users, which has it

View file

@ -9,6 +9,7 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
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.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -34,6 +35,7 @@ import org.keycloak.models.entities.RequiredCredentialEntity;
import org.keycloak.models.entities.UserFederationMapperEntity; import org.keycloak.models.entities.UserFederationMapperEntity;
import org.keycloak.models.entities.UserFederationProviderEntity; import org.keycloak.models.entities.UserFederationProviderEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity; import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -607,6 +609,79 @@ 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
public GroupModel getGroupById(String id) {
return model.getGroupById(id, this);
}
@Override
public List<GroupModel> getGroups() {
DBObject query = new QueryBuilder()
.and("realmId").is(getId())
.get();
List<MongoGroupEntity> groups = getMongoStore().loadEntities(MongoGroupEntity.class, query, invocationContext);
List<GroupModel> result = new LinkedList<>();
if (groups == null) return result;
for (MongoGroupEntity group : groups) {
result.add(new GroupAdapter(session, this, group, invocationContext));
}
return result;
}
@Override
public List<GroupModel> getTopLevelGroups() {
List<GroupModel> all = getGroups();
Iterator<GroupModel> it = all.iterator();
while (it.hasNext()) {
GroupModel group = it.next();
if (group.getParent() != null) {
it.remove();
}
}
return all;
}
@Override
public boolean removeGroup(GroupModel group) {
for (GroupModel subGroup : group.getSubGroups()) {
removeGroup(subGroup);
}
session.users().preRemove(this, group);
moveGroup(group, null);
return getMongoStore().removeEntity(MongoGroupEntity.class, group.getId(), invocationContext);
}
@Override @Override
public List<String> getDefaultRoles() { public List<String> getDefaultRoles() {
return realm.getDefaultRoles(); return realm.getDefaultRoles();

View file

@ -7,6 +7,7 @@ import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
@ -450,6 +451,37 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
return user; return user;
} }
@Override
public Set<GroupModel> getGroups() {
if (user.getGroupIds() == null && user.getGroupIds().size() == 0) return Collections.EMPTY_SET;
Set<GroupModel> groups = new HashSet<>();
for (String id : user.getGroupIds()) {
groups.add(realm.getGroupById(id));
}
return groups;
}
@Override
public void joinGroup(GroupModel group) {
getMongoStore().pushItemToList(getUser(), "groupIds", group.getId(), true, invocationContext);
}
@Override
public void leaveGroup(GroupModel group) {
if (user == null || group == null) return;
getMongoStore().pullItemFromList(getUser(), "groupIds", group.getId(), invocationContext);
}
@Override
public boolean isMemberOf(GroupModel group) {
if (user.getGroupIds().contains(group.getId())) return true;
Set<GroupModel> groups = getGroups();
return KeycloakModelUtils.isMember(groups, group);
}
@Override @Override
public boolean hasRole(RoleModel role) { public boolean hasRole(RoleModel role) {
Set<RoleModel> roles = getRoleMappings(); Set<RoleModel> roles = getRoleMappings();

View file

@ -0,0 +1,26 @@
package org.keycloak.models.mongo.keycloak.entities;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoField;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.GroupEntity;
import org.keycloak.models.entities.RoleEntity;
import java.util.List;
/**
*/
@MongoCollection(collectionName = "groups")
public class MongoGroupEntity extends GroupEntity implements MongoIdentifiableEntity {
private static final Logger logger = Logger.getLogger(MongoGroupEntity.class);
@Override
public void afterRemove(MongoStoreInvocationContext invContext) {
}
}

View file

@ -4,11 +4,13 @@ import com.mongodb.DBObject;
import com.mongodb.QueryBuilder; import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.entities.ClientEntity; import org.keycloak.models.entities.ClientEntity;
import org.keycloak.models.mongo.keycloak.adapters.ClientAdapter; import org.keycloak.models.mongo.keycloak.adapters.ClientAdapter;
import org.keycloak.models.mongo.keycloak.adapters.GroupAdapter;
import org.keycloak.models.mongo.keycloak.adapters.UserAdapter; import org.keycloak.models.mongo.keycloak.adapters.UserAdapter;
import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity; import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;

View file

@ -511,6 +511,7 @@ public class SamlService {
processor.setClientSession(clientSession) processor.setClientSession(clientSession)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH) .setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
.setFlowId(flowId) .setFlowId(flowId)
.setBrowserFlow(true)
.setConnection(clientConnection) .setConnection(clientConnection)
.setEventBuilder(event) .setEventBuilder(event)
.setProtector(authManager.getProtector()) .setProtector(authManager.getProtector())

View file

@ -347,6 +347,7 @@ public class AuthorizationEndpoint {
processor.setClientSession(clientSession) processor.setClientSession(clientSession)
.setFlowPath(flowPath) .setFlowPath(flowPath)
.setFlowId(flowId) .setFlowId(flowId)
.setBrowserFlow(true)
.setConnection(clientConnection) .setConnection(clientConnection)
.setEventBuilder(event) .setEventBuilder(event)
.setProtector(authManager.getProtector()) .setProtector(authManager.getProtector())

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;
@ -137,7 +138,7 @@ public class UsersResource {
throw new NotFoundException("User not found"); throw new NotFoundException("User not found");
} }
Set<String> attrsToRemove; Set<String> attrsToRemove;
if (rep.getAttributes() != null) { if (rep.getAttributes() != null) {
attrsToRemove = new HashSet<>(user.getAttributes().keySet()); attrsToRemove = new HashSet<>(user.getAttributes().keySet());
attrsToRemove.removeAll(rep.getAttributes().keySet()); attrsToRemove.removeAll(rep.getAttributes().keySet());
@ -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

@ -1,6 +1,7 @@
package org.keycloak.testsuite; package org.keycloak.testsuite;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
@ -67,6 +68,11 @@ public class DummyUserFederationProvider implements UserFederationProvider {
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
@Override @Override
public boolean isValid(RealmModel realm, UserModel local) { public boolean isValid(RealmModel realm, UserModel local) {
return false; return false;

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")) {