Source

tools/hooks/useIdentityCheck.ts

import {useOidc} from "./useOidc";
import {logger} from "../../services";

/**
 * @typedef {object} IdentityCheckReturn
 */
type IdentityCheckReturn = {
    /**
     * All of the provided values match / are present in the attribute
     */
    all: boolean,
    /**
     * At least one of the provided values match / is present in the attribute
     */
    any: boolean,
    /**
     * The supplied attribute is present in the user object (e.g. optional attributes like groups)
     */
    exists: boolean
}

/**
 * Custom hook for checking the current OIDC user's identity attributes.
 *
 * This hook tells you about identity attributes in the current user object.
 *
 * You can supply either strings or regex objects to the lookup values of the hook. Strings will be matched completely
 * (i.e. `x == y`) and regexes will use the built-in evaluation (i.e. `regex.test(value)`).
 *
 * The hook returns three booleans:  `exists`, `any`, and `all`
 *  - `exists`: The supplied attribute is present in the user object (e.g. optional attributes like groups)
 *  - `any`: At least one of the provided values match / is present in the attribute
 *  - `all`: All of the provided values match / are present in the attribute
 *
 * ### Hook Usage Examples
 * #### Checking if the user has a specific `groups` value (e.g. orgu-1440)
 *
 * When a single value argument is supplied, the results for `any` and `all` will be the same.
 *
 * ```
 * const {any} = useIdentityCheck('groups', /orgu-1440/)
 * ```
 *
 * <br/>
 *
 * #### Checking if the user is in one or more of a set of `groups` <span id="use-identity-check-example-anchor"/>
 *
 * Multiple values can be provided to the hook to be evaluated collectively. In this case, `any` will be true if any of
 * the supplied values match / are present in the supplied attribute and `all` will only be true if **all** the supplied
 * values match / are present in the supplied attribute.
 *
 * See [below](#use-identity-check-multiple-note) for gotchas about regexes and searching for multiple values.
 *
 * ```
 * const {all, any} = useIdentityCheck('groups', /144040/, /144060/)
 * ```
 * <br/>
 *
 * #### Checking if the users first name is 'john' or contains a 'y'
 *
 * This example is not likely to be a common use case but demonstrates the ability to check any piece of identity
 * information.
 *
 * ```
 * const {any} = useIdentityCheck('given_name', 'john', /y/)
 * ```
 *
 * <br/>
 *
 * #### Regex matching
 *
 * Because the matching portion of the hook evaluates supplied regexes using the built regex engine, you can provide
 * "complex" regexes to match more specific patterns.
 *
 * For the sake of demonstration, lets assume scrum masters have a specific orgu role assigned to their identity (e.g. orgu-144040-ScrumMaster)
 * in addition to their standard staff orgu role (e.g. orgu-144040-Staff)
 *
 * We can use a regex pattern to only allow access for someone who is a scrum master in orgu 144040 OR 144060.
 *
 * ```
 * const {all, any} = useIdentityCheck('groups', /1440(4|6).*scrum/i)
 * ```
 *
 * <span id="use-identity-check-multiple-note">Note:</span> The logic above can be confusing due to the regex having multiple "clauses". The `any` and `all` values of the
 * hook relate to how many of the provided values match, NOT whether the supplied value matches multiple attribute values.
 *
 * In the above example, the `all` value will be true if a user is in 144040 OR 144060 because the supplied value matches
 * an attribute value. If you want to check multiple conditions, they should be supplied as two separate argument values.
 * See [this example](#use-identity-check-example-anchor)
 *
 * @category Hooks
 *
 * @param attribute {string} Which identity attribute to check
 * @param values {string | RegExp} Variable number of values to look for in `attribute`
 *
 * @return {IdentityCheckReturn}
 */
export function useIdentityCheck(attribute: string, ...values: Array<string | RegExp>): IdentityCheckReturn {

    // Extract the user profile from the oidc user object
    const {user} = useOidc()

    //TODO: How defensive should we be?

    // Missing attribute or no present user
    if (!attribute || !user) {
        return {all: false, any: false, exists: false}
    }

    let {profile} = user

    // Check if attribute is present in the identity
    let exists = profile.hasOwnProperty(attribute)

    // If only attribute is provided or the attribute doesn't exist, finish here
    if (values.length < 1 || !exists) {
        return {all: exists, any: exists, exists}
    }

    let all = true, any = false
    for (let value of values) {
        // Check if the value is an invalid type, if so log dev error and return false for everything
        if (!(typeof value === 'string' || value instanceof String || value instanceof RegExp)) {
            logger.devError("Values for useIdentityCheck must be strings or RegExp")
            return {all: false, any: false, exists: false}
        }

        let attrValue = profile[attribute]
        let valuePresent = false

        if (Array.isArray(attrValue)) {
            // If attribute is an array, check if the requested value is present in one of the entries
            if (value instanceof RegExp) {
                // If the requested value is a regex, match it against
                //@ts-ignore
                valuePresent = attrValue.some((v) => value.test(v))
            } else {
                // Otherwise use equality check
                valuePresent = attrValue.some((v) => value == v)
            }
        } else {
            // If attribute is not an array, check the value directly
            if (value instanceof RegExp) {
                // If the requested value is a regex, match it against
                valuePresent = value.test(attrValue)
            } else {
                // Otherwise use equality check
                valuePresent = value == attrValue
            }
        }

        // Add the current values logic to the existing values
        all = all && valuePresent
        any = any || valuePresent
    }

    return {all, any, exists}
}