Skip to content

Commit

Permalink
Create FetchUserRolesAndGroupsService (#392)
Browse files Browse the repository at this point in the history
https://eaflood.atlassian.net/browse/WATER-4085

We create a service `FetchUserRolesAndGroupsService` to look up a user in the `idm` schema and return the roles and groups assigned to them.

Roles are assigned as follows:
- Users can have roles assigned to them in the `user_roles` table
- Users can have groups assigned to them in the `user_groups` table
  - Groups have one or more roles assigned to them in the `group_roles` table

We want to collate all roles, no matter how they're assigned, and return the resulting list, along with the groups the user belongs to.
  • Loading branch information
StuAA78 authored Sep 4, 2023
1 parent 7af67fd commit 6698bb2
Show file tree
Hide file tree
Showing 15 changed files with 1,008 additions and 21 deletions.
23 changes: 23 additions & 0 deletions app/models/idm/group-role.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @module GroupRoleModel
*/

const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

class GroupRoleModel extends IDMBaseModel {
Expand All @@ -22,6 +24,27 @@ class GroupRoleModel extends IDMBaseModel {
{ database: 'dateUpdated', model: 'updatedAt' }
]
}

static get relationMappings () {
return {
role: {
relation: Model.BelongsToOneRelation,
modelClass: 'role.model',
join: {
from: 'groupRoles.roleId',
to: 'roles.roleId'
}
},
group: {
relation: Model.BelongsToOneRelation,
modelClass: 'group.model',
join: {
from: 'groupRoles.groupId',
to: 'groups.groupId'
}
}
}
}
}

module.exports = GroupRoleModel
47 changes: 47 additions & 0 deletions app/models/idm/group.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @module GroupModel
*/

const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

class GroupModel extends IDMBaseModel {
Expand All @@ -22,6 +24,51 @@ class GroupModel extends IDMBaseModel {
{ database: 'dateUpdated', model: 'updatedAt' }
]
}

static get relationMappings () {
return {
groupRoles: {
relation: Model.HasManyRelation,
modelClass: 'group-role.model',
join: {
from: 'groups.groupId',
to: 'groupRoles.groupId'
}
},
roles: {
relation: Model.ManyToManyRelation,
modelClass: 'role.model',
join: {
from: 'groups.groupId',
through: {
from: 'groupRoles.groupId',
to: 'groupRoles.roleId'
},
to: 'roles.roleId'
}
},
userGroups: {
relation: Model.HasManyRelation,
modelClass: 'user-group.model',
join: {
from: 'groups.groupId',
to: 'userGroups.groupId'
}
},
users: {
relation: Model.ManyToManyRelation,
modelClass: 'user.model',
join: {
from: 'groups.groupId',
through: {
from: 'userGroups.groupId',
to: 'userGroups.userId'
},
to: 'users.userId'
}
}
}
}
}

module.exports = GroupModel
47 changes: 47 additions & 0 deletions app/models/idm/role.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @module RoleModel
*/

const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

class RoleModel extends IDMBaseModel {
Expand All @@ -22,6 +24,51 @@ class RoleModel extends IDMBaseModel {
{ database: 'dateUpdated', model: 'updatedAt' }
]
}

static get relationMappings () {
return {
groupRoles: {
relation: Model.HasManyRelation,
modelClass: 'group-role.model',
join: {
from: 'roles.roleId',
to: 'groupRoles.roleId'
}
},
userRoles: {
relation: Model.HasManyRelation,
modelClass: 'user-role.model',
join: {
from: 'roles.roleId',
to: 'userRoles.roleId'
}
},
groups: {
relation: Model.ManyToManyRelation,
modelClass: 'group.model',
join: {
from: 'roles.roleId',
through: {
from: 'groupRoles.roleId',
to: 'groupRoles.groupId'
},
to: 'groups.groupId'
}
},
users: {
relation: Model.ManyToManyRelation,
modelClass: 'user.model',
join: {
from: 'roles.roleId',
through: {
from: 'userRoles.roleId',
to: 'userRoles.userId'
},
to: 'users.userId'
}
}
}
}
}

module.exports = RoleModel
23 changes: 23 additions & 0 deletions app/models/idm/user-group.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @module UserGroupModel
*/

const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

class UserGroupModel extends IDMBaseModel {
Expand All @@ -22,6 +24,27 @@ class UserGroupModel extends IDMBaseModel {
{ database: 'dateUpdated', model: 'updatedAt' }
]
}

static get relationMappings () {
return {
group: {
relation: Model.BelongsToOneRelation,
modelClass: 'group.model',
join: {
from: 'userGroups.groupId',
to: 'groups.groupId'
}
},
user: {
relation: Model.BelongsToOneRelation,
modelClass: 'user.model',
join: {
from: 'userGroups.userId',
to: 'users.userId'
}
}
}
}
}

module.exports = UserGroupModel
23 changes: 23 additions & 0 deletions app/models/idm/user-role.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @module UserRoleModel
*/

const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

class UserRoleModel extends IDMBaseModel {
Expand All @@ -22,6 +24,27 @@ class UserRoleModel extends IDMBaseModel {
{ database: 'dateUpdated', model: 'updatedAt' }
]
}

