Source

tools/hooks/useIdentityCheck.ts

  1. import {useOidc} from "./useOidc";
  2. import {logger} from "../../services";
  3. /**
  4. * @typedef {object} IdentityCheckReturn
  5. */
  6. type IdentityCheckReturn = {
  7. /**
  8. * All of the provided values match / are present in the attribute
  9. */
  10. all: boolean,
  11. /**
  12. * At least one of the provided values match / is present in the attribute
  13. */
  14. any: boolean,
  15. /**
  16. * The supplied attribute is present in the user object (e.g. optional attributes like groups)
  17. */
  18. exists: boolean
  19. }
  20. /**
  21. * Custom hook for checking the current OIDC user's identity attributes.
  22. *
  23. * This hook tells you about identity attributes in the current user object.
  24. *
  25. * You can supply either strings or regex objects to the lookup values of the hook. Strings will be matched completely
  26. * (i.e. `x == y`) and regexes will use the built-in evaluation (i.e. `regex.test(value)`).
  27. *
  28. * The hook returns three booleans: `exists`, `any`, and `all`
  29. * - `exists`: The supplied attribute is present in the user object (e.g. optional attributes like groups)
  30. * - `any`: At least one of the provided values match / is present in the attribute
  31. * - `all`: All of the provided values match / are present in the attribute
  32. *
  33. * ### Hook Usage Examples
  34. * #### Checking if the user has a specific `groups` value (e.g. orgu-1440)
  35. *
  36. * When a single value argument is supplied, the results for `any` and `all` will be the same.
  37. *
  38. * ```
  39. * const {any} = useIdentityCheck('groups', /orgu-1440/)
  40. * ```
  41. *
  42. * <br/>
  43. *
  44. * #### Checking if the user is in one or more of a set of `groups` <span id="use-identity-check-example-anchor"/>
  45. *
  46. * Multiple values can be provided to the hook to be evaluated collectively. In this case, `any` will be true if any of
  47. * the supplied values match / are present in the supplied attribute and `all` will only be true if **all** the supplied
  48. * values match / are present in the supplied attribute.
  49. *
  50. * See [below](#use-identity-check-multiple-note) for gotchas about regexes and searching for multiple values.
  51. *
  52. * ```
  53. * const {all, any} = useIdentityCheck('groups', /144040/, /144060/)
  54. * ```
  55. * <br/>
  56. *
  57. * #### Checking if the users first name is 'john' or contains a 'y'
  58. *
  59. * This example is not likely to be a common use case but demonstrates the ability to check any piece of identity
  60. * information.
  61. *
  62. * ```
  63. * const {any} = useIdentityCheck('given_name', 'john', /y/)
  64. * ```
  65. *
  66. * <br/>
  67. *
  68. * #### Regex matching
  69. *
  70. * Because the matching portion of the hook evaluates supplied regexes using the built regex engine, you can provide
  71. * "complex" regexes to match more specific patterns.
  72. *
  73. * For the sake of demonstration, lets assume scrum masters have a specific orgu role assigned to their identity (e.g. orgu-144040-ScrumMaster)
  74. * in addition to their standard staff orgu role (e.g. orgu-144040-Staff)
  75. *
  76. * We can use a regex pattern to only allow access for someone who is a scrum master in orgu 144040 OR 144060.
  77. *
  78. * ```
  79. * const {all, any} = useIdentityCheck('groups', /1440(4|6).*scrum/i)
  80. * ```
  81. *
  82. * <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
  83. * hook relate to how many of the provided values match, NOT whether the supplied value matches multiple attribute values.
  84. *
  85. * In the above example, the `all` value will be true if a user is in 144040 OR 144060 because the supplied value matches
  86. * an attribute value. If you want to check multiple conditions, they should be supplied as two separate argument values.
  87. * See [this example](#use-identity-check-example-anchor)
  88. *
  89. * @category Hooks
  90. *
  91. * @param attribute {string} Which identity attribute to check
  92. * @param values {string | RegExp} Variable number of values to look for in `attribute`
  93. *
  94. * @return {IdentityCheckReturn}
  95. */
  96. export function useIdentityCheck(attribute: string, ...values: Array<string | RegExp>): IdentityCheckReturn {
  97. // Extract the user profile from the oidc user object
  98. const {user} = useOidc()
  99. //TODO: How defensive should we be?
  100. // Missing attribute or no present user
  101. if (!attribute || !user) {
  102. return {all: false, any: false, exists: false}
  103. }
  104. let {profile} = user
  105. // Check if attribute is present in the identity
  106. let exists = profile.hasOwnProperty(attribute)
  107. // If only attribute is provided or the attribute doesn't exist, finish here
  108. if (values.length < 1 || !exists) {
  109. return {all: exists, any: exists, exists}
  110. }
  111. let all = true, any = false
  112. for (let value of values) {
  113. // Check if the value is an invalid type, if so log dev error and return false for everything
  114. if (!(typeof value === 'string' || value instanceof String || value instanceof RegExp)) {
  115. logger.devError("Values for useIdentityCheck must be strings or RegExp")
  116. return {all: false, any: false, exists: false}
  117. }
  118. let attrValue = profile[attribute]
  119. let valuePresent = false
  120. if (Array.isArray(attrValue)) {
  121. // If attribute is an array, check if the requested value is present in one of the entries
  122. if (value instanceof RegExp) {
  123. // If the requested value is a regex, match it against
  124. //@ts-ignore
  125. valuePresent = attrValue.some((v) => value.test(v))
  126. } else {
  127. // Otherwise use equality check
  128. valuePresent = attrValue.some((v) => value == v)
  129. }
  130. } else {
  131. // If attribute is not an array, check the value directly
  132. if (value instanceof RegExp) {
  133. // If the requested value is a regex, match it against
  134. valuePresent = value.test(attrValue)
  135. } else {
  136. // Otherwise use equality check
  137. valuePresent = value == attrValue
  138. }
  139. }
  140. // Add the current values logic to the existing values
  141. all = all && valuePresent
  142. any = any || valuePresent
  143. }
  144. return {all, any, exists}
  145. }