packages/oae-principals/lib/util.js
/*!
* Copyright 2014 Apereo Foundation (AF) Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
import _ from 'underscore';
import shortid from 'shortid';
import { ActivityConstants } from 'oae-activity/lib/constants.js';
import { AuthzConstants } from 'oae-authz/lib/constants.js';
import { PrincipalsConstants } from 'oae-principals/lib/constants.js';
import * as ActivityModel from 'oae-activity/lib/model.js';
import * as AuthzUtil from 'oae-authz/lib/util.js';
import * as ContentUtil from 'oae-content/lib/internal/util.js';
import * as TenantsUtil from 'oae-tenants/lib/util.js';
import PrincipalsEmitter from 'oae-principals/lib/internal/emitter.js';
import { User } from './model.js';
import * as PrincipalsDAO from './internal/dao.js';
/**
* Get a principal (user or group)
*
* @param {Context} ctx Current execution context
* @param {String} principalId The ID of the principal that should be retrieved.
* @param {Function} callback Standard callback function
* @param {Object} callback.err An error that occurred, if any
* @param {Group|User} callback.principal The asked for principal.
*/
const getPrincipal = function (ctx, principalId, callback) {
getPrincipals(ctx, [principalId], (error, principals) => {
if (error) {
return callback(error);
}
if (!principals[principalId]) {
return callback({ code: 404, msg: 'Could not find principal with id ' + principalId });
}
return callback(null, principals[principalId]);
});
};
/**
* Get a set of principals (user or groups). This method *will* return an error if some of the principals
* don't exist
*
* @param {Context} ctx Current execution context
* @param {String[]} principalIds The ID of the principal that should be retrieved
* @param {Object} callback.err An error that occurred, if any
* @param {Object} callback.err.existingPrincipals Object representing the principals that existed in storage. The keys will be the principal ids and the values will be the user-friendly principal basic profiles
* @param {String[]} callback.err.missingPrincipalIds The ids of the principals that did not exist
* @param {Object} callback.principals Object representing the retrieved principals. The keys will be the principal ids and the values will be the principal basic profiles
*/
const getPrincipals = function (ctx, principalIds, callback) {
PrincipalsDAO.getPrincipals(principalIds, null, (error, principals) => {
if (error) {
return callback(error);
}
_transformPrincipals(ctx, principals);
return callback(null, principals);
});
};
/**
* Touch the last modified date of the given principal
*
* @param {Principal} oldPrincipal The principal whose last modified date to touch
* @param {Function} callback Standard callback function
* @param {Object} callback.err An error that occurred, if any
* @param {Principal} callback.updatedPrincipal The updated version of the principal with its last modifed date updated
*/
const touchLastModified = function (oldPrincipal, callback) {
// Const oldLastModified = oldPrincipal.lastModified;
const newLastModified = Date.now().toString();
const updatedProfileFields = { lastModified: newLastModified };
PrincipalsDAO.updatePrincipal(oldPrincipal.id, updatedProfileFields, (error) => {
if (error) {
return callback(error);
}
const updatedPrincipal = _.extend({}, oldPrincipal, updatedProfileFields);
return callback(null, updatedPrincipal);
});
};
/**
* Set the verified email address of the specified user, clearing any invitations that they have
* pending
*
* @param {Context} ctx Current execution context
* @param {String} userId The id of the user to verify the email address for
* @param {String} email The email address to place as the verified email
* @param {Function} callback Standard callback function
* @param {Object} callback.err An error that occurred, if any
* @param {User} callback.user The updated user
*/
const verifyEmailAddress = function (ctx, user, email, callback) {
PrincipalsDAO.setEmailAddress(user, email, (error, updatedUser) => {
if (error) {
return callback(error);
}
PrincipalsEmitter.emit(PrincipalsConstants.events.VERIFIED_EMAIL, ctx, updatedUser, (errs) => {
if (errs) {
return callback(_.first(errs));
}
return callback(null, updatedUser);
});
});
};
/**
* Create a unique id for a group
*
* @param {String} tenantAlias The alias of the tenant for which to generate the group id
* @return {String} The id for the group
*/
const createGroupId = function (tenantAlias) {
return AuthzUtil.toId(AuthzConstants.principalTypes.GROUP, tenantAlias, shortid.generate());
};
/**
* Determine whether or not the given string represents a group id.
*
* @param {String} groupId A string that may or may not be a group id
* @return {Boolean} Whether or not the provided identifier is a group identifier.
*/
const isGroup = function (groupId) {
return PrincipalsDAO.isGroup(groupId);
};
/**
* Determine whether or not the given string represents a user id.
*
* @param {String} userId A string that may or may not be a user id
* @return {Boolean} Whether or not the provided identifier is a user identifier.
*/
const isUser = function (userId) {
return PrincipalsDAO.isUser(userId);
};
/**
* Hide sensitive user information that the (possibly anonymous) user in context does not have access to see.
*
* If the current user has no access to the user, then the following will be scrubbed:
*
* 1. The displayName will be replaced by the publicAlias
* 2. The following fields will be deleted
* ** publicAlias
* ** locale
* ** smallPicture
* ** smallPictureUri
* ** mediumPicture
* ** mediumPictureUri
* ** largePicture
* ** largePictureUri
*
* If the user has access but is not the user themself, the publicAlias is scrubbed from the user.
*
* @param {Context} ctx Current execution context
* @param {User} user The user object to hide as necessary
* @api private
*/
const hideUserData = (ctx, user) => {
const isAnon = !ctx.user();
const isLoggedIn = TenantsUtil.isLoggedIn(ctx, user.tenant.alias);
const isTargetUser = !isAnon && ctx.user().id === user.id;
const isAdmin = !isAnon && ctx.user().isAdmin && ctx.user().isAdmin(user.tenant.alias);
const needsLoggedIn = user.visibility === AuthzConstants.visibility.LOGGEDIN;
const isPrivate = user.visibility === AuthzConstants.visibility.PRIVATE;
if ((!user.deleted && isAdmin) || isTargetUser) {
return user;
}
// Hide the sensitive profile information if the user has limited access
if (user.deleted || (needsLoggedIn && !isLoggedIn) || isPrivate) {
/**
* Show user's publicAlias instead of displayName if it doesn't resemble a Shibboleth identifier
*/
const invalid = /https?:\/\/|shibboleth!|@/i;
if (!invalid.test(user.publicAlias)) {
user.displayName = user.publicAlias;
}
user.picture = {};
/**
* The profile path should be removed from the user object as well.
* This will tell the UI when to offer a link to the profile page and when not to
*/
delete user.profilePath;
}
// Always delete these guys if it is not the target user or admin
delete user.acceptedTC;
delete user.email;
delete user.emailPreference;
delete user.locale;
delete user.notificationsUnread;
delete user.notificationsLastRead;
delete user.publicAlias;
};
/**
* Given a user object, apply the given set of basic profile updates and return the updated user.
*
* @param {User} user The user object on which to apply the updates
* @param {Object} fieldUpdates An object of fieldKey -> value of the field updates to apply to the user object
* @return {User} The updated user with all field updates applied
*/
const createUpdatedUser = function (user, fieldUpdates) {
const newDisplayName = fieldUpdates.displayName || user.displayName;
const newEmail = fieldUpdates.email || user.email;
const newUser = new User(user.tenant.alias, user.id, newDisplayName, newEmail, {
visibility: fieldUpdates.visibility || user.visibility,
emailPreference: fieldUpdates.emailPreference || user.emailPreference,
locale: fieldUpdates.locale || user.locale,
publicAlias: fieldUpdates.publicAlias || user.publicAlias,
notificationsUnread: fieldUpdates.notificationsUnread || user.notificationsUnread,
notificationsLastRead: fieldUpdates.notificationsLastRead || user.notificationsLastRead,
acceptedTC: fieldUpdates.acceptedTC || user.acceptedTC,
isGlobalAdmin: user.isGlobalAdmin(),
isTenantAdmin: user.isTenantAdmin(user.tenant.alias)
});
return newUser;
};
/// ///////////////////////////
// ACTIVITY UTILITY METHODS //
/// ///////////////////////////
/**
* Create the persistent user entity that can be transformed into an activity entity for the UI.
*
* @param {String} userId The ID of the user
* @param {User} [user] The user that supplies the data for the entity
* @return {Object} An object containing the entity data that can be transformed into a UI user activity entity
*/
const createPersistentUserActivityEntity = function (userId, user) {
return new ActivityModel.ActivityEntity('user', userId, user.visibility, { user });
};
/**
* Transform a persisted user activity entity that can be used in an activity stream The returned activity entity will be
* output in the `activitystrea.ms`-compliant data model
*
* For more details on the transformed entity model, @see ActivityAPI#registerActivityEntityType
*
* @param {Context} ctx Current execution context
* @param {String} userId The id of the user
* @param {User} [user] The user object. If not specified, the generated entity with be abbreviated with just the information available
* @return {ActivityEntity} The activity entity that represents the given user data
*/
const transformPersistentUserActivityEntity = function (ctx, userId, user) {
const tenant = ctx.tenant();
const baseUrl = TenantsUtil.getBaseUrl(tenant);
const globalId = baseUrl + '/api/user/' + userId;
const options = { ext: {} };
options.ext[ActivityConstants.properties.OAE_ID] = userId;
if (user) {
hideUserData(ctx, user);
// Signed user profile picture URLs will last forever
_generatePictureURLs(ctx, user, -1);
options.displayName = user.displayName;
if (user.profilePath) {
options.url = baseUrl + user.profilePath;
if (user.picture.small) {
options.ext[ActivityConstants.properties.OAE_THUMBNAIL] = new ActivityModel.ActivityMediaLink(
user.picture.small,
PrincipalsConstants.picture.size.SMALL,
PrincipalsConstants.picture.size.SMALL
);
}
if (user.picture.medium) {
options.image = new ActivityModel.ActivityMediaLink(
user.picture.medium,
PrincipalsConstants.picture.size.MEDIUM,
PrincipalsConstants.picture.size.MEDIUM
);
}
}
options.ext[ActivityConstants.properties.OAE_VISIBILITY] = user.visibility;
options.ext[ActivityConstants.properties.OAE_PROFILEPATH] = user.profilePath;
}
return new ActivityModel.ActivityEntity('user', globalId, user.visibility, options);
};
/**
* Transform a persisted user activity entity that can be used in an activity stream. The user property will be taken from
* the persisted entity, scrubbed and returned as the "transformed entity"
*
* @param {Context} ctx Current execution context
* @param {String} userId The id of the user
* @param {User} [user] The user object. If not specified, the generated entity with be abbreviated with just the information available
* @return {User} The scrubbed user object
*/
const transformPersistentUserActivityEntityToInternal = function (ctx, userId, user) {
if (user) {
// Signed user profile picture URLs will last forever
hideUserData(ctx, user);
_generatePictureURLs(ctx, user, -1);
return user;
}
return { id: userId };
};
/**
* Create the persistent group entity that can be transformed into an activity entity for the UI
*
* @param {String} groupId The ID of the group
* @param {Group} [group] The group that supplies the data for the entity. If not specified, only the minimal data will be returned for transformation.
* @return {Object} An object containing the entity data that can be transformed into a UI group activity entity
*/
const createPersistentGroupActivityEntity = function (groupId, group) {
return new ActivityModel.ActivityEntity('group', groupId, group.visibility, { group });
};
/**
* Transform a persisted group activity entity that can be used in an activity stream
*
* For more details on the transformed entity model, @see ActivityAPI#registerActivityEntityType
*
* @param {Context} ctx Current execution context
* @param {String} groupId The id of the group
* @param {Group} [group] The group object. If not specified, the generated entity with be abbreviated with just the information available
* @return {ActivityEntity} The activity entity that represents the given group data
*/
const transformPersistentGroupActivityEntity = function (ctx, groupId, group) {
const tenant = ctx.tenant();
const baseUrl = TenantsUtil.getBaseUrl(tenant);
// Note that the globalId is used as a canonical reference and should not depend on whether or not
// the tenant is using http or https
const globalId = 'http://' + tenant.host + '/api/group/' + groupId;
const options = { ext: {} };
options.ext[ActivityConstants.properties.OAE_ID] = groupId;
if (group) {
// Signed group picture URLs will last forever
_generatePictureURLs(ctx, group, -1);
options.displayName = group.displayName;
options.url = baseUrl + group.profilePath;
if (group.picture.small) {
options.ext[ActivityConstants.properties.OAE_THUMBNAIL] = new ActivityModel.ActivityMediaLink(
group.picture.small,
PrincipalsConstants.picture.size.SMALL,
PrincipalsConstants.picture.size.SMALL
);
}
if (group.picture.medium) {
options.image = new ActivityModel.ActivityMediaLink(
group.picture.medium,
PrincipalsConstants.picture.size.MEDIUM,
PrincipalsConstants.picture.size.MEDIUM
);
}
// Extension properties
options.ext[ActivityConstants.properties.OAE_VISIBILITY] = group.visibility;
if (!group.deleted) {
options.ext[ActivityConstants.properties.OAE_PROFILEPATH] = group.profilePath;
}
options.ext[ActivityConstants.properties.OAE_JOINABLE] = group.joinable;
}
return new ActivityModel.ActivityEntity('group', globalId, group.visibility, options);
};
/**
* Transform a persisted group activity entity that can be used in an activity stream. The group property will be
* taken from the persisted entity, scrubbed and returned as the "transformed entity"
*
* For more details on the transformed entity model, @see ActivityAPI#registerActivityEntityType
*
* @param {Context} ctx Current execution context
* @param {String} groupId The id of the group
* @param {Group} [group] The group object. If not specified, the generated entity with be abbreviated with just the information available
* @return {Group} The group object
*/
const transformPersistentGroupActivityEntityToInternal = function (ctx, groupId, group) {
if (group) {
// Signed group picture URLs will last forever
_generatePictureURLs(ctx, group, -1);
return group;
}
return { id: groupId };
};
/**
* Given a set of principals, transform their model so the required UI-level information is
* available
*
* @param {Context} ctx Current execution context
* @param {Object[]} principals The array of users and groups to transform
* @api private
*/
const _transformPrincipals = function (ctx, principals) {
_.each(principals, (principal) => {
_generatePictureURLs(ctx, principal);
if (isUser(principal.id)) {
hideUserData(ctx, principal);
}
});
};
/**
* Replace the URI properties with signed URL paths to actually download the files
*
* @param {Context} ctx Current execution context
* @param {Group|User} principal The principal for which to generate the picture URL paths
* @param {Number} [duration] The approximate time in seconds for which the generated picture URLs will be valid. Default: 1 week
* @param {Number} [offset] The minimum time in seconds for which the generated picture URLs will be valid. Default: 1 week
* @api private
*/
const _generatePictureURLs = function (ctx, principal, duration, offset) {
if (principal.picture.smallUri) {
principal.picture.small = ContentUtil.getSignedDownloadUrl(ctx, principal.picture.smallUri, duration, offset);
delete principal.picture.smallUri;
}
if (principal.picture.mediumUri) {
principal.picture.medium = ContentUtil.getSignedDownloadUrl(ctx, principal.picture.mediumUri, duration, offset);
delete principal.picture.mediumUri;
}
if (principal.picture.largeUri) {
principal.picture.large = ContentUtil.getSignedDownloadUrl(ctx, principal.picture.largeUri, duration, offset);
delete principal.picture.largeUri;
}
};
export {
getPrincipal,
getPrincipals,
touchLastModified,
verifyEmailAddress,
createGroupId,
isGroup,
isUser,
hideUserData,
createUpdatedUser,
createPersistentUserActivityEntity,
transformPersistentUserActivityEntity,
transformPersistentUserActivityEntityToInternal,
createPersistentGroupActivityEntity,
transformPersistentGroupActivityEntity,
transformPersistentGroupActivityEntityToInternal
};