Skip to content

Commit

Permalink
[ADD] base: Revoke access to your account to all devices
Browse files Browse the repository at this point in the history
Prior to this, there was no way to shut an active session you didn't have the
access to anymore or close every active session remotely.

Adding the RevokeAllDevices class, it is now possible to close every open
sessions, including the one you are performing the action on of the account
you are logged into via the Account Security page in the preferences.

This system is based on the principle that every open session uses the password
hash to maintain it. When the hash is computed, salt is added to it to make it
not reversible. Thanks to that salt, it is possible to change the user's
password's hash without changing the password. Therefore, the flow of this
addition is the following :
1. User clicks on the button and uses his passord to confirm identity.
2. User's input is used to check his identity (_check_credentials()).
3. User's input is used to refresh the password hash in the db (_change_password()).
4. User is logged out.

This adds a way to close all sessions remotely and improve user's control over
their account and therefore their security.

task-3191567

closes odoo#113899

Related: odoo/upgrade#4951
Signed-off-by: Martin Trigaux (mat) <[email protected]>
  • Loading branch information
Megaaaaaa authored and mart-e committed Jul 18, 2023
1 parent 21e1a2e commit 8298529
Show file tree
Hide file tree
Showing 10 changed files with 372 additions and 209 deletions.
2 changes: 1 addition & 1 deletion addons/auth_totp_portal/static/src/js/totp_frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import {_t} from "web.core";
import publicWidget from "web.public.widget";
import Dialog from "web.Dialog";
import {handleCheckIdentity} from "portal.portal";
import {handleCheckIdentity} from "portal.security";

/**
* Replaces specific <field> elements by normal HTML, strip out the rest entirely
Expand Down
1 change: 1 addition & 0 deletions addons/portal/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
'portal/static/src/js/portal_chatter.js',
'portal/static/src/xml/portal_chatter.xml',
'portal/static/src/js/portal_composer.js',
'portal/static/src/js/portal_security.js',
'portal/static/src/js/portal_signature.js',
'portal/static/src/xml/portal_signature.xml',
'portal/static/src/js/portal_sidebar.js',
Expand Down
184 changes: 0 additions & 184 deletions addons/portal/static/src/js/portal.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
/** @odoo-module alias=portal.portal **/

import publicWidget from "web.public.widget";
import Dialog from "web.Dialog";
import {_t, qweb} from "web.core";
import session from "web.session";

