简体   繁体   中英

Grails Spring Security querying users which don't have a certain role

Using Grails spring security REST (which in itself uses Grails Spring Security Core ) I've generated User , Role , UserRole classes.

User:

class User extends DomainBase{

    transient springSecurityService

    String username
    String password
    String firstName
    String lastNameOrTitle
    String email
    boolean showEmail
    String phoneNumber
    boolean enabled = true
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    static transients = ['springSecurityService']

    static hasMany = [
            roles: Role,
            ratings: Rating,
            favorites: Favorite
    ]

    static constraints = {
        username blank: false, unique: true
        password blank: false
        firstName nullable: true, blank: false
        lastNameOrTitle nullable: false, blank: false
        email nullable: false, blank: false
        phoneNumber nullable: true
    }

    static mapping = {
        DomainUtil.inheritDomainMappingFrom(DomainBase, delegate)
        id column: 'user_id', generator: 'sequence', params: [sequence: 'user_seq']
        username column: 'username'
        password column: 'password'
        enabled column: 'enabled'
        accountExpired column: 'account_expired'
        accountLocked column: 'account_locked'
        passwordExpired column: 'password_expired'
        roles joinTable: [
                name: 'user_role',
                column: 'role_id',
                key: 'user_id']
    }

    Set<Role> getAuthorities() {
//        UserRole.findAllByUser(this).collect { it.role }
//        userRoles.collect { it.role }
        this.roles
    }

    def beforeInsert() {
        encodePassword()
    }

    def beforeUpdate() {
        super.beforeUpdate()
        if (isDirty('password')) {
            encodePassword()
        }
    }

    protected void encodePassword() {
        password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }
}

Role:

class Role {

    String authority

    static mapping = {
        cache true
        id column: 'role_id', generator: 'sequence', params: [sequence: 'role_seq']
        authority column: 'authority'
    }

    static constraints = {
        authority blank: false, unique: true
    }
}

UserRole:

class UserRole implements Serializable {

    private static final long serialVersionUID = 1

    static belongsTo = [
            user: User,
            role: Role
    ]
//    User user
//    Role role

    boolean equals(other) {
        if (!(other instanceof UserRole)) {
            return false
        }

        other.user?.id == user?.id &&
                other.role?.id == role?.id
    }

    int hashCode() {
        def builder = new HashCodeBuilder()
        if (user) builder.append(user.id)
        if (role) builder.append(role.id)
        builder.toHashCode()
    }

    static UserRole get(long userId, long roleId) {
        UserRole.where {
            user == User.load(userId) &&
                    role == Role.load(roleId)
        }.get()
    }

    static boolean exists(long userId, long roleId) {
        UserRole.where {
            user == User.load(userId) &&
                    role == Role.load(roleId)
        }.count() > 0
    }

    static UserRole create(User user, Role role, boolean flush = false) {
        def instance = new UserRole(user: user, role: role)
        instance.save(flush: flush, insert: true)
        instance
    }

    static boolean remove(User u, Role r, boolean flush = false) {
        if (u == null || r == null) return false

        int rowCount = UserRole.where {
            user == User.load(u.id) &&
                    role == Role.load(r.id)
        }.deleteAll()

        if (flush) {
            UserRole.withSession { it.flush() }
        }

        rowCount > 0
    }

    static void removeAll(User u, boolean flush = false) {
        if (u == null) return

        UserRole.where {
            user == User.load(u.id)
        }.deleteAll()

        if (flush) {
            UserRole.withSession { it.flush() }
        }
    }

    static void removeAll(Role r, boolean flush = false) {
        if (r == null) return

        UserRole.where {
            role == Role.load(r.id)
        }.deleteAll()

        if (flush) {
            UserRole.withSession { it.flush() }
        }
    }

    static constraints = {
        role validator: { Role r, UserRole ur ->
            if (ur.user == null) return
            boolean existing = false
            UserRole.withNewSession {
                existing = UserRole.exists(ur.user.id, r.id)
            }
            if (existing) {
                return 'userRole.exists'
            }
        }
    }

    static mapping = {
        id composite: ['role', 'user']
        version false
    }
}

Now I wish to create an admin area where admins can modify/enable user accounts, but can't touch other admins, so for that I've decided to create a pageable query which would select only the users which don't have the ROLE_ADMIN role, since admins have both ROLE_USER and ROLE_ADMIN roles.

As can be seen from the above code, I've modified the default generated code a bit and added a joinTable to the User class instead of hasMany: [roles:UserRole] or keeping it at the default without any references to roles. The reason for this change is because when querying UserRole I'd occasionally get duplicates which would make pagination difficult.

So with this current setup I've managed to create two queries which allow me to fetch only users which do not have an admin role.

def rolesToIgnore = ["ROLE_ADMIN"]
def userIdsWithGivenRoles = User.createCriteria().list() {
    projections {
        property "id"
    }
    roles {
        'in' "authority", rolesToIgnore
    }
}

def usersWithoutGivenRoles = User.createCriteria().list(max: 10, offset: 0) {
    not {
        'in' "id", userIdsWithGivenRoles
    }
}

First query fetches a list of all the user ids which have the ROLE_ADMIN role, and then the second query fetches all the users whose id is not in the previous list.

This works and is pageable, however It bothers me for two reasons:

  1. joinTable on User just seems "icky" to me. Why use a joinTable when i already have a specific class for that purpose UserRole , however that class is more difficult to query and I'm afraid of possible overhead of mapping Role for each found User even though I only need the User .
  2. Two queries, and only the second one can be paged.

So my questions are: Is there a more optimal way to construct a query for fetching users which don't contain certain roles (without restructuring the database into a pyramid role system where every user has only one role)?

Are two queries absolutely necessary? I've tried to construct a pure SQL query and I couldn't do it without subqueries.

If your UserRole has user and role properties instead of belongsTo , like this:

class UserRole implements Serializable {

    private static final long serialVersionUID = 1

    User user
    Role role
    ...
}

Then you can do this:

def rolesToIgnore = ["ROLE_ADMIN"]

def userIdsWithGivenRoles = UserRole.where {
    role.authority in rolesToIgnore
}.list().collect { it.user.id }.unique()

def userIdsWithoutGivenRoles = UserRole.where {
    !(role.authority in rolesToIgnore)
}.list().collect { it.user.id }.unique()

I suck at projections, so I remove duplicates with unique().

The SQL equivalents are:

SELECT DISTINCT ur.user_id
FROM    user_role AS ur INNER JOIN role AS r
        ON ur.authority_id = r.id
WHERE   r.authority IN ('ROLE_ADMIN');

SELECT DISTINCT ur.user_id
FROM    user_role AS ur INNER JOIN role AS r
        ON ur.authority_id = r.id
WHERE   r.authority NOT IN ('ROLE_ADMIN');

You can do something like it:

return UserRole.createCriteria().list {
    distinct('user')
    user {
        ne("enabled", false)
    }
    or {
        user {
            eq('id', springSecurityService.currentUser.id)
        }
        role {
            not {
                'in'('authority', ['ADMIN', 'EXECUTIVE'])
            }
        }
    }
}

With distinct('user') you only will get the Users

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM