Customization
Access Restrictions
This document shows how you can easily setup an additional security layer by defining access rules for queries or fields
Custom Field and Access Rules
With another customization file, it is possible to restrict the access of certain type fields for certain users. To make use of that, first configure a rules file in your configuration toml file:
[rules]
file="./rules.js"
This will look up your rules file in the given path relative to the config directory.
The IdentityHub rules are written in javascript. The following use cases show how to restrict access to certain fields by exporting the access
object.
Only a small group of developers should be able to use the introspection query
This may be a very common use case to prevent people from gaining too much insight into our system by exploring the schema. You can protect yourself from that with a rule like the following.
import { defineAccess, viewer } from '@carv/rules'
const isDeveloper = viewer.hasGroup({ dn: 'cn=developers,ou=groups,o=data' })
export const access = defineAccess({
'Query.__schema': isDeveloper,
})
At first we check if the logged in user is member of a certain group. We can do this by using the exported viewer
from @carv/rules
. By exporting the access
const and creating it with the defineAccess
function, we take care, that everytime someone requests Query.__schema
(the InterospectionQuery), the isDeveloper
check is performed first. The IdentityHub will return null for the restricted field if you do not meet the requirements.
Everyone can lookup users, but certain properties should only be viewable for managers
Maybe your customAttribute should only be seen by your admins or helpdesk users. No problem, since you can perform queries in your rules, we can achieve that with access rules as well:
export const access = defineAccess({
async 'User.customAttribute'({ info, query, token }) {
const { user } = await query(
gql`
query($userDN: String!, $managerDN: String!) {
user(by: { entryDN: $userDN, manager: $managerDN }) {
id
}
}
`,
{ userDN: info.rootValue.dn, managerDN: token.name },
)
return user != null
}
})
As you can see, we can perform some asynchronous actions. At first we use the info
, query
and token
arguments to perform a graphql query and try to find the user with the DN that we can extract from the root object (info.rootValue.dn
) and combine it with a filter that checks if the Viewer
is registered as a manager of that certain user. Only if we find that user and therfore know that the viewer is the manager of that user, we return true
and can resolve the customAttribute
.
Do I have to write a rule for each and every field?
No. You may use shortcuts here. Let's take the example from before, but this time make sure, that every field except some are protected in the same way.
export const access = defineAccess({
async 'User.*'({ info, query, token }) {
// non-nullable fields like id, dn, label, are always accessible
// allow access to nullable fields to can be view by everybody
if (['description', 'email'].includes(info.fieldName)) {
return true
}
const { user } = await query(
gql`
query($userDN: String!, $managerDN: String!) {
user(by: { entryDN: $userDN, manager: $managerDN }) {
id
}
}
`,
{ userDN: info.rootValue.dn, managerDN: token.name },
)
return user != null
}
})
The .*
takes care that our access rule is performed for every field of the User
type.
The logic is the same, but this time we can abort our check early if someone requires the description
or email
field, since we do not want to protect them.
What helpers are available?
To help you organize your access rules, the IdentityHub provides some useful functions like for example the query
or viewer
exports from @carv/rules
. There are some more, which are presented at a glance below. If you need help or want more information, please contact us.
Logical operators
To reuse parts of your logic or to organize, you may use the logical operator helpers and
, or
and not
that can also be imported from @carv/rules
. Use them as follows:
import { defineAccess, viewer, and, or, not } from '@carv/rules'
const isAdmin = viewer.hasGroup({ dn: 'cn=admin,ou=groups,o=data' })
const isHelpdesk = viewer.hasGroup({ dn: 'cn=helpdesk,ou=groups,o=data' })
const isExternal = viewer.hasGroup({ dn: 'cn=external,ou=groups,o=data' })
const canViewRoles = and(or(isAdmin, isHelpdesk), not(isExternal))
export const access = defineAccess({
'Role.*': canViewRoles,
})
This rule takes care that only people that are members of either the admin or the helpdesk group, but not the external group may request fields of a Role
.
allow and deny
Another shortcut are the functions allow
and deny
. If there is now special logic depending on the logged in user and you want to deny or allow the access to certain fields, you can use them:
import { defineAccess, allow, deny } from '@carv/rules'
export const access = defineAccess({
'Role.name': deny,
'Group.*': allow
})
Here we denied the access to the name property of a Role
for everyone, while allowing it to all fields of the User
type.
permissions
query to only return permissions of the type userappRequestDefinition
I want the Changing the behaviour of a query can be achieved by altering the arguments passed to it with the defineFieldArgs
helper.
Usually the permissions query can be used with a type filter:
query SearchViewerPermissions {
viewer {
permissions(where: { type: [role, userappResource, userappRequestDefinition] }) {
nodes {
id
}
}
}
}
With that query anyone can retrieve all his permissions including the types role
, userappResource
and userappRequestDefinition
. Maybe you don't want your users to see the role
permissions because anyone could request roles for himself. Instead you only want them to see the userappRequestDefinitions
, so they have to start a workflow. We can achieve that with the following code:
import { defineFieldArgs } from '@carv/rules'
export const fieldArgs = defineFieldArgs({
'Viewer.permissions'(args) {
return {
...args,
where: {
...args.where,
type: ['PRD'],
},
}
},
'Mutation.requestPermission'(args, { decodeGlobalId }) {
const [, type] = decodeGlobalId('UserappPermission', args.input.id)
if (type !== 'prd') {
throw new Error('Requesting permissions other than `prd` workflows is not allowed.')
}
},
'Mutation.requestPermissions'(args, { decodeGlobalId }) {
const [, type] = decodeGlobalId('UserappPermission', args.input.id)
if (type !== 'prd') {
throw new Error('Requesting permissions other than `prd` workflows is not allowed.')
}
},
})
In the first part, (Viewer.permissions
) query, we override the type
of the where
argument and ensure nobody can retireve any other permissions than PRD
s. For the mutation we ensured, that even if someone knows the id of a role
permission, he cannot request it.
Is it possible to check users permissions in my client application?
Yes. For example if you want to check if a user of your application is an administrator and then render additional content in your app, you can define rules for that.
import { defineAccess, viewer, and, or, not } from '@carv/rules'
const isAdmin = viewer.hasGroup({ dn: 'cn=admin,ou=groups,o=data' })
const isManager = viewer.hasGroup({ dn: 'cn=manager,ou=groups,o=data' })
const isUser = allow
export const access = defineAccess({
'level:admin': isAdmin,
'level:manager': or(isAdmin, isManager),
'level:user': or(isUser, isAdmin, isManager),
})
Here we defined three different levels, representing that the users of your app can be either normal users, managers or administrators. In your client application you now can send the following query:
query GetViewerAccessLevels {
viewer {
isUser: has(access: "level:user")
isManager: has(access: "level:manager")
isAdmin: has(access: "level:admin")
}
}
With the help of that query you can now always distinguish your users and their abilities from another and react accordingly.