publicWidget.registry.portalDetails = publicWidget.Widget.extend({
selector: '.o_portal_details',
Expand Down Expand Up @@ -177,184 +174,3 @@ publicWidget.registry.portalSearchPanel = publicWidget.Widget.extend({
this._search();
},
});

publicWidget.registry.NewAPIKeyButton = publicWidget.Widget.extend({
selector: '.o_portal_new_api_key',
events: {
click: '_onClick'
},

async _onClick(e){
e.preventDefault();
// This call is done just so it asks for the password confirmation before starting displaying the
// dialog forms, to mimic the behavior from the backend, in which it asks for the password before
// displaying the wizard.
// The result of the call is unused. But it's required to call a method with the decorator `@check_identity`
// in order to use `handleCheckIdentity`.
await handleCheckIdentity(this.proxy('_rpc'), this._rpc({
model: 'res.users',
method: 'api_key_wizard',
args: [session.user_id],
}));
const self = this;
const d_description = new Dialog(self, {
title: _t('New API Key'),
$content: qweb.render('portal.keydescription'),
buttons: [{text: _t('Confirm'), classes: 'btn-primary', close: true, click: async () => {
var description = d_description.el.querySelector('[name="description"]').value;
var wizard_id = await this._rpc({
model: 'res.users.apikeys.description',
method: 'create',
args: [{name: description}],
});
var res = await handleCheckIdentity(
this.proxy('_rpc'),
this._rpc({
model: 'res.users.apikeys.description',
method: 'make_key',
args: [wizard_id],
})
);
const d_show = new Dialog(self, {
title: _t('API Key Ready'),
$content: qweb.render('portal.keyshow', {key: res.context.default_key}),
buttons: [{text: _t('Close'), clases: 'btn-primary', close: true}],
});
d_show.on('closed', this, () => {
window.location = window.location;
});
d_show.open();
}}, {text: _t('Discard'), close: true}],
});
d_description.opened(() => {
const input = d_description.el.querySelector('[name="description"]');
input.focus();
d_description.el.addEventListener('submit', (e) => {
e.preventDefault();
d_description.$footer.find('.btn-primary').click();
});
});
d_description.open();
}
});

publicWidget.registry.RemoveAPIKeyButton = publicWidget.Widget.extend({
selector: '.o_portal_remove_api_key',
events: {
click: '_onClick'
},

async _onClick(e){
e.preventDefault();
await handleCheckIdentity(
this.proxy('_rpc'),
this._rpc({
model: 'res.users.apikeys',
method: 'remove',
args: [parseInt(this.el.id)]
})
);
window.location = window.location;
}
});

publicWidget.registry.portalSecurity = publicWidget.Widget.extend({
selector: '.o_portal_security_body',

/**
* @override
*/
init: function () {
// Show the "deactivate your account" modal if needed
$('.modal.show#portal_deactivate_account_modal').removeClass('d-block').modal('show');

// Remove the error messages when we close the modal,
// so when we re-open it again we get a fresh new form
$('.modal#portal_deactivate_account_modal').on('hide.bs.modal', (event) => {
const $target = $(event.currentTarget);
$target.find('.alert').remove();
$target.find('.invalid-feedback').remove();
$target.find('.is-invalid').removeClass('is-invalid');
});

return this._super(...arguments);
},

});

/**
* Wraps an RPC call in a check for the result being an identity check action
* descriptor. If no such result is found, just returns the wrapped promise's
* result as-is; otherwise shows an identity check dialog and resumes the call
* on success.
*
* Warning: does not in and of itself trigger an identity check, a promise which
* never triggers and identity check internally will do nothing of use.
*
* @param {Function} rpc Widget#_rpc bound do the widget
* @param {Promise} wrapped promise to check for an identity check request
* @returns {Promise} result of the original call
*/
function handleCheckIdentity(rpc, wrapped) {
return wrapped.then((r) => {
if (!(r.type === "ir.actions.act_window" && r.res_model === "res.users.identitycheck")) {
return r;
}
const check_id = r.res_id;
return new Promise((resolve, reject) => {
const d = new Dialog(null, {
title: _t("Security Control"),
$content: qweb.render('portal.identitycheck'),
buttons: [{
text: _t("Confirm Password"), classes: 'btn btn-primary',
// nb: if click & close, waits for click to resolve before closing
click() {
const password_input = this.el.querySelector('[name=password]');
if (!password_input.reportValidity()) {
password_input.classList.add('is-invalid');
return;
}
return rpc({
model: 'res.users.identitycheck',
method: 'write',
args: [check_id, {password: password_input.value}]
}).then(() => rpc({
model: 'res.users.identitycheck',
method: 'run_check',
args: [check_id]
})).then((r) => {
this.close();
resolve(r);
}, (err) => {
err.event.preventDefault(); // suppress crashmanager
password_input.classList.add('is-invalid');
password_input.setCustomValidity(_t("Check failed"));
password_input.reportValidity();
});
}
}, {
text: _t('Cancel'), close: true
}]
}).on('close', null, () => {
// unlink wizard object?
reject();
});
d.opened(() => {
const pw = d.el.querySelector('[name="password"]');
pw.focus();
pw.addEventListener('input', () => {
pw.classList.remove('is-invalid');
pw.setCustomValidity('');
});
d.el.addEventListener('submit', (e) => {
e.preventDefault();
d.$footer.find('.btn-primary').click();
});
});
d.open();
});
});
}
export default {
handleCheckIdentity,
}
Loading

0 comments on commit 8298529

Please sign in to comment.