1. Customization
  2. Access Restrictions

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:

toml
[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.

js
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:

js
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.

js
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:

js
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:

js
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.

I want the permissions query to only return permissions of the type userappRequestDefinition

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:

graphql
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:

js
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 PRDs. 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.

js
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:

graphql
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.