187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1
|
|
"""Per-client API key registry for fusion_helpdesk.
|
|
|
|
Each row links a client deployment label (e.g. ENTECH) to a real
|
|
`res.users.apikeys` row on a shared bot account. The plaintext key is
|
|
shown ONCE on creation, then cleared. Revoking a row deletes the
|
|
underlying API key so the client deployment can no longer authenticate.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FusionHelpdeskClientKey(models.Model):
|
|
_name = 'fusion.helpdesk.client.key'
|
|
_description = 'Fusion Helpdesk — Client API Key'
|
|
_order = 'create_date desc'
|
|
_rec_name = 'client_label'
|
|
|
|
client_label = fields.Char(
|
|
string='Client Label', required=True, index=True,
|
|
help='Short tag identifying this client deployment '
|
|
'(e.g. ENTECH, MOBILITY). Free text — used for your '
|
|
'reference and to find keys quickly when revoking.',
|
|
)
|
|
notes = fields.Text(
|
|
string='Notes',
|
|
help='Optional. Stamp deployment URL, contact, install date.',
|
|
)
|
|
bot_user_id = fields.Many2one(
|
|
'res.users', string='Bot User', readonly=True,
|
|
ondelete='restrict',
|
|
)
|
|
apikey_id = fields.Many2one(
|
|
'res.users.apikeys', string='API Key Record',
|
|
readonly=True, ondelete='set null',
|
|
help='Underlying res.users.apikeys row. Cleared if the key '
|
|
'is revoked or deleted out-of-band.',
|
|
)
|
|
apikey_name = fields.Char(
|
|
string='Key Name', related='apikey_id.name', store=True,
|
|
readonly=True,
|
|
)
|
|
plaintext_key = fields.Char(
|
|
string='Plaintext Key (one-time display)',
|
|
readonly=True, copy=False,
|
|
help='Shown ONCE right after creation. Copy it now — once you '
|
|
'click "Mark Stored" it is wiped from this record forever. '
|
|
'The key keeps working; we just stop showing it.',
|
|
)
|
|
is_revoked = fields.Boolean(
|
|
string='Revoked', compute='_compute_is_revoked', store=True,
|
|
)
|
|
create_date = fields.Datetime(readonly=True)
|
|
display_name = fields.Char(
|
|
string='Display Name', compute='_compute_display_name', store=True,
|
|
)
|
|
|
|
@api.depends('client_label', 'is_revoked')
|
|
def _compute_display_name(self):
|
|
for rec in self:
|
|
base = rec.client_label or _('(unnamed)')
|
|
rec.display_name = ('%s [REVOKED]' % base) if rec.is_revoked else base
|
|
|
|
_sql_constraints = [
|
|
('fhc_client_label_unique',
|
|
'unique(client_label)',
|
|
'Client label must be unique — one row per client deployment.'),
|
|
]
|
|
|
|
@api.depends('apikey_id')
|
|
def _compute_is_revoked(self):
|
|
for rec in self:
|
|
rec.is_revoked = not rec.apikey_id
|
|
|
|
# ----------------------------------------------------------------------
|
|
@api.model
|
|
def _default_expiration_date(self):
|
|
"""Odoo 19 requires API keys to carry an expiration. Default to
|
|
10 years out so client deployments don't break unexpectedly.
|
|
Override per row by writing to apikey_id.expiration_date later."""
|
|
years = int(
|
|
self.env['ir.config_parameter'].sudo().get_param(
|
|
'fusion_helpdesk.key_expiration_years', '10',
|
|
) or 10
|
|
)
|
|
return datetime.utcnow() + timedelta(days=365 * years)
|
|
|
|
@api.model
|
|
def _get_bot_user(self):
|
|
ICP = self.env['ir.config_parameter'].sudo()
|
|
login = (ICP.get_param('fusion_helpdesk.bot_login') or '').strip()
|
|
if not login:
|
|
raise UserError(_(
|
|
'No bot user configured. Set `fusion_helpdesk.bot_login` '
|
|
'in System Parameters first.'
|
|
))
|
|
user = self.env['res.users'].search([('login', '=', login)], limit=1)
|
|
if not user:
|
|
raise UserError(_(
|
|
'Bot user "%s" not found. Create it first or update '
|
|
'`fusion_helpdesk.bot_login`.', login
|
|
))
|
|
return user
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
ApiKey = self.env['res.users.apikeys'].with_context(
|
|
mail_create_nosubscribe=True,
|
|
)
|
|
bot = self._get_bot_user()
|
|
records = super().create(vals_list)
|
|
for rec in records:
|
|
# Generate the apikey AS the bot user — the key is bound to
|
|
# the user that creates it.
|
|
label = 'fusion_helpdesk:%s' % (rec.client_label or rec.id)
|
|
key = ApiKey.with_user(bot)._generate(
|
|
'rpc', label, self._default_expiration_date(),
|
|
)
|
|
# Find the apikey row that was just created so we can link it.
|
|
apikey_row = self.env['res.users.apikeys'].sudo().search(
|
|
[('user_id', '=', bot.id), ('name', '=', label)],
|
|
limit=1, order='id desc',
|
|
)
|
|
rec.write({
|
|
'bot_user_id': bot.id,
|
|
'apikey_id': apikey_row.id if apikey_row else False,
|
|
'plaintext_key': key,
|
|
})
|
|
_logger.info(
|
|
'fusion_helpdesk_central: issued API key for client "%s" '
|
|
'(apikey_id=%s, bot_user=%s)',
|
|
rec.client_label, apikey_row.id if apikey_row else '?',
|
|
bot.login,
|
|
)
|
|
return records
|
|
|
|
def action_mark_stored(self):
|
|
"""User confirms they have copied the plaintext key. Wipe it
|
|
from the DB so a later read can't recover it. The underlying
|
|
res.users.apikeys row keeps working — only the plaintext copy
|
|
on this row is destroyed."""
|
|
self.write({'plaintext_key': False})
|
|
return True
|
|
|
|
def action_revoke(self):
|
|
"""Delete the underlying res.users.apikeys row. The client
|
|
deployment's next request will get an auth_failed error."""
|
|
for rec in self:
|
|
if rec.apikey_id:
|
|
rec.apikey_id.sudo().unlink()
|
|
_logger.warning(
|
|
'fusion_helpdesk_central: REVOKED API key for "%s" '
|
|
'(was apikey_id=%s)',
|
|
rec.client_label, rec.apikey_id.id,
|
|
)
|
|
rec.write({'plaintext_key': False})
|
|
return True
|
|
|
|
def action_rotate(self):
|
|
"""One-shot: revoke the existing key + issue a new one. Plaintext
|
|
is shown once on the same row."""
|
|
ApiKey = self.env['res.users.apikeys']
|
|
bot = self._get_bot_user()
|
|
for rec in self:
|
|
if rec.apikey_id:
|
|
rec.apikey_id.sudo().unlink()
|
|
label = 'fusion_helpdesk:%s' % rec.client_label
|
|
key = ApiKey.with_user(bot)._generate(
|
|
'rpc', label, self._default_expiration_date(),
|
|
)
|
|
apikey_row = self.env['res.users.apikeys'].sudo().search(
|
|
[('user_id', '=', bot.id), ('name', '=', label)],
|
|
limit=1, order='id desc',
|
|
)
|
|
rec.write({
|
|
'apikey_id': apikey_row.id if apikey_row else False,
|
|
'plaintext_key': key,
|
|
})
|
|
return True
|