diff --git a/addons/portal/__manifest__.py b/addons/portal/__manifest__.py index 017aef30313caa181d7b76be9c248f59a88e6bc3..f545e16ee608f6948fee1fae6ea80ff5d1e5d961 100644 --- a/addons/portal/__manifest__.py +++ b/addons/portal/__manifest__.py @@ -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', ], diff --git a/addons/portal/controllers/portal.py b/addons/portal/controllers/portal.py index 410e78500f0cfc6d8bebc90a180a704add75bc2a..1db9192123fc111e9e3d8a7f680a039da149f58b 100644 --- a/addons/portal/controllers/portal.py +++ b/addons/portal/controllers/portal.py @@ -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( diff --git a/addons/portal/models/__init__.py b/addons/portal/models/__init__.py index 1c70d12386ab6ff6a1e805d817815db52994fa79..e4169356b07cc9e66fd42e21a8e5ae85302d074a 100644 --- a/addons/portal/models/__init__.py +++ b/addons/portal/models/__init__.py @@ -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 diff --git a/addons/portal/models/res_config_settings.py b/addons/portal/models/res_config_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..e483cf8160460f8ab79127137ff9145478f5a3c2 --- /dev/null +++ b/addons/portal/models/res_config_settings.py @@ -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 diff --git a/addons/portal/models/res_users_apikeys_description.py b/addons/portal/models/res_users_apikeys_description.py new file mode 100644 index 0000000000000000000000000000000000000000..2539da5140ff3ab76339617091d0090e21dcef06 --- /dev/null +++ b/addons/portal/models/res_users_apikeys_description.py @@ -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 diff --git a/addons/portal/static/src/js/portal.js b/addons/portal/static/src/js/portal.js index ac6dac67a6e41b217a93255d566760bb742c3554..b2c2853b2d00dd795e1ce44c22e90779a127fd7c 100644 --- a/addons/portal/static/src/js/portal.js +++ b/addons/portal/static/src/js/portal.js @@ -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 diff --git a/addons/portal/static/src/xml/portal_security.xml b/addons/portal/static/src/xml/portal_security.xml index 2c19cfeaa0d6afc6d6a5fa801041574abe334e3e..290c1779e6196508f35c3ea47a8ab9b9108f1a57 100644 --- a/addons/portal/static/src/xml/portal_security.xml +++ b/addons/portal/static/src/xml/portal_security.xml @@ -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> diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 38bc229f14419e55e04eabfabe9481f162dd2934..0f6334d1fbb0fe99b3572b016f93b09ed862be64 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -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> diff --git a/addons/portal/views/res_config_settings_views.xml b/addons/portal/views/res_config_settings_views.xml new file mode 100644 index 0000000000000000000000000000000000000000..51c394b28bf01021ba6e32eaea13fd1a6ac4459a --- /dev/null +++ b/addons/portal/views/res_config_settings_views.xml @@ -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> diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 5b691798ff3b9646cca33fc232f58fa3f0ac6ceb..ac3dc45305f1579913b690066eda8ca68b50ee29 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -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'