chnages
This commit is contained in:
2
fusion_helpdesk_central/models/__init__.py
Normal file
2
fusion_helpdesk_central/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import fusion_helpdesk_client_key
|
||||
186
fusion_helpdesk_central/models/fusion_helpdesk_client_key.py
Normal file
186
fusion_helpdesk_central/models/fusion_helpdesk_client_key.py
Normal file
@@ -0,0 +1,186 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user