Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/hold audience #512

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
24 changes: 24 additions & 0 deletions rfcs/inactive_users/user_and_organization_meta_update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# User/Organization metadata update
## Overview and Motivation
When user or organization metadata is updated, the Service should track audiences with assigned metadata.
For each assigned meta hash always exists a single `audience`, but there is no list of `audiences` assigned to the user or organization.

To achieve this ability, I advise these updates:

## Audience lists
Audiences stored in sets with names created from `USERS_AUDIENCE` or `ORGANISATION_AUDIENCE` constants and `Id`
(e.g.: `{ms-users}10110110111!audiences`). Both keys contain `audience` names that are currently have assigned values.

The `audience` list will be updated on each update of the metadata.

## Metadata Handling classes
Service logic is updated to use 2 specific classes that will perform all CRUD operations on User or Organization metadata.

* Classes located in: `utils/metadata/{user|organization}.js`.
* Both classes use same [Redis backend](#redis-metadata-backend-class).

## Redis Metadata Backend class
The class performs all work on metadata using Redis DB as a backend.

## Notice
* All User or Organization metadata operations should be performed using Provided classes otherwise, audiences won't be tracked.
22 changes: 7 additions & 15 deletions src/actions/activate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const jwt = require('../utils/jwt.js');
const { getInternalData } = require('../utils/userData');
const getMetadata = require('../utils/get-metadata');
const handlePipeline = require('../utils/pipeline-error');
const setMetadata = require('../utils/update-metadata');
const UserMetadata = require('../utils/metadata/user');

const {
USERS_INDEX,
USERS_DATA,
Expand All @@ -19,7 +20,7 @@ const {
USERS_USERNAME_FIELD,
USERS_ACTION_ACTIVATE,
USERS_ACTIVATED_FIELD,
} = require('../constants.js');
} = require('../constants');

// cache error
const Forbidden = new HttpStatusError(403, 'invalid token');
Expand Down Expand Up @@ -121,19 +122,6 @@ async function activateAccount(data, metadata) {
const userKey = redisKey(userId, USERS_DATA);
const { defaultAudience, service } = this;
const { redis } = service;

// if this goes through, but other async calls fail its ok to repeat that
// adds activation field
await setMetadata.call(service, {
userId,
audience: defaultAudience,
metadata: {
$set: {
[USERS_ACTIVATED_FIELD]: Date.now(),
},
},
});

// WARNING: `persist` is very important, otherwise we will lose user's information in 30 days
// set to active & persist
const pipeline = redis
Expand All @@ -143,6 +131,10 @@ async function activateAccount(data, metadata) {
.persist(userKey)
.sadd(USERS_INDEX, userId);

UserMetadata
.using(userId, defaultAudience, pipeline)
.update(USERS_ACTIVATED_FIELD, Date.now());

if (alias) {
pipeline.sadd(USERS_PUBLIC_INDEX, userId);
}
Expand Down
13 changes: 8 additions & 5 deletions src/actions/alias.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ const isBanned = require('../utils/is-banned');
const DetailedHttpStatusError = require('../utils/detailed-error');
const key = require('../utils/key');
const handlePipeline = require('../utils/pipeline-error');
const UserMetadata = require('../utils/metadata/user');

const {
USERS_DATA,
USERS_METADATA,
USERS_ALIAS_TO_ID,
USERS_ID_FIELD,
USERS_ALIAS_FIELD,
Expand Down Expand Up @@ -71,10 +72,12 @@ async function assignAlias({ params }) {
return Promise.reject(err);
}

const pipeline = redis.pipeline([
['hset', key(userId, USERS_DATA), USERS_ALIAS_FIELD, alias],
['hset', key(userId, USERS_METADATA, defaultAudience), USERS_ALIAS_FIELD, JSON.stringify(alias)],
]);
const pipeline = redis.pipeline();

pipeline.hset(key(userId, USERS_DATA), USERS_ALIAS_FIELD, alias);
UserMetadata
.using(userId, defaultAudience, pipeline)
.update(USERS_ALIAS_FIELD, JSON.stringify(alias));

if (activeUser) {
pipeline.sadd(USERS_PUBLIC_INDEX, username);
Expand Down
37 changes: 22 additions & 15 deletions src/actions/ban.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ const mapValues = require('lodash/mapValues');
const redisKey = require('../utils/key.js');
const { getInternalData } = require('../utils/userData');
const handlePipeline = require('../utils/pipeline-error');
const UserMetadata = require('../utils/metadata/user');

const {
USERS_DATA, USERS_METADATA,
USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA,
USERS_DATA, USERS_BANNED_FLAG, USERS_TOKENS, USERS_BANNED_DATA,
} = require('../constants.js');

// helper
Expand All @@ -25,26 +26,32 @@ function lockUser({
remoteip: remoteip || '',
},
};
const pipeline = redis.pipeline();

pipeline.hset(redisKey(id, USERS_DATA), USERS_BANNED_FLAG, 'true');
// set .banned on metadata for filtering & sorting users by that field
UserMetadata
.using(id, defaultAudience, pipeline)
.updateMulti(mapValues(data, stringify));
pipeline.del(redisKey(id, USERS_TOKENS));

return redis
.pipeline()
.hset(redisKey(id, USERS_DATA), USERS_BANNED_FLAG, 'true')
// set .banned on metadata for filtering & sorting users by that field
.hmset(redisKey(id, USERS_METADATA, defaultAudience), mapValues(data, stringify))
.del(redisKey(id, USERS_TOKENS))
.exec();
return pipeline.exec();
}

function unlockUser({ id }) {
const { redis, config } = this;
const { jwt: { defaultAudience } } = config;
const pipeline = redis.pipeline();

return redis
.pipeline()
.hdel(redisKey(id, USERS_DATA), USERS_BANNED_FLAG)
// remove .banned on metadata for filtering & sorting users by that field
.hdel(redisKey(id, USERS_METADATA, defaultAudience), 'banned', USERS_BANNED_DATA)
.exec();
pipeline.hdel(redisKey(id, USERS_DATA), USERS_BANNED_FLAG);
// remove .banned on metadata for filtering & sorting users by that field
UserMetadata
.using(id, defaultAudience, pipeline)
.delete([
'banned',
USERS_BANNED_DATA,
]);
return pipeline.exec();
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/actions/organization/delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const snakeCase = require('lodash/snakeCase');
const redisKey = require('../../utils/key');
const handlePipeline = require('../../utils/pipeline-error');
const { checkOrganizationExists, getInternalData } = require('../../utils/organization');
const OrganizationMetadata = require('../../utils/metadata/organization');
const {
ORGANIZATIONS_DATA,
ORGANIZATIONS_METADATA,
Expand Down Expand Up @@ -32,11 +33,15 @@ async function deleteOrganization({ params }) {
const organizationMembersListKey = redisKey(organizationId, ORGANIZATIONS_MEMBERS);
const organizationMembersIds = await redis.zrange(organizationMembersListKey, 0, -1);
const organization = await getInternalData.call(this, organizationId);
const organizationMetadata = new OrganizationMetadata(redis);

const pipeline = redis.pipeline();

pipeline.del(redisKey(organizationId, ORGANIZATIONS_DATA));
pipeline.del(redisKey(organizationId, ORGANIZATIONS_METADATA, audience));
// delete organization audiences index
pipeline.del(organizationMetadata.audience.getAudienceKey(organizationId));

pipeline.srem(ORGANIZATIONS_INDEX, organizationId);
if (organizationMembersIds) {
organizationMembersIds.forEach((memberId) => {
Expand Down
6 changes: 5 additions & 1 deletion src/actions/organization/members/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { checkOrganizationExists } = require('../../../utils/organization');
const redisKey = require('../../../utils/key');
const handlePipeline = require('../../../utils/pipeline-error');
const getUserId = require('../../../utils/userData/get-user-id');
const UserMetadata = require('../../../utils/metadata/user');
const { ErrorUserNotMember, USERS_METADATA, ORGANIZATIONS_MEMBERS } = require('../../../constants');

/**
Expand Down Expand Up @@ -41,7 +42,10 @@ async function setOrganizationMemberPermission({ params }) {
permissions = JSON.stringify(permissions);

const pipeline = redis.pipeline();
pipeline.hset(memberMetadataKey, organizationId, permissions);

UserMetadata
.using(userId, audience, pipeline)
.update(organizationId, permissions);
pipeline.hset(redisKey(organizationId, ORGANIZATIONS_MEMBERS, userId), 'permissions', permissions);

return pipeline.exec().then(handlePipeline);
Expand Down
5 changes: 4 additions & 1 deletion src/actions/organization/members/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const redisKey = require('../../../utils/key');
const getUserId = require('../../../utils/userData/get-user-id');
const handlePipeline = require('../../../utils/pipeline-error');
const { checkOrganizationExists } = require('../../../utils/organization');
const UserMetadata = require('../../../utils/metadata/user');
const {
ORGANIZATIONS_MEMBERS,
USERS_METADATA,
Expand Down Expand Up @@ -36,7 +37,9 @@ async function removeMember({ params }) {
const pipeline = redis.pipeline();
pipeline.del(memberKey);
pipeline.zrem(redisKey(organizationId, ORGANIZATIONS_MEMBERS), memberKey);
pipeline.hdel(memberMetadataKey, organizationId);
UserMetadata
.using(userId, audience, pipeline)
.delete(organizationId);

return pipeline.exec().then(handlePipeline);
}
Expand Down
16 changes: 8 additions & 8 deletions src/actions/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const reduce = require('lodash/reduce');
const last = require('lodash/last');

// internal deps
const setMetadata = require('../utils/update-metadata');
const UserMetadata = require('../utils/metadata/user');
const redisKey = require('../utils/key');
const jwt = require('../utils/jwt');
const isDisposable = require('../utils/is-disposable');
Expand Down Expand Up @@ -231,13 +231,13 @@ async function performRegistration({ service, params }) {
commonMeta[USERS_ACTIVATED_FIELD] = Date.now();
}

await setMetadata.call(service, {
userId,
audience,
metadata: audience.map((metaAudience) => ({
$set: Object.assign(metadata[metaAudience] || {}, metaAudience === defaultAudience && commonMeta),
})),
});
await UserMetadata
.using(userId, audience, service.redis)
.batchUpdate({
metadata: audience.map((metaAudience) => ({
$set: Object.assign(metadata[metaAudience] || {}, metaAudience === defaultAudience && commonMeta),
})),
});

// assign alias
if (alias) {
Expand Down
7 changes: 6 additions & 1 deletion src/actions/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const key = require('../utils/key');
const { getInternalData } = require('../utils/userData');
const getMetadata = require('../utils/get-metadata');
const handlePipeline = require('../utils/pipeline-error');
const UserMetadata = require('../utils/metadata/user');
const {
USERS_INDEX,
USERS_PUBLIC_INDEX,
Expand Down Expand Up @@ -92,6 +93,8 @@ async function removeUser({ params }) {
const alias = internal[USERS_ALIAS_FIELD];
const userId = internal[USERS_ID_FIELD];
const resolvedUsername = internal[USERS_USERNAME_FIELD];
const metaAudiences = await UserMetadata.using(userId, null, redis).getAudience();
const userMetadata = UserMetadata.using(userId, null, transaction);

if (alias) {
transaction.hdel(USERS_ALIAS_TO_ID, alias.toLowerCase(), alias);
Expand All @@ -114,7 +117,9 @@ async function removeUser({ params }) {

// remove metadata & internal data
transaction.del(key(userId, USERS_DATA));
transaction.del(key(userId, USERS_METADATA, audience));
for (const metaAudience of metaAudiences) {
userMetadata.deleteMetadata(metaAudience);
}

// remove auth tokens
transaction.del(key(userId, USERS_TOKENS));
Expand Down
16 changes: 9 additions & 7 deletions src/actions/updateMetadata.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const omit = require('lodash/omit');
const Promise = require('bluebird');
const updateMetadata = require('../utils/update-metadata');
const UserMetadata = require('../utils/metadata/user');
const { getUserId } = require('../utils/userData');

/**
Expand All @@ -19,12 +18,15 @@ const { getUserId } = require('../utils/userData');
* @apiParam (Payload) {Object} [script] - if present will be called with passed metadata keys & username, provides direct scripting access.
* Be careful with granting access to this function.
*/
module.exports = function updateMetadataAction(request) {
return Promise
module.exports = async function updateMetadataAction(request) {
const { username: _, audience, ...updateParams } = request.params;
const userId = await Promise
.bind(this, request.params.username)
.then(getUserId)
.then((userId) => ({ ...omit(request.params, 'username'), userId }))
.then(updateMetadata);
.then(getUserId);

return UserMetadata
.using(userId, audience, this.redis)
.batchUpdate(updateParams);
};

module.exports.transports = [require('@microfleet/core').ActionTransport.amqp];
29 changes: 15 additions & 14 deletions src/auth/oauth/utils/attach.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
const get = require('lodash/get');
const redisKey = require('../../../utils/key');
const updateMetadata = require('../../../utils/update-metadata');
const UserMetadata = require('../../../utils/metadata/user');
const handlePipeline = require('../../../utils/pipeline-error');
const {
USERS_SSO_TO_ID,
USERS_DATA,
} = require('../../../constants');

module.exports = function attach(account, user) {
module.exports = async function attach(account, user) {
const { redis, config } = this;
const { id: userId } = user;
const {
Expand All @@ -23,17 +23,18 @@ module.exports = function attach(account, user) {
// link uid to user id
pipeline.hset(USERS_SSO_TO_ID, uid, userId);

return pipeline.exec().then(handlePipeline)
.bind(this)
.return({
userId,
audience,
metadata: {
$set: {
[provider]: profile,
},
handlePipeline(await pipeline.exec());

const updateParams = {
metadata: {
$set: {
[provider]: profile,
},
})
.then(updateMetadata)
.return(profile);
},
};
await UserMetadata
.using(userId, audience, redis)
.batchUpdate(updateParams);

return profile;
};
12 changes: 7 additions & 5 deletions src/auth/oauth/utils/detach.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const Errors = require('common-errors');

const get = require('../../../utils/get-value');
const redisKey = require('../../../utils/key');
const updateMetadata = require('../../../utils/update-metadata');
const UserMetadata = require('../../../utils/metadata/user');
const handlePipeline = require('../../../utils/pipeline-error');

const {
Expand Down Expand Up @@ -30,13 +30,15 @@ module.exports = async function detach(provider, userData) {

handlePipeline(await pipeline.exec());

return updateMetadata.call(this, {
userId,
audience,
const updateParams = {
metadata: {
$remove: [
provider,
],
},
});
};

return UserMetadata
.using(userId, audience, redis)
.batchUpdate(updateParams);
};
Loading