static get relationMappings () {
return {
role: {
relation: Model.BelongsToOneRelation,
modelClass: 'role.model',
join: {
from: 'userRoles.roleId',
to: 'roles.roleId'
}
},
user: {
relation: Model.BelongsToOneRelation,
modelClass: 'user.model',
join: {
from: 'userRoles.userId',
to: 'users.userId'
}
}
}
}
}

module.exports = UserRoleModel
48 changes: 47 additions & 1 deletion app/models/idm/user.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

const { hashSync } = require('bcryptjs')
const { Model } = require('objection')

const IDMBaseModel = require('./idm-base.model.js')

Expand All @@ -28,11 +29,56 @@ class UserModel extends IDMBaseModel {
// Defining which fields contain json allows us to insert an object without needing to stringify it first
static get jsonAttributes () {
return [
'user_data',
'userData',
'role'
]
}

static get relationMappings () {
return {
userGroups: {
relation: Model.HasManyRelation,
modelClass: 'user-group.model',
join: {
from: 'users.userId',
to: 'userGroups.userId'
}
},
userRoles: {
relation: Model.HasManyRelation,
modelClass: 'user-role.model',
join: {
from: 'users.userId',
to: 'userRoles.userId'
}
},
roles: {
relation: Model.ManyToManyRelation,
modelClass: 'role.model',
join: {
from: 'users.userId',
through: {
from: 'userRoles.userId',
to: 'userRoles.roleId'
},
to: 'roles.roleId'
}
},
groups: {
relation: Model.ManyToManyRelation,
modelClass: 'group.model',
join: {
from: 'users.userId',
through: {
from: 'userGroups.userId',
to: 'userGroups.groupId'
},
to: 'groups.groupId'
}
}
}
}

static generateHashedPassword (password) {
// 10 is the number of salt rounds to perform to generate the salt. The legacy code uses
// const salt = bcrypt.genSaltSync(10) to pre-generate the salt before passing it to hashSync(). But this is
Expand Down
3 changes: 2 additions & 1 deletion app/models/legacy-base.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class LegacyBaseModel extends BaseModel {
currentPath,
path.join(currentPath, 'returns'),
path.join(currentPath, 'water'),
path.join(currentPath, 'crm-v2')
path.join(currentPath, 'crm-v2'),
path.join(currentPath, 'idm')
]
}

Expand Down
85 changes: 85 additions & 0 deletions app/services/idm/fetch-user-roles-and-groups.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict'

/**
* Looks up a user in the `idm` schema and returns the roles and groups assigned to them
* @module FetchUserRolesAndGroupsService
*/

const UserModel = require('../../models/idm/user.model.js')

/**
* A user can have roles assigned to them in two ways:
*
* - By directly assigning roles to the user;
* - By adding the user to a group, and assigning roles to the group.
*
* This service looks up the user in the `idm` (identity management) schema and returns a combined array of all roles
* (deduped in case the user is given the same role multiple times; for example, by being assigned a role directly, then
* later added to a group which also includes that role). It also returns an array of groups that the user is a member
* of.
*
* @param {Number} userId The user id to get roles and groups for
*
* @returns {Object} result The resulting roles and groups
* @returns {RoleModel[]} result.roles An array of RoleModel objects representing the roles the user has
* @returns {GroupModel[]} result.groups An array of GroupModel objects representing the groups the user is a member of
*/
async function go (userId) {
const user = await UserModel.query()
.findById(userId)
.withGraphFetched('[roles, groups.roles]')

if (!user) {
return {
roles: [],
groups: []
}
}

const { groups, roles } = user
const rolesFromGroups = _extractRolesFromGroups(groups)
const combinedAndDedupedRoles = _combineAndDedupeRoles([...roles, ...rolesFromGroups])

return {
roles: combinedAndDedupedRoles,
groups
}
}

/**
* The roles that the user is assigned to via groups are returned from our main query within the user's Group objects.
* We want to extract the roles and remove them from the groups in order to keep the Group objects clean and avoid
* duplication in the object returned by the service. This function returns a flat array of all the group Roles objects,
* deleting them from the Group objects in the process
*/
function _extractRolesFromGroups (groups) {
return groups.flatMap((group) => {
const { roles } = group
delete group.roles
return roles
})
}

function _combineAndDedupeRoles (rolesArrayToDedupe) {
// Our usual method of deduping arrays (putting the array into a new Set and then spreading it back into an array)
// doesn't work here as the Role objects are not considered to be equal when doing this. We therefore use reduce to
// dedupe by going through each Role object in the original array and only adding it to the accumulated results array
// if a Role object with the same id isn't already in it
const dedupedArray = rolesArrayToDedupe.reduce((acc, current) => {
const roleIsAlreadyInAcc = acc.find((item) => {
return item.roleId === current.roleId
})

if (!roleIsAlreadyInAcc) {
acc.push(current)
}

return acc
}, [])

return dedupedArray
}

module.exports = {
go
}
Loading

0 comments on commit 6698bb2

Please sign in to comment.