[ADD] portal: manage api keys in portal

Add the possibility for portal users to manage API keys
in the frontend portal interface.

- Allow for portal users to manage their API keys if the system parameter
  `portal.allow_api_keys` is set.
  A settting in `res.config.settings` is added in order
  to enable or disable the feature (to add/remove the system parameter)
  through a compute field,
  to avoid adding a new database column in standard
  (as this revision targets stable 15.0).
  The setting appear only in debug mode.

- Display the API keys in the portal in debug mode only
  (as in the back-end for regular users, in their profile)

- The flow in the frontend is a mimic of the flow from the backend,
  the wording and the look and feel is from the back-end.

closes odoo/odoo#86915

Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
This commit is contained in:
Denis Ledoux
2022-03-22 15:17:08 +01:00
parent 67f343ba6a
commit 5b078f2a77
10 changed files with 229 additions and 2 deletions
+1
View File
@@ -21,6 +21,7 @@ a dependency towards website editing and customization capabilities.""",
'data/mail_template_data.xml',
'data/mail_templates.xml',
'views/portal_templates.xml',
'views/res_config_settings_views.xml',
'wizard/portal_share_views.xml',
'wizard/portal_wizard_views.xml',
],
+1
View File
@@ -223,6 +223,7 @@ class CustomerPortal(Controller):
def security(self, **post):
values = self._prepare_portal_layout_values()
values['get_error'] = get_error
values['allow_api_keys'] = bool(request.env['ir.config_parameter'].sudo().get_param('portal.allow_api_keys'))
if request.httprequest.method == 'POST':
values.update(self._update_password(
+2
View File
@@ -6,4 +6,6 @@ from . import ir_ui_view
from . import mail_thread
from . import mail_message
from . import portal_mixin
from . import res_config_settings
from . import res_partner
from . import res_users_apikeys_description
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
portal_allow_api_keys = fields.Boolean(
string='Customer API Keys',
compute='_compute_portal_allow_api_keys',
inverse='_inverse_portal_allow_api_keys',
)
def _compute_portal_allow_api_keys(self):
for setting in self:
setting.portal_allow_api_keys = self.env['ir.config_parameter'].sudo().get_param('portal.allow_api_keys')
def _inverse_portal_allow_api_keys(self):
self.env['ir.config_parameter'].sudo().set_param('portal.allow_api_keys', self.portal_allow_api_keys)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res['portal_allow_api_keys'] = bool(self.env['ir.config_parameter'].sudo().get_param('portal.allow_api_keys'))
return res
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.exceptions import AccessError
class APIKeyDescription(models.TransientModel):
_inherit = 'res.users.apikeys.description'
def check_access_make_key(self):
try:
return super().check_access_make_key()
except AccessError:
if self.env['ir.config_parameter'].sudo().get_param('portal.allow_api_keys'):
if self.user_has_groups('base.group_portal'):
return
else:
raise AccessError(_("Only internal and portal users can create API keys"))
raise
+82
View File
@@ -5,6 +5,7 @@ var publicWidget = require('web.public.widget');
const Dialog = require('web.Dialog');
const {_t, qweb} = require('web.core');
const ajax = require('web.ajax');
const session = require('web.session');
publicWidget.registry.portalDetails = publicWidget.Widget.extend({
selector: '.o_portal_details',
@@ -155,6 +156,87 @@ publicWidget.registry.portalSearchPanel = publicWidget.Widget.extend({
},
});
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],
}));
await ajax.loadXML('/portal/static/src/xml/portal_security.xml', qweb);
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.target.id)]
})
);
window.location = window.location;
}
});
/**
* 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
@@ -10,4 +10,32 @@
<a href="/web/reset_password/" class="btn btn-link" role="button">Forgot password?</a>
</form>
</t>
<t t-name="portal.keydescription">
<form string="Key Description">
<h3><strong>Name your key</strong></h3>
<p>Enter a description of and purpose for the key.</p>
<input type="text" class="form-control col-10 col-md-6" placeholder="What's this key for?"
name="description" required="required"/>
<p>
It is very important that this description be clear
and complete, <strong>it will be the only way to
identify the key once created</strong>.
</p>
</form>
</t>
<t t-name="portal.keyshow">
<div>
<h3><strong>Write down your key</strong></h3>
<p>
Here is your new API key, use it instead of a password for RPC access.
Your login is still necessary for interactive usage.
</p>
<p><code><span t-out="key"/></code></p>
<p class="alert alert-warning" role="alert">
<strong>Important:</strong>
The key cannot be retrieved later and provides <b>full access</b>
to your user account, it is very important to store it securely.
</p>
</div>
</t>
</templates>
+35
View File
@@ -495,6 +495,41 @@
<button type="submit" class="btn btn-danger">Change Password</button>
</form>
</section>
<section t-if="debug and allow_api_keys">
<h3>
Developer API Keys
<a href="https://www.odoo.com/documentation/15.0/developer/misc/api/odoo.html#api-keys" target="_blank">
<i title="Documentation" class="fa fa-fw o_button_icon fa-info-circle"></i>
</a>
</h3>
<div>
<table class="table o_main_table">
<thead>
<tr>
<th>Description</th>
<th>Scope</th>
<th>Added On</th>
<th/>
</tr>
</thead>
<tbody>
<t t-foreach="request.env.user.api_key_ids" t-as="key">
<tr>
<td><span t-field="key.name"/></td>
<td><span t-field="key.scope"/></td>
<td><span t-field="key.create_date"/></td>
<td>
<i class="fa fa-trash text-danger o_portal_remove_api_key" type="button" t-att-id="key.id"/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
<div>
<button type="submit" class="btn btn-primary o_portal_new_api_key">New API Key</button>
</div>
</section>
</div></t>
</template>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.portal</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="40"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name=%(base.action_apikeys_admin)d]//ancestor::div[hasclass('o_setting_box')]" position="inside">
<div groups="base.group_no_one">
<div class="o_setting_left_pane">
<field name="portal_allow_api_keys"/>
</div>
<div class="o_setting_right_pane">
<label for="portal_allow_api_keys"/>
<div class="text-muted">
Let your customers create developer API keys
</div>
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
+5 -2
View File
@@ -1797,8 +1797,7 @@ class APIKeyDescription(models.TransientModel):
@check_identity
def make_key(self):
# only create keys for users who can delete their keys
if not self.user_has_groups('base.group_user'):
raise AccessError(_("Only internal users can create API keys"))
self.check_access_make_key()
description = self.sudo()
k = self.env['res.users.apikeys']._generate(None, self.sudo().name)
@@ -1815,6 +1814,10 @@ class APIKeyDescription(models.TransientModel):
}
}
def check_access_make_key(self):
if not self.user_has_groups('base.group_user'):
raise AccessError(_("Only internal users can create API keys"))
class APIKeyShow(models.AbstractModel):
_name = _description = 'res.users.apikeys.show'