[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:
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user