Security
Authentication is the process of identity verification and authorization is the process of determining access rights to resources in an application.
The authentication and authorization support in Axelor Open Platform is based on Apache Shiro.
In the next few sections we will see how authentication and authorization are supported in Axelor Open Platform.
Authentication
Authentication is the process of identity verification, i.e. allowing users to log in into to the system to use it.
The Axelor Open Platform is web application framework used to create web application. So by default it provided form based user authentication.
User information is backed by the application database and it’s also possible to integrate LDAP backend.
User
The User object has various properties, most important of them are:
-
code
- the user login name -
name
- the display name -
password
- the password (stored encrypted in database) -
blocked
- whether the uses is blocked -
activateOn
- the time from when access should be activated -
expiresOn
- the time from when access should be expired -
groups
- the groups assigned to the user -
roles
- the roles assigned to the user -
permissions
- explicit permissions granted to the user
The groups
, roles
, and permissions
are associated with authorization which
we will see in next section.
Authorization
Authorization, also called access control, is the process of determining access rights to resources in an application.
Authorization is a critical element of any application but it can quickly become very complex. Based on the simplicity of Apache Shiro, the Axelor Open Platform provides very simple yet powerful way to define authorization rules.
Special user admin and members of group admins have
full access to all the resources.
|
Features
-
Role based permissions
-
Permission defines single access rule (finer granularity)
-
Groups are for organizational structure but also supports roles & permissions
-
Deny all, grant selectively (proven most secure as all permissions are denied by default)
-
Package level permission rules
Authorization has four core elements permissions, roles, groups and users. They
are represented by corresponding backing domain objects Permission
, Role
,
Group
and User
respectively.
Objects
-
User
has oneGroup
-
User
has manyRole
-
User
has manyPermission
-
Group
has manyRole
-
Group
has manyPermission
-
Role
has manyPermission
The relationship between the authorization objects allows achieve finer level of granularity on access control.
Permission
The permission object defines the access rule. It has the following properties:
-
name
- permission name -
object
- the object name (class name or wild card package name) -
canRead
- whether to grant read permission -
canWrite
- whether to grant update permission -
canCreate
- whether to grant create permission -
canRemove
- whether to grant delete permission -
canExport
- whether to grant export data permission -
condition
- permission condition (JPQL where clause with positional parameters) -
conditionParams
- comma separated list of condition params (evaluates against current context)
The condition
is optional and the boolean flags are grant only, that is, false
value doesn’t mean deny.
Some permission examples (pseudo code):
name: perm.sale.read.all object: com.axelor.sale.db.* canRead: true
name: perm.sale.create.all object: com.axelor.sale.db.* canCreate: true
name: perm.sale.self object: com.axelor.sale.db.Order canRead: true canWrite: true canRemove: true canExport: true condition: self.createdBy = ? conditionParams: __user__
The first rule grants readonly permission to all the objects under com.axelor.sale.db
package.
The second rule grants create permission to all the objects under com.axelor.sale.db
package.
The third rule grants read, write, delete, export permission on com.axelor.sale.db.Order
to the creator user.
The permission resolution is done in this order:
-
check for permissions assigned to the user object
-
check for permissions assigned to the roles of the user
-
check for the permissions assigned to the group of the user
-
check for the permissions assigned to the group’s roles
View Access
Similar to the object authorization, view access permissions can be used to control object view fields for users, groups and roles.
The Permission (fields)
defined on User
, Group
and Role
objects can be
used to define permission rules for view item.
The permission rules are applied to all the views associated with the given object. The view items should have a name in order to define a rule for them.
The rule also allows setting client side conditions (js expressions) to control readonly/visiblity of the fields/items.
Some examples (pseudo code):
name: perm.sales.hide-total object: com.axelor.sale.db.Order rules: field: totalAmount canRead: false canWrite: false canExport: false
name: perm.sales.customer-change object: com.axelor.sale.db.Order rules: field: customer canRead: true canWrite: true canExport: true readonlyIf: confirmed && __group__ == 'manager' hideIf: __group__ == 'user'
The first rule hides the totalAmount
field from the views.
The second rule defines how the customer
field should behave depending on user group.
Unlike the object permission rules, view permission rules follows Grant all → Deny Selectively
strategy.
LDAP
The authentication system has built-in support for LDAP integration.
In order to enable LDAP authentication, you have to set the following configuration
in your application.properties
file:
# LDAP Configuration
# ~~~~~
ldap.server.url = ldap://localhost:10389
# can be "simple" or "CRAM-MD5"
ldap.auth.type = simple
ldap.system.user = uid=admin,ou=system
ldap.system.password = secret
# group search base
ldap.group.base = ou=groups,dc=example,dc=com
# if set, create groups on ldap server under ldap.group.base
ldap.group.object.class = groupOfUniqueNames
# a template to search groups by user login id
ldap.group.filter = (uniqueMember=uid={0})
# user search base
ldap.user.base = ou=users,dc=example,dc=com
# a template to search user by user login id
ldap.user.filter = (uid={0})
The most important settings here to understand are:
-
ldap.group.base
- the search base for the groups -
ldap.group.object.class
- if set, groups are created on ldap server from database groups -
ldap.group.filter
- a filter template to search groups by user id, the{0}
is replaced with user login name -
ldap.user.base
- the search base for the users -
ldap.user.filter
- a filter template to search user by user id, the{0}
is replaced with the user login name
When a user first log in using an LDAP account, corresponding User/Group objects are created in the database. These objects can be used to configure permissions.
The LDAP user password is never stored in the database, the authentication is done on the LDAP server only. Also note that, there is no synchronization done between database object and LDAP objects.
Single Sign-On
Common configuration for the different authentication mechanisms, to be added to application.properties
:
# Single sign-on common configuration
#
# callback URL for all indirect clients (defaults to application.baseUrl + "/callback")
auth.callback.url = http://localhost:8080/open-platform-demo/callback
# user provisioning: create / link / none
auth.user.provisioning = create
# default group for created users
auth.user.default.group = users
# logout URL
auth.logout.url =
# logout URL pattern
auth.logout.url.pattern =
# remove profiles from session
auth.logout.local = true
# call identity provider logout endpoint
auth.logout.central = false
OpenID Connect
Built-in clients
# OpenID Connect
# Google client
#
# Google client ID
auth.oidc.google.client.id = 127736102816-tc5mmsfaasa399jhqkfbv48nftoc55ft.apps.googleusercontent.com
# Google client secret
auth.oidc.google.secret = qySuozNl72zzM5SKW-0kczwV
# Azure Active Directory client
#
# Azure Active Directory client ID
auth.oidc.azuread.client.id = 53baf26b-526d-4f5c-e08a-dc207a808854
# Azure Active Directory client secret
auth.oidc.azuread.secret = NMubGVqkcDwwGs6fa01tBBqlkTisfUd4nCpYgcxxx=
# Azure Active Directory tenant ID
auth.oidc.azuread.tenant = 491caf37-da1b-774c-b91f-f428b77d5055
# Keycloak client
#
# Keycloak client ID
auth.oidc.keycloak.client.id =
# Keycloak client secret
auth.oidc.keycloak.secret =
# Keycloak authentication realm
auth.oidc.keycloak.realm =
# Keycloak server base URI
auth.oidc.keycloak.base.uri =
Custom clients
You can configure several custom OpenID Connect clients. Just replace generic
in the parameter names with your own unique client name.
# Generic OpenID Connect client
#
# name of the generic client (needs to be unique)
auth.oidc.generic.name = OidcClient
# client title
auth.oidc.generic.title = OpenID Connect
# client icon URL
auth.oidc.generic.icon =
# exclusive client (no form authentication) if no other client is specified
auth.oidc.generic.exclusive = false
# client ID
auth.oidc.generic.client.id =
# client secret
auth.oidc.generic.secret =
# discovery URI
auth.oidc.generic.discovery.uri =
#
# Additional configuration
#
# use the nonce parameter
auth.oidc.generic.use.nonce = false
# define flow's response_type
auth.oidc.generic.response.type = code
# define flow's response_mode
auth.oidc.generic.response.mode =
# define the scope
auth.oidc.generic.scope =
#
# Direct client
#
# header name
auth.oidc.generic.header.name =
# prefix header
auth.oidc.generic.prefix.header =
OAuth
Built-in clients
# OAuth
# Google client key
auth.oauth.google.key = 127736102816-tc5mmsfaasa399jhqkfbv48nftoc55ft.apps.googleusercontent.com
# Google client secret
auth.oauth.google.secret = qySuozNl72zzM5SKW-0kczwV
# Facebook client key
auth.oauth.facebook.key =
# Facebook client secret
auth.oauth.facebook.secret =
# Twitter client key
auth.oauth.twitter.key =
# Twitter client secret
auth.oauth.twitter.secret =
# Yahoo! client key
auth.oauth.yahoo.key =
# Yahoo! client secret
auth.oauth.yahoo.secret =
# LinkedIn client key
auth.oauth.linkedin.key =
# LinkedIn client secret
auth.oauth.linkedin.secret =
# Windows Live client key
auth.oauth.windowslive.key =
# Windows Live client secret
auth.oauth.windowslive.secret =
# WeChat client key
auth.oauth.wechat.key =
# WeChat client secret
auth.oauth.wechat.secret =
# GitHub client key
auth.oauth.github.key =
# GitHub client secret
auth.oauth.github.secret =
Custom clients
You can configure several custom OAuth 2.0 clients. Just replace generic
in the parameter names with your own unique client name.
# Generic OAuth 2.0 client
#
# name of the generic client (needs to be unique)
auth.oauth.generic.name = GenericOAuth20Client
# client title
auth.oauth.generic.title = OAuth 2.0
# client icon URL
auth.oauth.generic.icon =
# exclusive client (no form authentication) if no other client is specified
auth.oauth.generic.exclusive = false
# client key
auth.oauth.generic.key =
# client secret
auth.oauth.generic.secret =
# authentication URL
auth.oauth.generic.auth.url =
# token URL
auth.oauth.generic.token.url =
# profile attributes: list of comma-separated key:type|tag
# supported types: Integer, Boolean, Color, Gender, Locale, Long, URI, String (default)
auth.oauth.generic.profile.attrs = age:Integer|age,is_admin:Boolean|is_admin
SAML 2.0
# SAML 2.0
# Basic configuration
#
# path to keystore
auth.saml.keystore.path = path/to/samlKeystore.jks
# value of the -storepass option for the keystore
auth.saml.keystore.password = open-platform-demo-passwd
# value of the -keypass option
auth.saml.private.key.password = open-platform-demo-passwd
# path to IdP metadata
auth.saml.identity.provider.metadata.path = path/to/idp-metadata.xml
# Additional configuration
#
# accept assertions based on a previous authentication for one hour by default
auth.saml.maximum.authentication.lifetime = 3600
# custom SP entity ID
auth.saml.service.provider.entity.id = http://localhost:8080/open-platform-demo/callback?client_name=SAML2Client
# path to SP metadata
auth.saml.service.provider.metadata.path = path/to/sp-metadata.xml
# Advanced configuration
#
# forced authentication
auth.saml.force.auth = false
# passive authentication
auth.saml.passive = false
# binding type for the authentication request: SAML2_POST_BINDING_URI / SAML2_POST_SIMPLE_SIGN_BINDING_URI / SAML2_REDIRECT_BINDING_URI
auth.saml.authn.request.binding.type = SAML2_POST_BINDING_URI
# force a NameQualifier in the request
auth.saml.use.name.qualifier = false
# attribute consuming index
auth.saml.attribute.consuming.service.index = -1
# assertion consumer service index
auth.saml.assertion.consumer.service.index = -1
# list of blacklisted signature signing algorithms
auth.saml.blacklisted.signature.signing.algorithms =
# list of signature algorithms
auth.saml.signature.algorithms =
# list of signature reference digest methods
auth.saml.signature.reference.digest.methods =
# signature canonicalization algorithm
auth.saml.signature.canonicalization.algorithm =
# whether assertions must be signed
auth.saml.wants.assertions.signed = false
# enable signing of the authentication requests
auth.saml.authn.request.signed = false
CAS
# CAS
# Application configuration
#
# login URL of CAS server
auth.cas.login.url = https://localhost:8443/cas
# CAS prefix URL
auth.cas.prefix.url =
# CAS protocol: CAS10 / CAS20 / CAS20_PROXY / CAS30 / CAS30_PROXY / SAML
auth.cas.protocol = CAS30
# Various parameters
#
# encoding used for parsing the CAS responses
auth.cas.encoding = UTF-8
# whether the renew parameter will be used
auth.cas.renew = false
# whether the gateway parameter will be used
auth.cas.gateway = false
# time tolerance for the SAML ticket validation
auth.cas.time.tolerance = 1000
# class name for specific CallbackUrlResolver
auth.cas.url.resolver.class =
# class name for default TicketValidator
auth.cas.default.ticket.validator.class =
# use proxy support
auth.cas.proxy.support = false
# class name for specific LogoutHandler
auth.cas.logout.handler.class =
# client type: indirect / direct / direct-proxy / rest-form / rest-basic-auth
auth.cas.client.type = indirect
# direct-proxy client configuration
#
# service URL
auth.cas.service.url =
# rest-form client configuration
#
# username parameter
auth.cas.username.parameter =
# password parameter
auth.cas.password.parameter =
# rest-basic-auth client configuration
#
# header name
auth.cas.header.name =
# prefix header
auth.cas.prefix.header =