src/current-user/CurrentUser.js
/**
* @module current-user
*/
import UserAuthorities from './UserAuthorities'
import UserSettings from './UserSettings'
import { noCreateAllowedFor } from '../defaultConfig'
import UserDataStore from '../datastore/UserDataStore'
const models = Symbol('models')
const propertiesToIgnore = new Set([
'userCredentials',
'userGroups',
'userRoles',
'organisationUnits',
'dataViewOrganisationUnits',
])
/**
* Authorities lookup map to be used for determining the list of authorities to check.
*
* @private
* @type {Object.<string, string[]>}
* @readonly
* @typedef {Object} AuthorityType
* @memberof module:current-user
*/
const authTypes = {
READ: ['READ'],
CREATE: ['CREATE', 'CREATE_PUBLIC', 'CREATE_PRIVATE'],
CREATE_PUBLIC: ['CREATE_PUBLIC'],
CREATE_PRIVATE: ['CREATE_PRIVATE'],
DELETE: ['DELETE'],
UPDATE: ['UPDATE'],
EXTERNALIZE: ['EXTERNALIZE'],
}
/**
* Create a map of `propertyName` -> `Symbol`. This map is used to hide values for these properties. We will instead add
* add convenience methods for these properties. (e.g. the `userGroups` property on the currentUser object becomes
* `getUserGroups()`
*
* @private
* @type {Object.<string, Symbol>}
*/
const propertySymbols = Array.from(propertiesToIgnore).reduce(
(result, property) => {
result[property] = Symbol(property)
return result
},
{}
)
/**
* Creates a map of propertyName and propertyValue pairs of properties to be attached to the currentUser object.
* These are all the regular properties that are returned when calling `api/27/me` but with the `userCredentials`
* merged onto the same object.
*
* What would originally be `currentUser.userCredentials.username` would just be `currentUser.username`
*
* @private
* @param {Object} currentUserObject The user payload as it is received from the api. https://play.dhis2.org/demo/api/27/me
* @returns {Object} A map with propertyName/propertyValue pairs.
*/
function getPropertiesForCurrentUserObject(currentUserObject) {
let properties
// The userCredentials object on the userObject is confusing so we set the properties straight onto the currentUser
// object itself
if (currentUserObject.userCredentials) {
properties = Object.assign(
{},
currentUserObject.userCredentials,
currentUserObject
)
} else {
properties = Object.assign({}, currentUserObject)
}
return Object.keys(properties).reduce((result, property) => {
if (propertiesToIgnore.has(property)) {
if (properties[property].map) {
result[propertySymbols[property]] = properties[property].map(
(value) => value.id
)
}
} else {
result[property] = properties[property]
}
return result
}, {})
}
/**
* Checks the noCreateAllowedFor list if the object can be created.
*
* @private
* @param {ModelDefinition} modelDefinition The modelDefinition to check for.
* @returns {boolean} True when it exists in the list, false otherwise.
*/
function isInNoCreateAllowedForList(modelDefinition) {
return Boolean(
modelDefinition && noCreateAllowedFor.has(modelDefinition.name)
)
}
/**
* Represents the current logged in user
*
* @memberof module:current-user
*/
class CurrentUser {
/**
* Creates the CurrentUser.
*
* @param {Object} userData Payload as returned from the api when requesting the currentUser object.
* @param {UserAuthorities} userAuthorities The UserAuthorities object for the currentUsers authorities.
* @param {ModelDefinition[]} modelDefinitions The modelDefinitions that need to be used for checking access.
* @param {UserSettings} settings The userSettings object to be set onto the current user object.
*/
constructor(userData, userAuthorities, modelDefinitions, settings) {
Object.assign(this, getPropertiesForCurrentUserObject(userData))
/**
*
* @type {UserAuthorities}
*/
this.authorities = userAuthorities
this[models] = modelDefinitions
/**
* Contains a reference to a `UserSettings` instance that can be used
* to retrieve and save system settings.
* @type {UserSettings}
* @description
* ```js
* d2.currentUser.userSettings.get('keyUiLocale')
* .then(userSettingsValue => {
* console.log('UI Locale: ' + userSettingsValue);
* });
* ```
*/
this.userSettings = settings
/**
* Contains a reference to {@link module:current-user.UserDataStore UserDataStore}
* @type UserDataStore
*
*/
this.dataStore = UserDataStore.getUserDataStore()
}
/**
* Get a list of group ids the current user belongs to.
*
* @returns {Array} The list of ids of all the user's groups.
*/
getUserGroupIds() {
return this[propertySymbols.userGroups]
}
/**
* Get a ModelCollection of userGroup models that are assigned to the currentUser
*
* The user groups are lazy loaded on init of the library. This method can be used to load the full representation
* of the userGroups.
*
* The request done is equivalent do doing https://play.dhis2.org/demo/api/27/me.json?fields=userGroups[:all]
*
* @param {Object} [listOptions={}] Additional query parameters that should be send with the request.
* @returns {Promise<ModelCollection>} The model collection that contains the user's groups.
*/
getUserGroups(listOptions = {}) {
const userGroupIds = this[propertySymbols.userGroups]
return this[models].userGroup.list(
Object.assign({ paging: false }, listOptions, {
filter: [`id:in:[${userGroupIds.join(',')}]`],
})
)
}
/**
* Get a ModelCollection of userRole models that are assigned to the currentUser
*
* The user roles are lazy loaded on init of the library. This method can be used to load the full representation
* of the userGroups.
*
* The request done is equivalent do doing https://play.dhis2.org/demo/api/27/me.json?fields=userCredentials[userRoles[:all]]
*
* @returns {Promise<ModelCollection>} A ModelCollection that contains the user's groups.
*/
getUserRoles() {
const userRoleIds = this[propertySymbols.userRoles]
return this[models].userRole.list({
filter: [`id:in:[${userRoleIds.join(',')}]`],
paging: false,
})
}
/**
* Requests a users primary organisation units from the api.
*
* The users organisation units are lazy loaded on init of the library (just the ids). This method can be used to
* load the full representation of the organisationUnits.
*
* @param {Object} [listOptions={}] Additional query parameters that should be send with the request.
* @returns {Promise<ModelCollection>} A ModelCollection that contains the user's organisationUnits.
*/
getOrganisationUnits(listOptions = {}) {
const organisationUnitsIds = this[propertySymbols.organisationUnits]
return this[models].organisationUnit.list(
Object.assign(
{
fields: ':all,displayName,path,children[id,displayName,path,children::isNotEmpty]',
paging: false,
},
listOptions,
{ filter: [`id:in:[${organisationUnitsIds.join(',')}]`] }
)
)
}
/**
* Requests a users data view organisation units from the api.
*
* The users data view organisation units are lazy loaded on init of the library (just the ids). This method can
* be used to load the full representation of the dataViewOrganisationUnits.
*
* @param {Object} [listOptions={}] Additional query parameters that should be send with the request.
* @returns {Promise<ModelCollection>} A ModelCollection that contains the user's dataViewOrganisationUnits.
*/
getDataViewOrganisationUnits(listOptions) {
const organisationUnitsIds =
this[propertySymbols.dataViewOrganisationUnits]
return this[models].organisationUnit.list(
Object.assign(
{
fields: ':all,displayName,path,children[id,displayName,path,children::isNotEmpty]',
paging: false,
},
listOptions,
{ filter: [`id:in:[${organisationUnitsIds.join(',')}]`] }
)
)
}
/**
* Helper function to check if the currentUser can perform an action.
*
* A schema from the api defines the authorities as follows:
* "authorities": [
* {
* "type": "CREATE_PUBLIC",
* "authorities": [
* "F_DATAELEMENT_PUBLIC_ADD"
* ]
* }, {
* "type": "CREATE_PRIVATE",
* "authorities": [
* "F_DATAELEMENT_PRIVATE_ADD"
* ]
* }, {
* "type": "DELETE",
* "authorities": [
* "F_DATAELEMENT_DELETE"
* ]
* }
*
* ],
*
* So for example, when asking for `currentUser.canCreate(modelDefinition)` we look at the authorities map for CREATE, we
* collect the authorities from the schema based on the values of `CREATE`. So in the case of above schema authorities
* we take the authorities for 'CREATE_PUBLIC' and 'CREATE_PRIVATE' and check if the user has at least one of the
* authorities in the combined list. If we had asked for `currentUser.canCreatePrivate(modelDefinition)` we would only
* need to check if the user has `F_DATAELEMENT_PRIVATE_ADD` However for just create the user could still create when he
* has either private or public.
*
* @private
* @param {AuthorityType} authorityType The type of authority to check for.
* @param {ModelDefinition} modelDefinition The ModelDefinition for the type of object that the authority should
* be checked for.
* @returns {boolean} True when the user has the authority to perform the action, otherwise false.
*/
checkAuthorityForType(authorityType, modelDefinition) {
if (!modelDefinition || !Array.isArray(modelDefinition.authorities)) {
return false
}
return (
modelDefinition.authorities
// Filter the correct authority to check for from the model
.filter((authority) =>
authorityType.some(
(authToHave) => authToHave === authority.type
)
)
// Check the left over schema authority types
.some(
(schemaAuthority) =>
schemaAuthority.authorities.some(
(authorityToCheckFor) =>
this.authorities.has(authorityToCheckFor)
) // Check if one of the schema authorities are available in the users authorities
)
)
}
/**
* Helper method to first check the special "not allowed to create list" before checking the users authorities.
*
* @private
* @param {AuthorityType} authType The type of authority that should be checked. (CREATE, CREATE_PRIVATE
* or CREATE_PUBLIC)
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to create the object
*/
checkCreateAuthorityForType(authType, modelDefinition) {
// When the modelDefinition is mentioned in the the list of modelTypes that are not
// allowed to be created we return false
if (isInNoCreateAllowedForList(modelDefinition)) {
return false
}
// Otherwise we check using the normal procedure for checking authorities
return this.checkAuthorityForType(authType, modelDefinition)
}
/**
* Check if the user has the authority to create objects of a type of Model (based on it's modelDefinition)
* If the object supports public and private objects then both are checked, and the method will return true if one
* of them is true. (e.g. will also return true if the user can only create private objects)
*
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to create the object
*/
canCreate(modelDefinition) {
return this.checkCreateAuthorityForType(
authTypes.CREATE,
modelDefinition
)
}
/**
* Check if the user has the authority to create public objects of a type of Model (based on it's modelDefinition)
*
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to create the object
*/
canCreatePublic(modelDefinition) {
return this.checkCreateAuthorityForType(
authTypes.CREATE_PUBLIC,
modelDefinition
)
}
/**
* Check if the user has the authority to create private objects of a type of Model (based on it's modelDefinition)
*
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to create the object
*/
canCreatePrivate(modelDefinition) {
return this.checkCreateAuthorityForType(
authTypes.CREATE_PRIVATE,
modelDefinition
)
}
/**
* Check if the user has the authority to delete objects of a type of Model (based on it's modelDefinition)
*
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to delete the object
*
* @deprecated Delete should be checked through the `Model.access.delete` property instead, as that also takes into
* account sharing. Just checking if the user has the authority to delete a specific object does not take into account
* the full ACL.
*/
canDelete(modelDefinition) {
return this.checkAuthorityForType(authTypes.DELETE, modelDefinition)
}
/**
* Check if the user has the authority to update objects of a type of Model (based on it's modelDefinition)
*
* @param {ModelDefinition} modelDefinition The modelDefinition that the authorities should be checked for.
* @returns {boolean} True when the user has the permission to update the object
*
* @deprecated Update should be checked through the `Model.access.update` property instead, as that also takes into
* account sharing. Just checking if the user has the authority to update a specific object does not take into account
* the full ACL.
*/
canUpdate(modelDefinition) {
if (this.checkAuthorityForType(authTypes.UPDATE, modelDefinition)) {
return true
}
return this.checkAuthorityForType(authTypes.CREATE, modelDefinition)
}
/**
* Factory method for creating a CurrentUser instance
*
* @param {Object} userData Payload as returned from the api when requesting the currentUser object.
* @param {string[]} authorities A list of authorities that the currentUser has.
* @param {ModelDefinition[]} modelDefinitions The modelDefinitions that need to be used for checking access.
* @param {Object} userSettings Payload as returned from the api when request the userSettings
* @returns {CurrentUser} The created CurrentUser object based on the data given.
*/
static create(userData, authorities, modelDefinitions, userSettings) {
return new CurrentUser(
userData,
UserAuthorities.create(authorities),
modelDefinitions,
new UserSettings(userSettings)
)
}
}
export default CurrentUser