Introduction
The Grails Security Bridge plugin provides a decoupled, cross-plugin security interface for Grails applications. It allows you to keep the majority of authentication logic in one plugin, while other plugins can reference a common API to retrieve user identity, authorization status, and other security information without depending on a specific security implementation.
Key Features
-
Decoupled Security Interface: A common
SecurityBridgeinterface that any security provider can implement -
Shared Security Service: A single
SharedSecurityServicethat delegates to the active bridge implementation -
Annotation-Based Access Control: Use
@Secureannotations on controllers and actions to restrict access by role -
Interceptor-Based Enforcement: Automatic interception of requests to enforce
@Secureannotations -
GSP Tag Library: Security-aware tags for conditionally rendering content in views
-
Multi-Plugin Friendly: Designed so multiple plugins can share a single security contract without tight coupling
Installation
Grails 7
Add the plugin dependency to your build.gradle:
dependencies {
implementation 'cloud.wondrify:grails-security-bridge:7.0.0'
}
Grails 6.x and Earlier
For Grails 6.x and earlier versions, use the previous artifact coordinates:
dependencies {
implementation 'com.bertramlabs.plugins:grails-security-bridge:6.x.x'
}
Quick Start
To use the Security Bridge plugin, you need to:
-
Add the plugin dependency
-
Create a class that implements the
SecurityBridgeinterface -
Register your implementation as a Spring bean named
sharedSecurityBridge -
Optionally annotate controllers with
@Secure
Step 1: Implement the SecurityBridge Interface
Create a class that implements SecurityBridge (or extends AbstractSecurityBridge for convenience):
package com.myapp.security
import org.grails.plugin.securitybridge.AbstractSecurityBridge
class MySecurityBridge extends AbstractSecurityBridge {
def springSecurityService // or whatever your auth provider is
def getCurrentUser() {
springSecurityService.currentUser
}
def getUserIdentity() {
springSecurityService.currentUser?.username
}
def getCurrentUserDisplayName() {
def user = springSecurityService.currentUser
return user ? "${user.firstName} ${user.lastName}" : null
}
boolean isLoggedIn() {
springSecurityService.isLoggedIn()
}
boolean hasRole(role) {
springSecurityService.currentUser?.authorities?.any {
it.authority == role
} ?: false
}
boolean hasPermission(permission, opts = null) {
// Implement permission checking based on your model
false
}
boolean isAuthorized(object, action) {
// Implement authorization logic
isLoggedIn()
}
def storeLocation(request) {
// Store the original request URL for post-login redirect
}
def withUser(identity, Closure code) {
// Execute closure as the given user
}
Map createLink(String action) {
switch (action) {
case 'login':
return [controller: 'login', action: 'auth']
case 'logout':
return [controller: 'logout']
case 'signup':
return [controller: 'register']
default:
return [:]
}
}
}
Step 2: Register the Bean
In your application’s grails-app/conf/spring/resources.groovy (or via a plugin descriptor), register your bridge:
beans = {
sharedSecurityBridge(com.myapp.security.MySecurityBridge)
}
Step 3: Use the Shared Security Service
Inject sharedSecurityService in any service or controller:
class DashboardController {
def sharedSecurityService
def index() {
if (!sharedSecurityService.isLoggedIn()) {
redirect(sharedSecurityService.createLink('login'))
return
}
[
user: sharedSecurityService.currentUser,
displayName: sharedSecurityService.currentUserDisplayName
]
}
}
SecurityBridge Interface
The SecurityBridge interface defines the contract that all security provider implementations must fulfill.
Methods
-
getCurrentUser()— Returns the current user object, ornullif nobody is logged in -
getUserIdentity()— Returns the user identity (e.g. username), ornull -
getCurrentAccount()— Returns the account object of the logged-in user, ornull -
getAccountIdentity()— Returns the account identity (useful for multi-tenant setups), ornull -
getCurrentUserDisplayName()— Returns the display name of the current user -
isLoggedIn()— Returnstrueif a user is currently authenticated -
isAuthorized(object, action)— Checks whether the current user is authorized to performactiononobject -
hasRole(role)— Checks if the current user has the specified role -
hasPermission(permission)— Checks if the current user has the specified permission -
hasPermission(permission, opts)— Checks permission with additional options -
storeLocation(request)— Stores the current request URL so the user can be redirected after login -
withUser(identity, Closure code)— Executes a closure masquerading as the specified user -
createLink(String action)— Returns a Map of link parameters for security actions ("login","logout","signup")
AbstractSecurityBridge
The AbstractSecurityBridge class provides a convenient base implementation of SecurityBridge where all methods return safe defaults (null, false, or no-op). You can extend it and override only the methods relevant to your security provider.
class MinimalBridge extends AbstractSecurityBridge {
boolean isLoggedIn() {
// your implementation
}
def getCurrentUser() {
// your implementation
}
}
SharedSecurityService
The SharedSecurityService is the primary service you interact with in your application code. It delegates all calls to the registered sharedSecurityBridge bean.
Additional Methods
Beyond the SecurityBridge interface methods, the service provides:
-
hasProvider()— Returnstrueif a security bridge implementation has been registered -
hasAnyRole(roles)— Checks if the current user has any of the given roles (accepts a single role or a Collection) -
ifAuthorized(object, action, Closure code)— Executes the closure only if the user is authorized; the closure receives(currentUser, currentAccount)as arguments
Example
class ReportService {
def sharedSecurityService
def generateReport() {
sharedSecurityService.ifAuthorized('reports', 'generate') { user, account ->
// Build report for this user/account
}
}
}
The @Secure Annotation
The @Secure annotation can be applied to controllers or individual actions to restrict access by role. When a request matches a secured controller/action, the SecurityBridgeInterceptor checks whether the current user has any of the specified roles.
Usage
import org.grails.plugin.securitybridge.Secure
// Secure the entire controller — all actions require ROLE_ADMIN
@Secure(['ROLE_ADMIN'])
class AdminController {
def index() {
// Only accessible to users with ROLE_ADMIN
}
def settings() {
// Also requires ROLE_ADMIN
}
}
import org.grails.plugin.securitybridge.Secure
class UserController {
def index() {
// No restriction
}
// Secure a single action
@Secure(['ROLE_ADMIN', 'ROLE_MANAGER'])
def edit() {
// Only accessible to ROLE_ADMIN or ROLE_MANAGER
}
}
If the user does not have any of the required roles, the interceptor stores the current request location and redirects to the login page (as returned by SecurityBridge.createLink('login')).
Tag Library
The plugin provides GSP tags under the security namespace.
security:ifLoggedIn
Renders the body only if the user is logged in.
<security:ifLoggedIn>
Welcome back!
</security:ifLoggedIn>
security:ifNotLoggedIn
Renders the body only if the user is NOT logged in.
<security:ifNotLoggedIn>
<a href="/login">Please log in</a>
</security:ifNotLoggedIn>
security:currentUserProperty
Outputs a property of the current user object.
Hello, <security:currentUserProperty property="firstName" />!
security:ifAuthorized
Renders the body if the current user is authorized for a given controller/action.
<security:ifAuthorized controller="admin" action="index">
<a href="/admin">Admin Panel</a>
</security:ifAuthorized>
Supports an optional namespace attribute to disambiguate controllers.
security:ifNotAuthorized
Renders the body if the current user is NOT authorized for a given controller/action.
<security:ifNotAuthorized controller="admin" action="index">
You do not have access to the admin panel.
</security:ifNotAuthorized>
security:createLink
Creates an anchor tag linking to a security action (login, logout, signup).
<security:createLink action="login" class="btn">Sign In</security:createLink>
<security:createLink action="logout" class="btn">Sign Out</security:createLink>
Interceptor
The SecurityBridgeInterceptor runs at HIGHEST_PRECEDENCE and matches all requests. It inspects the target controller and action for the @Secure annotation. If present, it checks sharedSecurityService.hasAnyRole(annotation.value()).
-
If the user has one of the required roles, the request proceeds.
-
If not,
storeLocation(request)is called, and the user is redirected to the login link. -
If no
@Secureannotation is present, the request passes through without restriction.
Getting Help
-
GitHub Issues: https://github.com/wondrify/grails-security-bridge/issues
-
Source Code: https://github.com/wondrify/grails-security-bridge
License
Grails Security Bridge is open source software licensed under the Apache License 2.0.