var _deletePrincipalFields = function(principalId, profileFields, callback) { // Remove the specified fields var query = util.format('DELETE "%s" FROM "Principals" where "principalId" = ?', profileFields.join('", "')); Cassandra.runQuery(query, [principalId], function(err) { if (err) { return callback(err); } // If the principal is a user, invalidate their cache entry. They are being invalidated // rather than simply updated in the cache because we have removed fields OaeUtil.invokeIfNecessary(isUser(principalId), invalidateCachedUsers, [principalId], function(err) { if (err) { return callback(err); } // This is an update, so we also need to touch the principal _updatePrincipal(principalId, {}, function(err) { if (err) { // We bypass indicating an error to the consumer in this case as we have // successfully removed the fields in both Cassandra and the cache log().warn({ 'err': err, 'principalId': principalId }, 'An unexpected error occurred while trying to touch a principal timestamp'); } return callback(); }); }); }); };
var createMeeting = module.exports.createMeeting = function(createdBy, displayName, description, record, allModerators, waitModerator, visibility, opts, callback) { opts = opts || {}; var created = opts.created || Date.now(); created = created.toString(); var tenantAlias = AuthzUtil.getPrincipalFromId(createdBy).tenantAlias; var meetingId = _createMeetingId(tenantAlias); var storageHash = { 'tenantAlias': tenantAlias, 'createdBy': createdBy, 'displayName': displayName, 'description': description, 'record': record, 'allModerators': allModerators, 'waitModerator': waitModerator, 'visibility': visibility, 'created': created, 'lastModified': created }; var query = Cassandra.constructUpsertCQL('Meetings', 'id', meetingId, storageHash); Cassandra.runQuery(query.query, query.parameters, function(err) { if (err) { console.info(err); return callback(err); } return callback(null, _storageHashToMeeting(meetingId, storageHash)); }); };
var createDiscussion = module.exports.createDiscussion = function(createdBy, displayName, description, visibility, opts, callback) { opts = opts || {}; var created = opts.created || Date.now(); var tenantAlias = AuthzUtil.getPrincipalFromId(createdBy).tenantAlias; var discussionId = _createDiscussionId(tenantAlias); var storageHash = { 'tenantAlias': tenantAlias, 'createdBy': createdBy, 'displayName': displayName, 'description': description, 'visibility': visibility, 'created': created, 'lastModified': created }; var query = Cassandra.constructUpsertCQL('Discussions', 'id', discussionId, storageHash, 'QUORUM'); Cassandra.runQuery(query.query, query.parameters, function(err) { if (err) { return callback(err); } return callback(null, _storageHashToDiscussion(discussionId, storageHash)); }); };
const getPreviewUris = function(revisionIds, callback) { revisionIds = _.uniq(revisionIds); if (_.isEmpty(revisionIds)) { return callback(null, {}); } Cassandra.runQuery( 'SELECT "revisionId", "thumbnailUri", "wideUri" FROM "Revisions" WHERE "revisionId" IN ?', [revisionIds], (err, rows) => { if (err) { return callback(err); } const previews = {}; rows.forEach(row => { const revisionId = row.get('revisionId'); previews[revisionId] = {}; const thumbnailUri = row.get('thumbnailUri'); if (thumbnailUri) { previews[revisionId].thumbnailUri = thumbnailUri; } const wideUri = row.get('wideUri'); if (wideUri) { previews[revisionId].wideUri = wideUri; } }); return callback(null, previews); } ); };
var getContentPreviewsMetadata = module.exports.getContentPreviewsMetadata = function(contentIds, callback) { if (!contentIds || !contentIds.length) { return callback(null, {}); } Cassandra.runQuery('SELECT lastModified, previews FROM Content WHERE contentId IN (?)', [contentIds], function(err, rows) { if (err) { return callback(err); } var previews = {}; rows.forEach(function(row) { var lastModified = row.get('lastModified'); var preview = row.get('previews'); if (preview && preview.value && lastModified && lastModified.value) { lastModified = lastModified.value; try { preview = JSON.parse(preview.value); } catch (err) { log().warn({'contentId': row.key, 'previews': preview.value}, 'Could not parse preview data for content item.'); return; } previews[row.key] = {'lastModified': lastModified, 'previews': preview}; } }); return callback(null, previews); }); };
var updatePrincipal = module.exports.updatePrincipal = function(principalId, profileFields, callback) { var validator = new Validator(); // Ensure we're using a real principal id. If we weren't, we would be dangerously upserting an invalid principal validator.check(principalId, {'code': 400, 'msg': 'Attempted to update a principal with a non-principal id'}).isPrincipalId(); // Ensure the caller is not trying to set an invalid field var invalidKeys = _.intersection(RESTRICTED_FIELDS, _.keys(profileFields)); validator.check(invalidKeys.length, {'code': 400, 'msg': 'Attempted to update an invalid property'}).max(0); if (validator.hasErrors()) { return callback(validator.getFirstError()); } var q = Cassandra.constructUpsertCQL('Principals', 'principalId', principalId, profileFields, 'QUORUM'); if (!q) { return callback({'code': 500, 'msg': 'Unable to store profile fields'}); } Cassandra.runQuery(q.query, q.parameters, function(err) { if (err) { return callback(err); } if (isUser(principalId)) { // Update the user in cache // Ensure the `principalId` is not part of the hash to avoid revalidating a stale cache entry profileFields = _.extend({}, profileFields); delete profileFields.principalId; return _updateCachedUser(principalId, profileFields, callback); } else { return callback(); } }); };
var getPreviewUris = module.exports.getPreviewUris = function(revisionIds, callback) { revisionIds = _.uniq(revisionIds); if (!revisionIds || revisionIds.length === 0) { return callback(null, {}); } Cassandra.runQuery('SELECT thumbnailUri, wideUri FROM Revisions WHERE revisionId IN (?)', [revisionIds], function(err, rows) { if (err) { return callback(err); } var previews = {}; rows.forEach(function(row) { previews[row.key] = {}; var thumbnailUri = row.get('thumbnailUri'); if (thumbnailUri && thumbnailUri.value) { previews[row.key].thumbnailUri = thumbnailUri.value; } var wideUri = row.get('wideUri'); if (wideUri && wideUri.value) { previews[row.key].wideUri = wideUri.value; } }); return callback(null, previews); }); };
var updateContent = module.exports.updateContent = function(contentObj, profileUpdates, librariesUpdate, callback) { // Set the lastModified timestamp. var oldLastModified = contentObj.lastModified; profileUpdates.lastModified = Date.now(); var q = Cassandra.constructUpsertCQL('Content', 'contentId', contentObj.id, profileUpdates, 'QUORUM'); Cassandra.runQuery(q.query, q.parameters, function(err) { if (err) { return callback(err); } // Create the new content object by merging in the metadata changes over the old content object var newContentObj = _.extend({}, contentObj, profileUpdates); if (!librariesUpdate) { return callback(null, newContentObj); } else { _updateLibraries(newContentObj, oldLastModified, newContentObj.lastModified, [], function(err) { if (err) { return callback(err); } callback(null, newContentObj); }); } }); };
canCreateGroup(ctx, groupId, function(err) { if (err) { return callback(err); } // Create the group. Cassandra.runQuery('INSERT INTO Principals (principalId, alias, tenant, displayName, description, visibility, joinable) VALUES (?, ?, ?, ?, ?, ?, ?) USING CONSISTENCY QUORUM', [groupId, alias, tenant.alias, displayName, description, visibility, joinable], function (err) { if (err) { return callback(err); } // Immediately add the current user as a manager var currentUser = getUserId(ctx); members[currentUser] = Constants.roles.MANAGER; var opts = { 'displayName': displayName, 'description': description, 'visibility': visibility, 'joinable': joinable }; var group = new Group(tenant.alias, groupId, alias, opts); _setGroupMembers(ctx, group, members, function(err) { PrincipalsEmitter.emit(PrincipalsConstants.events.CREATED_GROUP, ctx, group, members); if (err) { return callback(err); } return callback(null, groupId); }); }); });
var getActivitiesFromStreams = module.exports.getActivitiesFromStreams = function(activityStreamIds, start, callback) { var query = 'SELECT * FROM "ActivityStreams" WHERE "activityStreamId" IN ?'; var parameters = [ activityStreamIds ]; if (start) { query += ' AND "activityId" > ?'; parameters.push(start + ':'); } Cassandra.runQuery(query, parameters, function(err, rows) { if (err) { return callback(err); } var activitiesPerStream = {}; _.each(rows, function(row) { var activityStreamId = row.get('activityStreamId'); var activityId = row.get('activityId'); var activityStr = row.get('activity'); try { var activity = JSON.parse(activityStr); activitiesPerStream[activityStreamId] = activitiesPerStream[activityStreamId] || []; activitiesPerStream[activityStreamId].push(activity); } catch (err) { activityStr = activityStr.slice(0, 300); log().warn({'err': err, 'activityId': activityId, 'value': activityStr}, 'Error parsing activity from Cassandra'); } }); return callback(null, activitiesPerStream); }); };
var _cacheTenants = function(callback) { callback = callback || function() {}; // Get the available tenants Cassandra.runQuery('SELECT * FROM "Tenant"', false, function(err, rows) { if (err) { return callback(err); } // Reset the previously cached tenants tenants = {}; tenantsByHost = {}; // Create a dummy tenant object that can serve as the global admin tenant object globalTenant = new Tenant(serverConfig.globalAdminAlias, 'Global admin server', serverConfig.globalAdminHost, {'isGlobalAdminServer': true}); // Cache it as part of the available tenants tenants[globalTenant.alias] = globalTenant; tenantsByHost[globalTenant.host] = globalTenant; _.each(rows, function(row) { var tenant = mapToTenant(row); // Cache all tenants tenants[tenant.alias] = tenant; tenantsByHost[tenant.host] = tenant; }); return callback(null, tenants); }); };
const _runQueryIfSpecified = function(query, callback) { if (!query) { return callback(); } Cassandra.runQuery(query.query, query.parameters, callback); };
var acceptTermsAndConditions = module.exports.acceptTermsAndConditions = function(userId, callback) { Cassandra.runQuery('UPDATE "Principals" SET "acceptedTC" = ? WHERE "principalId" = ?', [Date.now().toString(), userId], function(err) { if (err) { return callback(err); } return invalidateCachedUsers([userId], callback); }); };
var getPrincipals = module.exports.getPrincipals = function(principalIds, fields, callback) { if (_.isEmpty(principalIds)) { return callback(null, {}); } // If we're only requesting 1 principal we can hand it off to the getPrincipal method. // This will try looking in the cache first, which might be faster if (principalIds.length === 1) { getPrincipal(principalIds[0], function(err, user) { if (err && err.code === 404) { // This method never returns an error if any principals in the listing are missing, // even if it is just a listing of 1 principal (e.g., a library of 1 item) return callback(null, {}); } else if (err) { return callback(err); } var users = {}; users[principalIds[0]] = user; return callback(null, users); }); return; } // Build the query and parameters to select just the specified fields var query = null; var parameters = []; // If `fields` was specified, we select only the fields specified. Otherwise we select all (i.e., *) if (fields) { var columns = []; _.map(fields, function(field) { columns.push(util.format('"%s"', field)); }); query = 'SELECT ' + columns.join(',') + ' FROM "Principals" WHERE "principalId" IN (?)'; } else { query = 'SELECT * FROM "Principals" WHERE "principalId" IN (?)'; } parameters.push(principalIds); Cassandra.runQuery(query, parameters, function(err, rows) { if (err) { return callback(err); } var principals = _.chain(rows) .map(_getPrincipalFromRow) .compact() .indexBy('id') .value(); return callback(null, principals); }); };
var updateDiscussion = module.exports.updateDiscussion = function(discussion, profileFields, callback) { var storageHash = _.extend({}, profileFields); storageHash.lastModified = storageHash.lastModified || Date.now(); var query = Cassandra.constructUpsertCQL('Discussions', 'id', discussion.id, storageHash, 'QUORUM'); Cassandra.runQuery(query.query, query.parameters, function(err) { if (err) { return callback(err); } return callback(null, _createUpdatedDiscussionFromStorageHash(discussion, storageHash)); }); };
var getEmailToken = module.exports.getEmailToken = function(userId, callback) { Cassandra.runQuery('SELECT * FROM "PrincipalsEmailToken" WHERE "principalId" = ?', [userId], function(err, rows) { if (err) { return callback(err); } else if (_.isEmpty(rows)) { return callback({'code': 404, 'msg': 'No email token found for the given user id'}); } var token = _.first(rows).get('token').value; var email = _.first(rows).get('email').value; return callback(null, email, token); }); };
var isStale = module.exports.isStale = function(indexName, libraryId, visibility, callback) { // Select both the high and low slug column from the library var cql = 'SELECT "value" FROM "LibraryIndex" WHERE "bucketKey" = ? AND "rankedResourceId" IN (?)'; Cassandra.runQuery(cql, [_createBucketKey(indexName, libraryId, visibility), [SLUG_HIGH, SLUG_LOW]], function(err, rows) { if (err) { return callback(err); } // If we got exactly 2 rows, it means that both the high and low slug were there, so the // library index is recent return callback(null, (rows.length !== 2)); }); };
var createTenant = module.exports.createTenant = function(ctx, alias, displayName, host, callback) { callback = callback || function() {}; if (!ctx.user() || !ctx.user().isGlobalAdmin()) { return callback({'code': 401, 'msg': 'Only global administrators can create new tenants'}); } var validator = new Validator(); validator.check(alias, {'code': 400, 'msg': 'Missing alias'}).notEmpty(); validator.check(alias, {'code': 400, 'msg': 'The tenant alias should not contain a space'}).notContains(' '); validator.check(alias, {'code': 400, 'msg': 'The tenant alias should not contain a colon'}).notContains(':'); validator.check(displayName, {'code': 400, 'msg': 'Missing tenant displayName'}).notEmpty(); validator.check(host, {'code': 400, 'msg': 'Missing tenant host'}).notEmpty(); if (validator.hasErrors()) { return callback(validator.getFirstError()); } // Make sure the tenant host name is all lower-case host = host.toLowerCase(); // Make sure that a tenant with the same alias doesn't exist yet if (!getTenant(alias)) { // Make sure that a tenant with the same hostname doesn't exist yet if (!getTenantByHost(host)) { // Create the tenant var tenant = new Tenant(alias, displayName, host); Cassandra.runQuery('UPDATE "Tenant" SET "displayName" = ?, "host" = ?, "active" = ? WHERE "alias" = ?', [tenant.displayName, host, tenant.active, tenant.alias], function(err) { if (err) { return callback(err); } // Send a message to all the app servers in the cluster notifying them that the tenant should be started Pubsub.publish('oae-tenants', 'start ' + tenant.alias, function(err) { if (err) { return callback(err); } // Let the configuration module know that a new tenant has been created and configuration needs to be fetched Pubsub.publish('oae-config', tenant.alias); callback(null, tenant); }); }); } else { callback({'code': 400, 'msg': 'A tenant with the host ' + host + ' already exists'}); } } else { callback({'code': 400, 'msg': 'A tenant with the alias ' + alias + ' already exists'}); } };
var getRevision = module.exports.getRevision = function(revisionId, callback) { Cassandra.runQuery('SELECT * FROM Revisions WHERE revisionId = ?', [revisionId], function (err, rows) { if (err) { return callback(err); } // Cassandra always returns the key as a column so the count property will always be 1. if (rows[0].count <= 1) { return callback({'code': 404, 'msg': 'Couldn\'t find revision: ' + revisionId}); } var revision = _rowToRevision(rows[0]); callback(null, revision); }); };
var getContent = module.exports.getContent = function(contentId, callback) { Cassandra.runQuery('SELECT * FROM Content USING CONSISTENCY QUORUM WHERE contentId = ?', [contentId], function (err, rows) { if (err) { return callback(err); } // Cassandra always returns the key as a column so the count will always be 1. if (rows[0].count <= 1) { return callback({'code': 404, 'msg': 'Couldn\'t find content: ' + contentId}, null); } var contentObj = _rowToContent(rows[0]); return callback(null, contentObj); }); };
var updateMeeting = module.exports.updateMeeting = function(meeting, profileFields, callback) { var storageHash = _.extend({}, profileFields); storageHash.lastModified = storageHash.lastModified || Date.now(); storageHash.lastModified = storageHash.lastModified.toString(); var query = Cassandra.constructUpsertCQL('Meetings', 'id', meeting.id, storageHash); Cassandra.runQuery(query.query, query.parameters, function(err) { if (err) { console.info(err); return callback(err); } return callback(null, _createUpdatedMeetingFromStorageHash(meeting, storageHash)); }); };
var _getUserIdFromLoginId = function(loginId, callback) { Cassandra.runQuery('SELECT userId FROM AuthenticationLoginId USING CONSISTENCY QUORUM WHERE loginId = ?', [_flattenLoginId(loginId)], function(err, rows) { if (err) { return callback(err); } var row = rows[0]; if (row) { var result = Cassandra.rowToHash(row); return callback(null, result.userId); } else { return callback(); } }); };
Cassandra.runQuery(q.query, q.parameters, function(err) { if (err) { return callback(err); } // Add the revision to the list. Cassandra.runQuery('UPDATE RevisionByContent SET ?=? WHERE contentId=?', [values.created, revisionId, contentId], function(err) { if (err) { return callback(err); } var revision = new Revision(contentId, revisionId, values.createdBy, values.created, opts); callback(null, revision); }); });
var getPrincipals = module.exports.getPrincipals = function(principalIds, callback) { if (!principalIds || principalIds.length === 0) { return callback(null, {}); } // If we're only requesting 1 principal we can hand it off to the getPrincipal method. // This will try looking in the cache first, which might be faster. if (principalIds.length === 1) { return getPrincipal(principalIds[0], function(err, user) { if (err) { return callback(err); } var users = {}; users[principalIds[0]] = user; return callback(null, users); }); } Cassandra.runQuery('SELECT * FROM Principals USING CONSISTENCY QUORUM WHERE principalId IN (?)', [principalIds], function(err, rows) { if (err) { return callback(err); } var principals = {}; var missing = []; for (var i = 0; i < rows.length; i++) { var row = rows[i]; var principal = getPrincipalFromRow(row); if (principal) { principals[principal.id] = principal; } else { missing.push(row.get('principalId').value); } } if (missing.length > 0) { return callback({ 'code': 400, 'msg': 'Some principals could not be found', 'existing': _.keys(principals), 'missing': missing }); } return callback(null, principals); }); };
var getUserIdsByEmails = module.exports.getUserIdsByEmails = function(emails, callback) { Cassandra.runQuery('SELECT * FROM "PrincipalsByEmail" WHERE "email" IN (?)', [emails], function(err, rows) { if (err) { return callback(err); } var userIdsByEmail = _.chain(rows) .map(Cassandra.rowToHash) .groupBy('email') .mapObject(function(principalIdEmailHashes) { return _.pluck(principalIdEmailHashes, 'principalId'); }) .value(); return callback(null, userIdsByEmail); }); };
var writeConfig = module.exports.writeConfig = function(tenantId, configValues, callback) { var validator = new Validator(); validator.check(tenantId, {'code': 400, 'msg': 'Missing tenantid'}).notEmpty(); validator.check(_.keys(configValues).length, {'code': 400, 'msg': 'Missing configuration. Example configuration: {"oae-authentication/google-authentication/google-authentication-enabled": {"tenantid": "global","value": false}}'}).min(1); if (validator.hasErrors()) { return callback(validator.getFirstError()); } var q = Cassandra.constructUpsertCQL('Config', 'tenantId', tenantId, configValues, 'QUORUM'); Cassandra.runQuery(q.query, q.parameters, function(err, config) { if (!err) { Pubsub.publish('oae-config', tenantId + ' config updated'); } callback(err, config); }); };
var setAdmin = module.exports.setAdmin = function(adminType, isAdmin, userId, callback) { // Ensure we're using a real principal id. If we weren't, we would be dangerously upserting an invalid row var validator = new Validator(); validator.check(userId, {'code': 400, 'msg': 'Attempted to update a principal with a non-principal id'}).isPrincipalId(); if (validator.hasErrors()) { return callback(validator.getError()); } Cassandra.runQuery(util.format('UPDATE "Principals" SET "%s" = ? WHERE "principalId" = ?', adminType), [sanitize(isAdmin).toBooleanStrict().toString(), userId], function(err) { if (err) { return callback(err); } return invalidateCachedUsers([userId], callback); }); };
var getConfigFromCassandra = function(tenantId, callback) { var cqlParams = [OAE.serverTenant.alias]; if (tenantId !== OAE.serverTenant.alias) { cqlParams.push(tenantId); } Cassandra.runQuery('SELECT * FROM Config USING CONSISTENCY QUORUM WHERE tenantId IN (?)', [cqlParams], function(err, rows) { if (err) { return callback(err); } getDefaultConfiguration(function(defaultConfig) { callback(false, mergeStoredConfigIntoOriginal(rows, defaultConfig)); }); }); };
var getAllTenants = module.exports.getAllTenants = function(callback) { Cassandra.runQuery('SELECT * FROM Tenant USING CONSISTENCY QUORUM', false, function(err, rows) { if (err) { return callback(err); } var tenants = {}; for (var i = 0; i < rows.length; i++) { var tenant = mapToTenant(rows[i], false); if (!tenant.deleted) { tenants[tenant.alias] = tenant; } } callback(null, tenants); }); };
err => { if (err) { _notifyOfError('AuthenticationUserLoginId', userHash, err, callback); } Cassandra.runQuery( 'INSERT INTO "AuthenticationLoginId" ("loginId", "userId") VALUES (?, ?)', [newLoginId, userId], err => { if (err) { _notifyOfError('AuthenticationLoginId', userHash, err, callback); } log().info('Created Shibboleth login record for user %s', userHash.displayName); return callback(); } ); }