Authorization
Overview
Authorization, sometimes referred to as permissions or rights, is the concept of ensuring users can only access features and data from the application that they are authorized to access, for example ensure only permitted users may view and update (create, edit, delete) data in an application.
The Genesis Application Platform makes it simple to set up powerful authorization models based on business requirements.
There are three levels that can be configured:
- Feature access : Accessing a UI component (for example a grid, chart, form, ...) or server component (for example a query or event)
- Entity access : Accessing particular entities which make up the application data. For example users being permitted to submit events or read data for a particular counterparty, book, or some other application entity.
- Attribute access : Accessing specific fields / column values. For example restricting access to view or update a particular field in a table.
All levels can be configured in applications to ensure complete security.
This model enables full control of data within an application at the most granular level.
Typically levels 1 and 3 are controlled by permission codes, and level 2 is controlled by entity access, however you can set up the application as per your business requirements. The following sections give an overview of the 2 layers and example use-cases.
Beyond this page, there is a how to guide on authorization available with a project with code examples you can clone and run, which explains further the practical implementation of authorization.
Configuration options
Permission (Right) codes
Permission (right) codes need to be inserted into the RIGHT
table in the server. Each entry has:
- A
CODE
: The code which will be set in various components (server, client) - A
DESCRIPTION
: A description of the type of application access the code will enable for the user.
See assigning users permissions for details on assigning them to users.
Entity access
There are two methods for configuring entity access. The first is simple and quite a common use case, the second allows full configuration to meet more complex requirements. Both methods look to achieve the same thing, which is "is a given user authorized to access this entity"
A services called GENESIS_AUTH_PERMS
is responsible for building maps around this data, which are in turn accessed by any other components.
Simple configuration
A common requirement is to restrict access to rows of data based on a given entity contained within them. For example if we had a COUNTERPARTY
table, which represents a client, it is common in financial applications that only certain sets of users see rows of data relating to a given set of clients.
In this pattern, it is most common to have a simple USER_<ENTITY>_MAP
table, in this example USER_COUNTERPARTY_MAP
, in which presence of a USER_NAME
+ COUNTERPARTY
combination in a row means that user has access to that counterparty, and should be able to view and update data. If no such row exists, they should not be able to.
To set this up, you can configure the following system definition items in your application's genesis-system-definition.kts
.
systemDefinition {
...
global {
...
item(name = "ADMIN_PERMISSION_ENTITY_TABLE", value = "COUNTERPARTY")
item(name = "ADMIN_PERMISSION_ENTITY_FIELD", value = "COUNTERPARTY_ID")
...
}
...
}
In this example we use COUNTERPARTY
table and it's ID field COUNTERPARTY_ID
, however you can set any entity in your data model and it's common ID field as required.
Setting these items and installing your application will create the USER_COUNTERPARTY_MAP
table which can be populated. You can then set "ENTITY_VISIBILITY"
in auth
blocks to restrict entity access according to that table in any client facing components as required.
Full configuration
In addition to the simple entity mapping configuration, you can create your own entity mapName
values to use in auth
per the examples listed in that section.
To do this, we need to add configuration to a *-permissions.kts
file which should be located in your application's scripts
folder.
Examples
In this example, user authorization is based on an ACCOUNT
entity.
A table called TAG
which stores USER_NAME
and a PERSON_TYPE
in rows is read to see the type of user.
The user will only have access to an ACCOUNT
if:
- The user has a
PERSON_TYPE
ofSALES_OFFICE
and theACCOUNT
OFFICE_ID
is set to the user's username. - The user has a
PERSON_TYPE
ofASSET_MANAGER
and theACCOUNT
ASSET_MANAGER_ID
is set to the user's username.
If neither of those things are true for a given USER
+ ACCOUNT
, then the user cannot access the ACCOUNT
entity.
dynamicPermissions {
entity(ACCOUNT) {
maxEntries = 10000
batchingPeriod = 15
expression {
var allowed = false
val username = user.userName
val tagRec = entityDb.get(Tag.byCodeEntityId("PERSON_TYPE", username))
val entityType = tagRec?.tagValue
if (entityType == "SALES_OFFICE" && entity.officeId == username) {
allowed = true
}
if (entityType == "ASSET_MANAGER" && entity.assetManagerId == username) {
allowed = true
}
allowed
}
}
}
Further examples
dynamicPermissions {
entity(POSITION_VIEW) {
averageEntityChars = 15
averageUserNameChars = 10
averageUsers = 10000
maxEntries = 10000
batchingPeriod = 15
idField = idField = listOf(POSITION_VIEW.INSTRUMENT_ID) //Specifying custom field to be used as cache key instead of using the fields from the primary key
backwardsJoin = true
expression {
entity.companyId == user.companyId
}
}
entity(TRADE) {
name = "INNER_TRADES" //Specifying custom entity name instead of using the name of the TRADE_TABLE
averageEntityChars = 150
averageUserNameChars = 100
averageUsers = 100000
maxEntries = 100000
batchingPeriod = 150
expression {
entity.allowedTraders.contains(user.userName) // User's name is in a list of allowedTraders defined on the TRADE
}
}
}
entity
The entity
block will need an entity, which can be a table or a view from your application's data model.
An entity can have the following attributes:
Name | Description | Mandatory |
---|---|---|
name | The name of the entity you want to authorize, which you will refer to in auth blocks | No. Default value is the name of the Table or the View |
maxEntries | The maximum number of entries that the authorization map will contain. | Yes |
batchingPeriod | Period in seconds to batch records before processing | Yes |
idField | Field(s) to use for keying internal collection (should be unique) | No. Default value is the primary key for the entity |
averageUserNameChars | Average number of characters in each username. This setting defines backing data structure for the authorization map. | No. Default = 7 |
averageEntityChars | Average number of characters of each entity. This setting defines the backing data structure for the authorization map. | No. Default = 20 |
averageUsers | Average number of users on the system. This setting defines the backing data structure for the authorization map. | No. Default = 1,000 |
expression | Function that calculates whether an entity can be accessed by a user. The result is either true (user has access) or false (user has no access) | Yes |
Each expression
has the following properties and should return either true
or false
based on whether the user
can access the entity
:
Name | Description |
---|---|
entityDb | Read only access to the database if additional data query is required. Note: The expression is called on each data update, and querying the database each time will result in performance hit. A better approach would be to define a view that joins all the necessary tables |
entity | The entity to be evaluated for access |
user | The user to be evaluated for access to the entity |
entityId | The value of the idField for this `entity |
Component configuration options
Server component configuration
The permissioning
block allows application developers to set all the described levels of authorization, an overview of setting them up is included below:
permissioning
The permissioning
block is used to implement access control measures on the data being returned to a user.
With the exception of the inner auth
block which relies on inbound data, the permissioning
block and its contents can also be placed in the outer block and apply to all query
/requestServer
/eventHandler
blocks named in the file.
permissionCodes
permissionCodes
takes a list of permission codes. The client user accessing the query must have access to at least one of the permission codes in the list to be able to access any query data.
permissioning {
permissionCodes = listOf("TradeView", "TradeUpdate")
}
customPermissions
The customPermissions
block takes boolean logic. If this function returns true, the user will be able to access the resource; otherwise, the request will be rejected.
It is useful, for example, in the case you have an external API to check against. In this example an entitlementUtils
object wraps an external API we can call to check the user's name has access.
customPermissions { message ->
entitlementUtils.userIsEntitled(message.userName)
}
The customPermissions
block also has access to entityDb
so that you can query any data in the database. You can add complex logic in here as needed, and have access to the full inbound message.
customPermissions { message ->
val userAttributes = entityDb.get(UserAttributes.byUserName(message.userName))
userAttributes?.accessType == AccessType.ALL
}
Where utilizing customPermissions
you should also consider configuring a customLoginAck
so that the front end of the application can also permission its components in a similar way.
auth
auth
is used to restrict rows (queries) or events with a particular entity based on a user's entity access, also known as row-level permissions.
permissioning {
auth(mapName = "ENTITY_VISIBILITY") {
authKey {
key(data.counterpartyId)
}
}
Details on restricting query entity access here
Auth definitions can be grouped with “and” or “or” operators.
- You could have two simple permission maps, for example: one by counterparty and another one for forbidden symbols. If the user wants to see a specific row, they need to have both permissions at once.
- You could have two permission maps: one for buyer and one for seller. A user would be allowed to see a row if they have a seller or buyer profile, but users without one of the those profiles would be denied access.
This example shows an AND grouping:
permissioning {
auth(mapName = "ENTITY_VISIBILITY") {
authKey {
key(data.counterpartyId)
}
} and auth(mapName = "SYMBOL_RESTRICTED") {
authKey {
key(data.symbol)
}
}
}
This example shows OR grouping:
permissioning {
auth(mapName = "ENTITY_VISIBILITY") {
authKey {
key(data.buyerId)
}
} or auth(mapName = "ENTITY_VISIBILITY") {
authKey {
key(data.sellerId)
}
}
}
userHasRight
userHasRight
is a helpful function which can be called within the auth block, or anywhere else in client facing server components which have a user accessing them, to determine if a user has a given right.
if (!userHasRight(userName, "TradeViewFull")) { ... }
hideFields
For queries, the auth
block also allows a hideFields
which can be used to restrict attribute/field/column values being returned to users.
For example, you can hide a column (or set of columns) based on boolean logic, for example if a user does not have a specific Right Code in their profile.
In the example below, the code uses auth
and hideFields
to check if the user has the Right Code TradeViewFull
. If this code is not set for the user, then the column CUSTOMER_NAME
will not be returned to the user:
permissioning {
auth {
hideFields { userName, rowData ->
if (!userHasRight(userName, "TradeViewFull")) listOf(CUSTOMER_NAME)
else emptyList()
}
}
}
RightSummaryCache
GPAL server components have the permissionCodes
block mentioned above for convenience. If you are writing custom code, or event need more complex boolean logic to determine access within a GPAL server component, the RightSummaryCache
is injectable into any java or kotlin code and can be used for efficient lookups base on user and permission codes.
See the example below where we first want to check a trade is not on a restricted stock, and where it is not ensure the user has a TRADE_INSERT
permission.
- Kotlin
- Java
import global.genesis.session.RightSummaryCache
...
val rightSummaryCache = inject<RightSummaryCache>()
...
if (stockInRestrictedList != null) {
false
} else {
rightSummaryCache.userHasRight(userName, "TRADE_INSERT")
}
import global.genesis.session.RightSummaryCache
...
private final RightSummaryCache rightSummaryCache;
...
@Inject
public EventTrade(RightSummaryCache rightSummaryCache) {
this.rightSummaryCache = rightSummaryCache;
}
...
if (stockInRestrictedList != null) {
return false
} else {
return rightSummaryCache.userHasRight(userName, "TRADE_INSERT")
}
AuthCacheFactory
GPAL server components have the auth
block mentioned above for convenience. If you are writing custom code, or event need more complex boolean logic to determine entity access within a GPAL server component, the AuthCacheFactory
is injectable into any java or kotlin code and can be used for efficient lookups base on user and an entity
.
See the example below for creating a ReadOnlyAuthCache
from the AuthCacheFactory
onto a particular permissioned entity
, and example boolean logic to see if the user has access
- Kotlin
- Java
import global.genesis.session.AuthCacheFactory
...
val authFactory = inject<AuthCacheFactory>()
val myMap = authFactory.newReader("ENTITY_VISIBILITY")
...
myMap.isAuthorised(companyId, userName)
import global.genesis.session.AuthCacheFactory
private final ReadOnlyAuthCache authCacheMap;
...
@Inject
public EventTrade(AuthCacheFactory authFactory) {
this.authCacheMap = authFactory.newReader("ENTITY_VISIBILITY");
}
...
myMap.isAuthorised(companyId, userName)
Client component configuration
You can apply authorization at the front end to remove grids and buttons where there is no appropriate permission. If you don't want to do this, the system is still protected by the permissioning you have set on the back end. Users will receive error messages explaining their lack of permissions.
Here is an example of restricting tab visibility (based on the Right Code TradeView that provides access to the tab/route):
navItems: [
{
title: 'Trades',
permission: 'TradeView',
},
],
Here is an example of restricting button visibility (based on the Right Code TradeUpdate, which provides access to the button):
createEvent="${(x) => getViewUpdateRightComponent(x.user, 'TradeUpdate', 'EVENT_TRADE_INSERT')}"
See the relevant client capabilities pages for more details on how to secure them with permissions.
Assigning users permissions
Most applications will include the User management component for easily assigning rights and users to profiles. Users gain the superset of permissions that are assigned to the profiles they are a part of.
The following section provides details as to how user permissions assignment works, and the table records which make it work.
How it works
Genesis has the concept of users, profiles and right codes. For each one, there is a table to store the related entity data:
USER
PROFILE
RIGHT
Users gain rights via profiles. So we have tables to determine which users and rights belong to each given profile. Note that you cannot allocate right codes directly to a specific user. However, a user can have multiple profiles.
A profile can have zero or more rights and zero or more users.
These relationships are held in the following tables:
PROFILE_RIGHT
PROFILE_USER
Related to these tables, we have theRIGHT_SUMMARY
table, which contains the superset of rights any given user has. These are based on the profiles assigned to them. This is the key table used when checking rights, and it exists to allow the efficient checking of a user's rights.
Using GENESIS_AUTH_MANAGER
You can use the GENESIS_AUTH_MANAGER
process to add users and maintain their rights. As long as you use this process, then the entries in the RIGHT_SUMMARY
table are maintained automatically by the system in real time, so the rights are easily accessible at speed.
For example, if you add a new user or you update a profile with new rights, the RIGHT_SUMMARY
table is updated immediately and all the users in that profile receive the new right automatically.
Updating directly in the database
Note that you can also maintain the following tables manually using DbMon
or SendIt
:
USER
PROFILE
RIGHT
PROFILE_USER
PROFILE_RIGHT
This is an easy way to bulk-load permissions data in a brand new environment, for example.
However, when you change any of the permissions tables in this way, the RIGHT_SUMMARY
table will not be maintained automatically. To update the the RIGHT_SUMMARY
so that the changes take effect, run ConsolidateRights
.
If you update any of the permission tables manually, the changes won't take effect until you run ConsolidateRights
.
Sample explanation
See the following simple system set-up. We have a set of entities (our user, rights and profiles), a set of profile mappings (to users and rights) and, finally, the resultant set of right entries we would see in RIGHT_SUMMARY
:
The above image shows:
- 3 profiles, each with particular rights assigned
- 4 users, three of which have one profile assigned and one of which, Jenny.Super, is assigned to have all rights.
Another way of achieving this set-up would be to have a fourth profile, say SUPER, as per below, and to have all rights assigned to it, and Jenny.Super assigned just to the one profile:
Note how we now have an extra profile, and edits to the PROFILE_USER
and PROFILE_RIGHT
entries, but the resulting rights are the same.
As you can tell, this enables you to build powerful combinations, and since Users, Profiles, Profile_Users and Profile_Rights are all editable by system administrators, they can build their own set-up that makes sense for their organization.
Good practice
Having profiles as an intermediary between users and rights enables admin users of the system to create complex permission models with no code change. Rights codes generally need to be added to the code. Although this is simple to do, it requires a code change. Our advice is to design applications with enough granularity in the rights to ensure that code changes aren't required.