Skip to content

Commit 0ad48fa

Browse files
committed
feat: delete inactivated users
* organization action tokens remove * user action tokens remove
1 parent 9309387 commit 0ad48fa

5 files changed

Lines changed: 96 additions & 52 deletions

File tree

rfcs/inactive_users_cleanup.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ Every Activation and Registration request event executes `users:cleanup` hook.
2222
The Activation request executes the hook first this strategy saves from inactive
2323
users that hit TTL but tried to pass the activation process.
2424
The Registration request executes the hook after `username` exists check.
25-
If Error happens inside hook, it logged into a logfile and not raised.
2625

2726
## Registration process
2827
When the user succeeds registration but activation not requested, the new entry added to `inactive-users`.
@@ -34,6 +33,7 @@ When the user succeeds activation `userId`,the entry deleted from `inactive-user
3433
## `users:cleanup` hook `cleanUsers(suppress?)`
3534
`suppress` parameter defines function error behavior. If parameter set, the function throws errors,
3635
otherwise, function calls `log.error` with `error.message` as message.
36+
Default value is `true`. IMHO User shouldn't know about our problems.
3737

3838
Other option, is to define new config parameter as object and move `config.deleteInactiveAccounts` into it:
3939
```javascript
@@ -45,11 +45,14 @@ const conf = {
4545
}
4646
```
4747
Calls `deleteInactivatedUsers` script with TTL parameter from `service.config.deleteInactiveAccounts`.
48+
When script finished, calls TokenManager to delete Action tokens(`USER_ACTION_*`, ``).
49+
*NOTE*: Should we update TokenManager to add support of pipeline?
4850

4951
## Redis Delete Inactive User Script
5052
When the service connects to the Redis server and fires event "plugin:connect:$redisType" `utils/inactive/defineCommand.js` executed.
5153
Function rendering `deleteInactivatedUsers.lua.hbs` and evals resulting script into IORedis.
5254
The Script using a dozen constants, keys, and templates, so all these values rendered inside of the template using template context.
55+
Returns list of deleted users.
5356

5457
#### deleteInactivatedUsers `USERS_ACTIVATED` `TTL` as seconds
5558
##### Script paramerters:
@@ -71,5 +74,4 @@ Using the username, id, alias and SSO providers fields, script checks and remove
7174
* User tokens.
7275
* Private and public id indexes
7376
* Links and data used in Organization assignment
74-
* Throttle actions (???). **THROTTLE_PREFIX constant doesn't exist in constants.js, so assuming this left for backward
75-
compatibility with previous version**
77+

scripts/deleteInactivatedUsers.lua.hbs

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
local usersInactiveKey = KEYS[1]
22
local exTime = ARGV[1]
3-
4-
-- #var defs
3+
---
4+
-- var defs
5+
---
56
local delimiter = '{{ KEY_SEPARATOR }}'
67
local keyPrefix = '{{ keyPrefix }}'
78

@@ -11,6 +12,7 @@ local ssoProviders = {
1112
{{/each}}
1213
};
1314

15+
-- key templates
1416
local usersDataKeyTemplate = '{{ keyTemplates.USERS_DATA }}'
1517
local usersMetaKeyTemplate = '{{ keyTemplates.USERS_METADATA }}'
1618
local usersTokenKeyTemplate = '{{ keyTemplates.USERS_TOKENS }}'
@@ -21,23 +23,23 @@ local organizationsMembersKeyTemplate = '{{ keyTemplates.ORGANIZATIONS_MEMBERS }
2123
local organizationsMemberKeyTemplate = '{{ keyTemplates.ORGANIZATIONS_MEMBER }}'
2224
local organizationMemberTemplate = '{{ templates.ORGANIZATIONS_MEMBER }}'
2325

26+
-- simple keys
2427
local usersAliasToIDKey = '{{ keys.USERS_ALIAS_TO_ID }}'
2528
local usersUsernameToIDKey = '{{ keys.USERS_USERNAME_TO_ID }}'
2629
local usersSSOToIDKey = '{{ keys.USERS_SSO_TO_ID }}'
2730

31+
-- indexes
2832
local organizationsInvitationIndex = '{{ keys.ORGANIZATIONS_INVITATIONS_INDEX }}'
29-
3033
local usersPublicIndex = '{{ keys.USERS_PUBLIC_INDEX }}'
3134
local usersIndex = '{{ keys.USERS_INDEX }}'
3235

33-
local usersThrottleKeyPrefix = '{{ keys.THROTTLE_PREFIX }}'
34-
36+
-- fields
3537
local usersUsernameField = '{{ fields.USERS_USERNAME_FIELD }}'
3638
local usersAliasField = '{{ fields.USERS_ALIAS_FIELD }}'
37-
-- /var defs
39+
3840

3941
---
40-
--- Helper functions
42+
-- Helper functions
4143
---
4244
local function isempty(s)
4345
return s == nil or s == '' or s == false;
@@ -52,11 +54,12 @@ local function decode(strval)
5254
end
5355
end
5456

55-
-- key generators
57+
-- key generator
5658
local function key(...)
5759
return table.concat(arg, delimiter)
5860
end
5961

62+
-- key from template generator
6063
local function makeKey(template, templateValues)
6164
local str = template
6265
for param, value in pairs(templateValues) do
@@ -65,14 +68,14 @@ local function makeKey(template, templateValues)
6568
return str
6669
end
6770

68-
71+
-- gets user data
6972
local function getData(key)
7073
local fields = { usersUsernameField, usersAliasField, unpack(ssoProviders) }
7174
local data = redis.call("HMGET", key, unpack(fields))
7275

7376
if #data > 0 then
7477
local result = {};
75-
--convert into table
78+
--convert to table
7679
for i = 1, #data, 1 do
7780
result[fields[i]] = data[i]
7881
end
@@ -82,6 +85,11 @@ local function getData(key)
8285
return nil
8386
end
8487

88+
---
89+
-- Script logic functions
90+
---
91+
92+
-- deletes organization bindings
8593
local function deleteOrganizationMember(username)
8694
local userOrganizationsKey = makeKey(usersOrganizationsKeyTemplate, { username = username })
8795
local organizationIds = redis.call("HKEYS", userOrganizationsKey)
@@ -100,21 +108,28 @@ local function deleteOrganizationMember(username)
100108

101109
end
102110

111+
-- handles emails (usernames)
112+
local inactiveUserNames = {}
113+
103114
-- delete logic
104115
local function deleteUser(userID, userData)
105116
local alias = userData[usersAliasField]
106117
local username = userData[usersUsernameField]
107118

119+
-- save username
120+
table.insert(inactiveUserNames, username)
121+
122+
-- delete alias
108123
if isempty(alias) == false then
109124
redis.call("HDEL", usersAliasToIDKey, alias, string.lower(alias))
110125
end
111126

112-
--almost impossible but
113-
if isempty(username) == false then
114-
redis.call("HDEL", usersUsernameToIDKey, username)
115-
deleteOrganizationMember(username)
116-
end
127+
redis.call("HDEL", usersUsernameToIDKey, username)
128+
129+
-- if user assigned to organization
130+
deleteOrganizationMember(username)
117131

132+
-- delete SSO data
118133
for k, provider in pairs(ssoProviders) do
119134
local rawData = userData[provider]
120135
local providerData = decode(rawData)
@@ -131,7 +146,7 @@ local function deleteUser(userID, userData)
131146
local userDataKey = makeKey(usersDataKeyTemplate, { id = userID })
132147
redis.call("DEL", userDataKey)
133148

134-
-- delete meta
149+
-- delete meta data
135150
local usersAudienceKey = makeKey(usersAudienceKeyTemplate, { id = userID })
136151
local userAudiences = redis.call("SMEMBERS", usersAudienceKey)
137152

@@ -144,16 +159,15 @@ local function deleteUser(userID, userData)
144159
local userTokensKey = makeKey(usersTokenKeyTemplate, { id = userID })
145160
redis.call("DEL", userTokensKey)
146161

162+
-- delete user audiences list
147163
redis.call("DEL", usersAudienceKey)
148164

149-
-- throttle constants
150-
{{#each throttle_actions}}
151-
redis.call("DEL", key(userThrottleKeyPrefix, {{this}}, userID))
152-
{{/each}}
153-
154165
end
155166

156-
-- Command body
167+
---
168+
-- Script Logic
169+
---
170+
157171
redis.replicate_commands();
158172

159173
local inactiveUsers = redis.call("ZRANGEBYSCORE", usersInactiveKey, '-inf', exTime)
@@ -171,5 +185,6 @@ for _ , userID in pairs(inactiveUsers) do
171185
redis.call('ZREM', usersInactiveKey, userID)
172186
end
173187

174-
-- returns count of deleted users
175-
return #inactiveUsers
188+
-- returns emails of deleted users
189+
-- helps to remove TokenManager tokens
190+
return inactiveUserNames

src/utils/inactiveUsers/defineCommand.js

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const Promise = require('bluebird');
22
const hbs = require('handlebars');
33
const path = require('path');
44
const fs = require('fs');
5-
const ld = require('lodash');
65

76
const key = require('../key');
87
const {
@@ -19,29 +18,24 @@ const {
1918
USERS_USERNAME_TO_ID,
2019
USERS_USERNAME_FIELD,
2120
SSO_PROVIDERS,
22-
THROTTLE_PREFIX,
23-
USERS_ACTION_ACTIVATE,
24-
USERS_ACTION_REGISTER,
25-
USERS_ACTION_PASSWORD,
26-
USERS_ACTION_RESET,
27-
2821
ORGANIZATIONS_MEMBERS,
2922
ORGANIZATIONS_INVITATIONS_INDEX,
3023
} = require('../../constants');
3124

3225
const templateName = 'deleteInactivatedUsers.lua.hbs';
3326
const KEY_SEPARATOR = '!';
3427

28+
// keys used in script
3529
const keys = {
3630
USERS_ALIAS_TO_ID,
3731
USERS_SSO_TO_ID,
3832
USERS_USERNAME_TO_ID,
3933
USERS_INDEX,
4034
USERS_PUBLIC_INDEX,
41-
THROTTLE_PREFIX,
4235
ORGANIZATIONS_INVITATIONS_INDEX,
4336
};
4437

38+
// key templates used in script
4539
const keyTemplates = {
4640
USERS_DATA: key('{id}', USERS_DATA),
4741
USERS_METADATA: key('{id}', USERS_METADATA, '{audience}'),
@@ -52,31 +46,28 @@ const keyTemplates = {
5246
ORGANIZATIONS_MEMBER: key('{orgid}', ORGANIZATIONS_MEMBERS, '{username}'),
5347
};
5448

49+
// data template, organization stores members without prefix
5550
const templates = {
5651
ORGANIZATIONS_MEMBER: key('{orgid}', ORGANIZATIONS_MEMBERS, '{username}'),
5752
};
5853

54+
// fields used in script
5955
const fields = {
6056
USERS_ALIAS_FIELD,
6157
USERS_USERNAME_FIELD,
6258
};
63-
const throttleActions = [
64-
USERS_ACTION_ACTIVATE,
65-
USERS_ACTION_PASSWORD,
66-
USERS_ACTION_REGISTER,
67-
USERS_ACTION_RESET,
68-
];
69-
7059

7160
const readFile = f => Promise.fromCallback((cb) => {
7261
return fs.readFile(f, 'utf-8', cb);
7362
});
7463

7564
const prefixify = (prefix, obj) => {
7665
if (prefix !== '') {
77-
return ld.mapValues(obj, (value) => {
78-
return `${prefix}${value}`;
79-
});
66+
const objEntries = Object.entries(obj);
67+
68+
for (const [prop, value] of objEntries) {
69+
obj[prop] = `${prefix}${value}`;
70+
}
8071
}
8172
return obj;
8273
};
@@ -109,7 +100,6 @@ function templateContext(redisOptions) {
109100
return {
110101
KEY_SEPARATOR,
111102
fields,
112-
throttleActions,
113103
templates,
114104
keys: prefixify(keyPrefix, keys),
115105
keyTemplates: prefixify(keyPrefix, keyTemplates),

src/utils/inactiveUsers/index.js

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
const { USERS_INACTIVATED } = require('../../constants');
1+
const Promise = require('bluebird');
22

3+
const {
4+
USERS_INACTIVATED,
5+
USERS_ACTION_ACTIVATE,
6+
USERS_ACTION_REGISTER,
7+
USERS_ACTION_PASSWORD,
8+
USERS_ACTION_RESET,
9+
USERS_ACTION_ORGANIZATION_INVITE,
10+
USERS_ACTION_ORGANIZATION_REGISTER,
11+
} = require('../../constants');
312
const deleteInactiveUsers = require('./delete');
413
const defineCommand = require('./defineCommand');
514

@@ -22,18 +31,44 @@ function removeFromInactiveUsers(redis, userId) {
2231
redis.zrem(USERS_INACTIVATED, userId);
2332
}
2433

34+
/**
35+
* Deletes invites and action tokens for provided user list
36+
* @param userIds
37+
* @returns {Promise<[*]>}
38+
*/
39+
function cleanTokens(userIds) {
40+
const actions = [
41+
USERS_ACTION_ACTIVATE, USERS_ACTION_REGISTER,
42+
USERS_ACTION_PASSWORD, USERS_ACTION_RESET,
43+
USERS_ACTION_ORGANIZATION_INVITE, USERS_ACTION_ORGANIZATION_REGISTER,
44+
];
45+
46+
const work = userIds.reduce((prev, id) => {
47+
const delTokenActions = [];
48+
for (const action of actions) {
49+
delTokenActions.push(
50+
this.tokenManager.remove({ id, action })
51+
);
52+
}
53+
return [...prev, ...delTokenActions];
54+
}, []);
55+
56+
return Promise.all(work);
57+
}
58+
2559
/**
2660
* Deletes users that didn't pass activation
2761
* @param suppressError boolean throw or suppress error
28-
* @returns {Promise<number>}
62+
* @returns {Promise<[]>}
2963
*/
3064
async function cleanUsers(suppressError = true) {
3165
const { redis } = this;
3266
const { deleteInactiveAccounts } = this.config;
33-
let deletedUsers = 0;
67+
let deletedUsers = [];
3468

3569
try {
3670
deletedUsers = await deleteInactiveUsers(redis, deleteInactiveAccounts);
71+
await cleanTokens.call(this, deletedUsers);
3772
} catch (e) {
3873
if (suppressError) {
3974
this.log.error({ error: e }, e.message);

test/suites/deleteInactivatedUsers.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,11 @@ describe('#inactive user', function registerSuite() {
3939

4040
beforeEach(async function pretest() {
4141
this.users.config.deleteInactiveAccounts = 1;
42+
const dispatcher = simpleDispatcher(this.users.router);
43+
4244
await createOrganization.call(this);
43-
await simpleDispatcher(this.users.router)('users.register', { ...regUser });
44-
return simpleDispatcher(this.users.router)('users.register', { ...regUserNoAlias });
45+
await dispatcher('users.register', { ...regUser });
46+
return dispatcher('users.register', { ...regUserNoAlias });
4547
});
4648

4749
describe('throw suppress error', function test() {
@@ -77,7 +79,7 @@ describe('#inactive user', function registerSuite() {
7779
return delay(() => {
7880
return cleanUsers.call(this.users)
7981
.then(async (res) => {
80-
expect(res).to.be.eq(2);
82+
expect(res.length).to.be.eq(2);
8183

8284
const { username } = regUser;
8385
const dispatcher = simpleDispatcher(this.users.router);

0 commit comments

Comments
 (0)