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

Loose coupling: security module #1630

Merged
merged 79 commits into from
Jul 7, 2020
Merged

Loose coupling: security module #1630

merged 79 commits into from
Jul 7, 2020

Conversation

scottinet
Copy link
Contributor

@scottinet scottinet commented May 16, 2020

Description

Loosely couple the security module to any other objects.

Also makes the security module entirely responsible for managing its stored documents and cache:

  • all cache changes (storage or invalidation) are now made by the security module itself, by listening to the relevant Kuzzle events.
  • metadata changes in security documents are handled by the security module. They were previously handled by API methods, and there were a few omissions because of that
  • implementation details on repository objects are now handled by the security module objects, instead of being handled by API methods (expect a much lighter and more readable code, especially in the security controller)
  • Exceptions related to security documents (e.g. security.[role|profile|user].not_found) are now handled by the security module, preventing duplicated code in API methods

Changes / Fixes related to security API methods

  • When finding a user without profiles associated to it, we "autofixed" it by associating the restricted profiles to it. I consider this a security issue, as there is no telling from where this user document comes from. If that happens, this is either a Kuzzle bug (shouldn't be the case though), a database manipulation gone wrong, or a malicious user document insertion. Either way, we should refuse to load the user and throw an error instead of trying to "make things better"
  • The body.content argument is no longer required for security:createRestrictedUser: it's required (for now) for security:createUser because we need the profileIds argument in it, but createRestrictedUser forbids this argument... and the remaining user content is completely optional, so the whole object itself should be so as well
  • security:updateUser, security:updateProfile, security:updateRole: the retryOnConflict option was accepted but undocumented (fixed). Also, the default value for retryOnConflict has been set to 10 (instead of 0) to prevent common support tickets
  • security:createFirstAdmin:
    • (fix) the error thrown when an admin user already exists didn't have an error id, and wasn't a KuzzleError either
    • (fix) that API method used hardcoded content to reset profiles, instead of the configuration stored in kuzzlerc (this means that profiles loaded that way didn't have rate limits, contrary to the ones defined in our default configuration file)
    • For the same reasons than for security:createRestrictedUser, the content argument is no longer required
    • Now rejects if a profileIds property is found in the body content (not a fix: previous versions of kuzzle silently overwrite this property)
  • auth:updateSelf now accepts the following options: refresh, retryOnConflict (behavior harmonized with security:updateUser)
  • (fix) security:mGetRoles now throws an exception if there is at least 1 role that doesn't exist. Before, empty objects were returned for non existing roles (this made a functional test return a false-positive success on an erroneous argument provided to that route)
  • remove the now unused funnel:mExecute method: security:m* were the only ones using it. It's ugly and inefficient: now security repositories handle m* methods themselves, without duplicating API requests
  • security:search[Roles|Profiles|Users]: the size option is now, by default, set to the maximum number of documents that a single request can fetch (taken from the kuzzlerc configuration)
  • security user creation (createUser, createRestrictedUser, createFirstAdmin): fix the rollback sequence when a user has multiple strategies (already created strategy credentials weren't rollbacked)
  • truncating roles, profiles and users now also clear the RAM cache (even if it fails, because it might have deleted a few objects before the failure occurs)
  • (fix) fix redundant (because erroneous) unit tests on roleRepository methods checkRoleNativeRights and checkRolePluginsRights
  • Process the content and profileIds arguments separately in the core:security:user API, laying the groundwork for future Kuzzle API changes, and to store user documents differently / in a safer way.

Other changes

  • Asyncify functions whenever possible
  • Ask events without answerers now throw a core.fatal.assertion_failed exception. This change was made because otherwise, debugging simple typos in event names is a nightmare 😑
  • Make the security controller use the NativeController secure getters instead of methods in util/requestAssertions as much as possible. It should soon be possible to get rid of requestAssertions (will need to move a few methods in NativeController beforehand though)
  • Modify the getId method in the NativeController class:
    • add a ifMissing option: error (default: throws if id is missing), generate (returns a UUIDv4 id if missing), ignore (returns null if missing)
  • The option resetCache has been removed from roleRepository.loadRoles and profileRepository.loadProfiles. It was only used by admin:resetSecurity to preload reinitialized roles and security, and it has been replaced by a global cache invalidation, letting the security module lazily load resetted roles/profiles when they are requested

vidiben and others added 30 commits April 28, 2020 17:02
@scottinet scottinet marked this pull request as ready for review June 30, 2020 15:15
Copy link
Contributor

@Aschen Aschen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few minor things I spotted, otherwise LGTM 👍

@@ -67,6 +69,11 @@ class TokenManager {
this.timer = null;
}

async init () {
const anonymous = this.kuzzle.ask('core:security:user:anonymous');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing await?

Copy link
Contributor Author

@scottinet scottinet Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's there 🤔
You might be reviewing an older version, I caught this one recently when I completed the unit tests suite.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible because I started this review a long time ago 😅

apiKeyId = request.input.resource._id || null,
description = this.getBodyString(request, 'description');
const expiresIn = request.input.args.expiresIn || -1;
const refresh = this.getRefresh(request);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const refresh = this.getRefresh(request);
const refresh = this.getRefresh(request, 'wait_for');

null,
body,
{
refresh: this.getRefresh(request),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be wait_for by default

Suggested change
refresh: this.getRefresh(request),
refresh: this.getRefresh(request, 'wait_for'),

const id = request.input.resource._id;
const content = this.getBody(request);
async create (request) {
const id = this.getId(request, ifMissingEnum.IGNORE);
const userId = this.getUserId(request);
const refresh = this.getString(request, 'refresh', 'false');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const refresh = this.getString(request, 'refresh', 'false');
const refresh = this.getRefresh(request);

Comment on lines 515 to 517
const opts = Object.assign({}, strategy.config.strategyOptions, {
passReqToCallback: true
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking

Suggested change
const opts = Object.assign({}, strategy.config.strategyOptions, {
passReqToCallback: true
});
const opts = { ...strategy.config.strategyOptions, passReqToCallback: true };

promises.push(this.kuzzle.funnel.processRequest(request));
}

await Promise.all(promises);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bluebird is much faster for // execution

Suggested change
await Promise.all(promises);
await Bluebird.all(promises);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed for the sake of uniformity, but performances are not a concern in the security loader, which should be at most called 1 time during the life of a kuzzle instance.

'use strict';

const { isEmpty } = require('lodash');
const { Request } = require('kuzzle-common-objects');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bluebird is much faster for // execution

Suggested change
const { Request } = require('kuzzle-common-objects');
const { Request } = require('kuzzle-common-objects');
const Bluebird = require('bluebird');

if ( action !== '*'
&& !this.kuzzle.pluginsManager.isAction(roleController, action)
) {
if ( action !== '*' && !plugins.isAction(roleController, action)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ( action !== '*' && !plugins.isAction(roleController, action)) {
if (action !== '*' && !plugins.isAction(roleController, action)) {

@scottinet
Copy link
Contributor Author

@Aschen > suggestions applied.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jul 7, 2020

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities (and Security Hotspot 0 Security Hotspots to review)
Code Smell A 0 Code Smells

0.0% 0.0% Coverage
0.2% 0.2% Duplication

@scottinet scottinet merged commit ccb73a4 into 2-dev Jul 7, 2020
@scottinet scottinet deleted the loose-coupling-security branch July 7, 2020 09:42
This was referenced Jul 7, 2020
scottinet added a commit that referenced this pull request Jul 8, 2020
# [2.3.2](/~https://github.com/kuzzleio/kuzzle/releases/tag/2.3.2) (2020-07-07)


#### Bug fixes

- [ [#1701](#1701) ] Fix: description should not be required for plugin custom errors   ([scottinet](/~https://github.com/scottinet))
- [ [#1695](#1695) ] Fix dump generation   ([Aschen](/~https://github.com/Aschen))
- [ [#1698](#1698) ] Fix generic events on search queries   ([scottinet](/~https://github.com/scottinet))

#### Enhancements

- [ [#1702](#1702) ] Update search indexes only when the "dynamic" setting changes from false to true/strict   ([Aschen](/~https://github.com/Aschen))
- [ [#1630](#1630) ] Loose coupling: security module   ([scottinet](/~https://github.com/scottinet))
- [ [#1604](#1604) ] Add request ID in log   ([Aschen](/~https://github.com/Aschen))
---
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants