-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
813 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
grails-app/controllers/io/xh/hoist/admin/RoleAdminController.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package io.xh.hoist.admin | ||
|
||
import io.xh.hoist.BaseController | ||
import io.xh.hoist.role.Role | ||
import io.xh.hoist.role.RoleAdminService | ||
import io.xh.hoist.security.Access | ||
|
||
@Access(['HOIST_ROLE_MANAGER']) | ||
class RoleAdminController extends BaseController { | ||
RoleAdminService roleAdminService | ||
|
||
@Access(['HOIST_ADMIN_READER']) | ||
def list() { | ||
List<Map> roles = roleAdminService.list() | ||
renderJSON(data:roles) | ||
} | ||
|
||
def create() { | ||
ensureAuthUserCanEdit() | ||
Map roleSpec = parseRequestJSON() | ||
Role role = roleAdminService.create(roleSpec) | ||
renderJSON(data:role) | ||
} | ||
|
||
def update() { | ||
ensureAuthUserCanEdit() | ||
Map roleSpec = parseRequestJSON() | ||
Role role = roleAdminService.update(roleSpec) | ||
renderJSON(data:role) | ||
} | ||
|
||
def delete(String id) { | ||
ensureAuthUserCanEdit() | ||
roleAdminService.delete(id) | ||
renderJSON(success:true) | ||
} | ||
|
||
|
||
//----------------------- | ||
// Implementation | ||
//----------------------- | ||
private void ensureAuthUserCanEdit() { | ||
if (!authUser.hasRole('HOIST_ROLE_MANAGER')) { | ||
throw new RuntimeException("$authUsername is not a 'HOIST_ROLE_MANAGER'") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
package io.xh.hoist.role | ||
|
||
import io.xh.hoist.json.JSONFormat | ||
|
||
/** | ||
* Backing domain class for Hoist's built-in role management. Methods on this class are used | ||
* internally by Hoist and should not be called directly by application code. | ||
*/ | ||
class Role implements JSONFormat { | ||
String name | ||
String category | ||
String notes | ||
Date lastUpdated | ||
String lastUpdatedBy | ||
static hasMany = [members: RoleMember] | ||
|
||
static mapping = { | ||
table 'xh_role' | ||
id name: 'name', generator: 'assigned', type: 'string' | ||
cache true | ||
members cascade: 'all-delete-orphan', fetch: 'join', cache: true | ||
} | ||
|
||
static constraints = { | ||
category nullable: true, maxSize: 30, blank: false | ||
notes nullable: true, maxSize: 1200 | ||
lastUpdatedBy maxSize: 50 | ||
} | ||
|
||
Map formatForJSON() { | ||
[ | ||
name: name, | ||
category: category, | ||
notes: notes, | ||
lastUpdated: lastUpdated, | ||
lastUpdatedBy: lastUpdatedBy | ||
] | ||
} | ||
|
||
List<String> getUsers() { | ||
members.findAll { it.type == RoleMember.Type.USER }.collect { it.name } | ||
} | ||
|
||
List<String> getDirectoryGroups() { | ||
members.findAll { it.type == RoleMember.Type.DIRECTORY_GROUP }.collect { it.name } | ||
} | ||
|
||
List<String> getRoles() { | ||
members.findAll { it.type == RoleMember.Type.ROLE }.collect { it.name } | ||
} | ||
|
||
Map<RoleMember.Type, List<EffectiveMember>> resolveEffectiveMembers() { | ||
Map<RoleMember.Type, List<EffectiveMember>> ret = [:] | ||
List<EffectiveMember> effectiveRoles = listEffectiveRoles() | ||
|
||
ret.put(RoleMember.Type.USER, listEffectiveUsers(effectiveRoles)) | ||
ret.put(RoleMember.Type.DIRECTORY_GROUP, listEffectiveDirectoryGroups(effectiveRoles)) | ||
ret.put(RoleMember.Type.ROLE, effectiveRoles) | ||
|
||
return ret | ||
} | ||
|
||
/** | ||
* Use BFS to find all roles for which this role is an effective member, with source association | ||
*/ | ||
List<EffectiveMember> listInheritedRoles() { | ||
Set<String> visitedRoles = [name] | ||
Queue<Role> rolesToVisit = [this] as Queue | ||
List<Role> allRoles = list() | ||
Map<String, EffectiveMember> ret = [:].withDefault { new EffectiveMember([name: it])} | ||
|
||
while (!rolesToVisit.isEmpty()) { | ||
Role role = rolesToVisit.poll() | ||
allRoles | ||
.findAll { it.roles.contains(role.name) } | ||
.each { inheritedRole -> | ||
ret[inheritedRole.name].sourceRoles << role.name | ||
if (!visitedRoles.contains(inheritedRole.name)) { | ||
visitedRoles.add(inheritedRole.name) | ||
rolesToVisit.offer(inheritedRole) | ||
} | ||
} | ||
} | ||
|
||
ret.values() as List<EffectiveMember> | ||
} | ||
|
||
//------------------------ | ||
// Implementation | ||
//------------------------ | ||
|
||
/** | ||
* List users, each with a list of role-names justifying why they inherit this role | ||
*/ | ||
private List<EffectiveMember> listEffectiveUsers(List<EffectiveMember> effectiveRoles) { | ||
collectEffectiveMembers(effectiveRoles) { it.users } | ||
} | ||
|
||
/** | ||
* List directory groups, each with a list of role-names justifying why they inherit this role | ||
*/ | ||
private List<EffectiveMember> listEffectiveDirectoryGroups(List<EffectiveMember> effectiveRoles) { | ||
collectEffectiveMembers(effectiveRoles) { it.directoryGroups } | ||
} | ||
|
||
/** | ||
* List effective members of this role with source associations | ||
*/ | ||
private List<EffectiveMember> listEffectiveRoles() { | ||
Set<String> visitedRoles = [name] | ||
Queue<Role> rolesToVisit = new LinkedList<Role>() | ||
rolesToVisit.offer(this) | ||
Map<String, EffectiveMember> ret = [:].withDefault { new EffectiveMember([name: it])} | ||
|
||
while (!rolesToVisit.isEmpty()) { | ||
Role role = rolesToVisit.poll() | ||
role.roles.each { memberName -> | ||
ret[memberName].sourceRoles << role.name | ||
if (!visitedRoles.contains(memberName)) { | ||
visitedRoles.add(memberName) | ||
rolesToVisit.offer(get(memberName)) | ||
} | ||
} | ||
} | ||
|
||
ret.values() as List<EffectiveMember> | ||
} | ||
|
||
/** | ||
* Implementation for `listEffectiveUsers` and `listEffectiveDirectoryGroups` | ||
*/ | ||
private List<EffectiveMember> collectEffectiveMembers( | ||
List<EffectiveMember> sourceRoles, | ||
Closure<List<String>> memberNamesFn | ||
) { | ||
Map<String, EffectiveMember> ret = [:].withDefault { new EffectiveMember([name: it])} | ||
|
||
memberNamesFn(this).each { memberName -> | ||
ret[memberName].sourceRoles << name | ||
} | ||
sourceRoles.each { sourceRole -> | ||
String sourceRoleName = sourceRole.name | ||
memberNamesFn(get(sourceRoleName)).each { memberName -> | ||
ret[memberName].sourceRoles << sourceRoleName | ||
} | ||
} | ||
|
||
ret.values() as List<EffectiveMember> | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package io.xh.hoist.role | ||
|
||
import io.xh.hoist.json.JSONFormat | ||
|
||
class RoleMember implements JSONFormat { | ||
enum Type { | ||
USER, | ||
DIRECTORY_GROUP, | ||
ROLE | ||
} | ||
|
||
Type type | ||
String name | ||
static belongsTo = [role: Role] | ||
|
||
Date dateCreated | ||
String createdBy | ||
|
||
static mapping = { | ||
table 'xh_role_member' | ||
cache true | ||
} | ||
|
||
Map formatForJSON() { | ||
return [ | ||
type: type.name(), | ||
name: name, | ||
dateCreated: dateCreated, | ||
createdBy: createdBy | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.