Skip to content

Commit

Permalink
2.14.4 - Hotfix unsubscribe from Embedded SDK (#2144)
Browse files Browse the repository at this point in the history
When unsubscribing, Kuzzle trigger an event containing (among other information) the `kuid` of the user who made the subscription.
The EmbeddedSDK is not necessarily using an user to execute requests. This case is now handled

### Other changes

  - Convert related files to typescript
  • Loading branch information
Aschen authored Sep 21, 2021
1 parent 355e012 commit 63407f8
Show file tree
Hide file tree
Showing 13 changed files with 159 additions and 109 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ lib/util/mutex.js
lib/util/inflector.js
lib/util/koncordeCompat.js
features/support/application/functional-tests-app.js
lib/model/security/token.js
lib/core/auth/tokenManager.js
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ lib/util/koncordeCompat.js
features-sdk/support/application/functional-tests-app.js
docker/scripts/functional-tests-controller.js
docker/scripts/start-kuzzle-dev.js
lib/model/security/token.js
lib/core/auth/tokenManager.js
188 changes: 106 additions & 82 deletions lib/core/auth/tokenManager.js → lib/core/auth/tokenManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,46 @@
* limitations under the License.
*/

'use strict';
import SortedArray from 'sorted-array';

const SortedArray = require('sorted-array');
import '../../types/Global';
import { Token } from '../../model/security/token';

const Token = require('../../model/security/token');
interface ISortedArray<T> {
array: T[];

search (item: any): number;

insert (item: T): void;
}

/**
* Extends the Token model with a set of linked connection IDs.
*/
class ManagedToken extends Token {
/**
* Unique string to identify the token and sort it by expiration date
*/
idx: string;

/**
* Set of connection ID that use this token.
*/
connectionIds: Set<string>;

constructor (token: Token, connectionIds: Set<string>) {
super(token);

this.connectionIds = connectionIds;
}

/**
* Returns an unique string that identify a token and allows to sort them by expiration date.
*/
static indexFor (token: Token) {
return `${token.expiresAt};${token._id}`;
}
}

/*
Maximum delay of a setTimeout call. If larger than this value,
Expand All @@ -36,16 +71,18 @@ const Token = require('../../model/security/token');
const TIMEOUT_MAX = Math.pow(2, 31) - 1;

/**
* Maintains a list of valid tokens used by real-time subscriptions
* Maintains a list of valid tokens used by connected protocols.
*
* When a token expires, this module cleans up the corresponding connection's
* subscriptions, and notify the user
*
* @class TokenManager
* subscriptions if any, and notify the user
*/
class TokenManager {
constructor () {
this.anonymousUserId = null;
export class TokenManager {
private tokens: ISortedArray<ManagedToken>;
private anonymousUserId: string = null;
private tokensByConnection = new Map<string, ManagedToken>();
private timer: NodeJS.Timeout = null;

constructor () {
/*
* Tokens are sorted by their expiration date
*
Expand Down Expand Up @@ -74,9 +111,6 @@ class TokenManager {

return a.idx < b.idx ? -1 : 1;
});
this.tokensByConnection = new Map();

this.timer = null;
}

async init () {
Expand All @@ -99,56 +133,57 @@ class TokenManager {
/**
* Link a connection and a token.
* If one or another expires, associated subscriptions are cleaned up
* @param {Token} token
* @param {String} connectionId
* @param token
* @param connectionId
*/
link (token, connectionId) {
link (token: Token, connectionId: string) {
// Embedded SDK does not use tokens
if (! token || token._id === this.anonymousUserId) {
return;
}

const idx = getTokenIndex(token);
const idx = ManagedToken.indexFor(token);
const currentToken = this.tokensByConnection.get(connectionId);

if (currentToken) {
if (currentToken._id === token._id) {
return; // Connection and Token already linked
}
this._removeConnectionLinkedToToken(connectionId, currentToken);
this.removeConnectionLinkedToToken(connectionId, currentToken);
}
const pos = this.tokens.search({idx});
const pos = this.tokens.search({ idx });

if (pos === -1) {
this._add(token, [connectionId]);
this.add(token, new Set([connectionId]));
}
else {
const data = this.tokens.array[pos];
data.connectionIds.add(connectionId);
this.tokensByConnection.set(connectionId, data);
const managedToken = this.tokens.array[pos];
managedToken.connectionIds.add(connectionId);

this.tokensByConnection.set(connectionId, managedToken);
}
}

/**
* Unlink a connection from its associated token
*
* @param {Token} token
* @param {String} connectionId
* @param token
* @param connectionId
*/
unlink (token, connectionId) {
unlink (token: Token, connectionId: string) {
// Embedded SDK does not use tokens
if (! token || token._id === this.anonymousUserId) {
return;
}

const idx = getTokenIndex(token);
const idx = ManagedToken.indexFor(token);
const pos = this.tokens.search({ idx });

if (pos === -1) {
return;
}

this._removeConnectionLinkedToToken(connectionId, this.tokens.array[pos]);
this.removeConnectionLinkedToToken(connectionId, this.tokens.array[pos]);

const currentToken = this.tokensByConnection.get(connectionId);
if (currentToken && currentToken._id === token._id) {
Expand All @@ -164,36 +199,35 @@ class TokenManager {
*
* @param token
*/
async expire (token) {
async expire (token: Token) {
if (token._id === this.anonymousUserId) {
return;
}

const idx = getTokenIndex(token);
const idx = ManagedToken.indexFor(token);
const searchResult = this.tokens.search({idx});

if (searchResult > -1) {
const data = this.tokens.array[searchResult];
const managedToken = this.tokens.array[searchResult];

for (const connectionId of data.connectionIds) {
for (const connectionId of managedToken.connectionIds) {
this.tokensByConnection.delete(connectionId);
await global.kuzzle.ask('core:realtime:user:remove', connectionId);
}

this._deleteByIndex(searchResult);
this.deleteByIndex(searchResult);
}
}

/**
* Refresh an existing token with a new one
*
* @param {Token} oldToken
* @param {Token} newToken
* @param oldToken
* @param newToken
*/
refresh (oldToken, newToken) {
const
oldIndex = getTokenIndex(oldToken),
pos = this.tokens.search({idx: oldIndex});
refresh (oldToken: Token, newToken: Token) {
const oldIndex = ManagedToken.indexFor(oldToken);
const pos = this.tokens.search({ idx: oldIndex });

// If the old token has been created and then refreshed within the same
// second, then it has the exact same characteristics than the new one.
Expand All @@ -205,14 +239,14 @@ class TokenManager {
if (pos > -1 && oldToken._id !== newToken._id) {
const connectionIds = this.tokens.array[pos].connectionIds;

this._add(newToken, connectionIds);
this.add(newToken, connectionIds);

// Delete old token
this._deleteByIndex(pos);
this.deleteByIndex(pos);
}
}

async checkTokensValidity() {
async checkTokensValidity () {
const arr = this.tokens.array;

// API key can never expire (-1)
Expand All @@ -224,7 +258,9 @@ class TokenManager {
for (const connectionId of connectionIds) {
await global.kuzzle.ask('core:realtime:tokenExpired:notify', connectionId);
}

setImmediate(() => this.checkTokensValidity());

return;
}

Expand All @@ -235,76 +271,64 @@ class TokenManager {

/**
* Gets the token matching user & connection if any
*
* @param {string} userId
* @param {string} connectionId
* @returns {Token}
*/
getConnectedUserToken(userId, connectionId) {
const data = this.tokensByConnection.get(connectionId);
getConnectedUserToken (userId: string, connectionId: string): Token | null {
const token = this.tokensByConnection.get(connectionId);

return data && data.userId === userId
? new Token({...data, connectionId})
: null;
return token && token.userId === userId ? token : null;
}

/**
* Returns the token associated to a connection
* Returns the kuid associated to a connection
*/
getToken (connectionId) {
return this.tokensByConnection.get(connectionId);
getKuidFromConnection (connectionId: string): string | null {
const token = this.tokensByConnection.get(connectionId);

if (! token) {
return null;
}

return token.userId;
}

/**
* Adds token to internal collections
*
* @param {Token} token
* @param {string} connectionId
* @private
* @param token
* @param connectionId
*/
_add(token, connectionIds) {
const data = Object.assign({}, token, {
private add (token: Token, connectionIds: Set<string>) {
const orderedToken = Object.assign({}, token, {
connectionIds: new Set(connectionIds),
idx: getTokenIndex(token)
idx: ManagedToken.indexFor(token)
});

for (const connectionId of connectionIds) {
this.tokensByConnection.set(connectionId, data);
this.tokensByConnection.set(connectionId, orderedToken);
}
this.tokens.insert(data);
this.tokens.insert(orderedToken);

if (this.tokens.array[0].idx === data.idx) {
if (this.tokens.array[0].idx === orderedToken.idx) {
this.runTimer();
}
}

_removeConnectionLinkedToToken(connectionId, token) {
token.connectionIds.delete(connectionId);
private removeConnectionLinkedToToken (connectionId: string, managedToken: ManagedToken) {
managedToken.connectionIds.delete(connectionId);

if (token.connectionIds.size === 0) {
const pos = this.tokens.search({ idx: token.idx });
this._deleteByIndex(pos);
if (managedToken.connectionIds.size === 0) {
const pos = this.tokens.search({ idx: managedToken.idx });
this.deleteByIndex(pos);
}
}

_deleteByIndex(index) {
const data = this.tokens.array[index];
private deleteByIndex (index: number) {
const orderedToken = this.tokens.array[index];

if (!data) {
if (! orderedToken) {
return;
}

this.tokens.array.splice(index, 1);
}
}

/**
* Calculate a simple sortable token index
* @param token
* @returns {string}
*/
function getTokenIndex(token) {
return `${token.expiresAt};${token._id}`;
}

module.exports = TokenManager;
4 changes: 2 additions & 2 deletions lib/core/realtime/hotelClerk.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,15 @@ class HotelClerk {
});
}

const { userId } = global.kuzzle.tokenManager.getToken(connectionId);
const kuid = global.kuzzle.tokenManager.getKuidFromConnection(connectionId);

const subscription = new Subscription(
room.index,
room.collection,
undefined,
roomId,
connectionId,
{ _id: userId });
{ _id: kuid });

global.kuzzle.emit('core:realtime:user:unsubscribe:after', {
/* @deprecated */
Expand Down
2 changes: 1 addition & 1 deletion lib/core/security/tokenRepository.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const Bluebird = require('bluebird');

const ApiKey = require('../../model/storage/apiKey');
const { UnauthorizedError } = require('../../kerror/errors');
const Token = require('../../model/security/token');
const { Token } = require('../../model/security/token');
const Repository = require('../shared/repository');
const kerror = require('../../kerror');
const debug = require('../../util/debug')('kuzzle:bootstrap:tokens');
Expand Down
2 changes: 1 addition & 1 deletion lib/kuzzle/kuzzle.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const PassportWrapper = require('../core/auth/passportWrapper');
const PluginsManager = require('../core/plugin/pluginsManager');
const Router = require('../core/network/router');
const Statistics = require('../core/statistics');
const TokenManager = require('../core/auth/tokenManager');
const { TokenManager } = require('../core/auth/tokenManager');
const Validation = require('../core/validation');
const Logger = require('./log');
const vault = require('./vault');
Expand Down
Loading

0 comments on commit 63407f8

Please sign in to comment.