Files
Odoo-Modules/fusion_helpdesk_central/models/fusion_helpdesk_client_key.py
gsinghpal 586f05d567 chnages
2026-05-04 02:14:34 -04:00

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