|
| 1 | +# Copyright 2024 Akretion (http://www.akretion.com). |
| 2 | +# @author Florian Mounier <florian.mounier@akretion.com> |
| 3 | +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
| 4 | +from datetime import datetime, timedelta, timezone |
| 5 | +from secrets import token_urlsafe |
| 6 | + |
| 7 | +import jwt |
| 8 | + |
| 9 | +from odoo import _, api, fields, models |
| 10 | +from odoo.exceptions import AccessDenied |
| 11 | + |
| 12 | + |
| 13 | +class CrossConnectClient(models.Model): |
| 14 | + _name = "cross.connect.client" |
| 15 | + _description = "Cross Connect Client" |
| 16 | + _inherit = "server.env.mixin" |
| 17 | + |
| 18 | + name = fields.Char(required=True) |
| 19 | + |
| 20 | + endpoint_id = fields.Many2one( |
| 21 | + "fastapi.endpoint", |
| 22 | + required=True, |
| 23 | + string="Endpoint", |
| 24 | + ) |
| 25 | + |
| 26 | + api_key = fields.Char( |
| 27 | + required=True, |
| 28 | + string="API Key", |
| 29 | + help="The API key to give to configure on the client.", |
| 30 | + default=lambda self: self._generate_api_key(), |
| 31 | + ) |
| 32 | + |
| 33 | + allowed_group_ids = fields.Many2many( |
| 34 | + related="endpoint_id.cross_connect_allowed_group_ids", |
| 35 | + ) |
| 36 | + |
| 37 | + group_ids = fields.Many2many( |
| 38 | + "res.groups", |
| 39 | + string="Groups", |
| 40 | + help="The groups that this client belongs to.", |
| 41 | + domain="[('id', 'in', allowed_group_ids)]", |
| 42 | + ) |
| 43 | + |
| 44 | + user_ids = fields.One2many( |
| 45 | + "res.users", |
| 46 | + "cross_connect_client_id", |
| 47 | + string="Users", |
| 48 | + help="The users created by this cross connection.", |
| 49 | + ) |
| 50 | + user_count = fields.Integer( |
| 51 | + compute="_compute_user_count", |
| 52 | + string="Cross Connected User Count", |
| 53 | + help="The number of users created by this cross connection.", |
| 54 | + ) |
| 55 | + |
| 56 | + @api.model |
| 57 | + def _generate_api_key(self): |
| 58 | + # generate random ~64 chars secret key |
| 59 | + return token_urlsafe(64) |
| 60 | + |
| 61 | + @api.depends("user_ids") |
| 62 | + def _compute_user_count(self): |
| 63 | + for record in self: |
| 64 | + record.user_count = len(record.user_ids) |
| 65 | + |
| 66 | + def _request_access(self, access_request): |
| 67 | + # check groups |
| 68 | + groups = self.env["res.groups"].browse(access_request.groups) |
| 69 | + if groups - self.group_ids or not groups.exists(): |
| 70 | + raise AccessDenied(_("You are not allowed to access this endpoint.")) |
| 71 | + |
| 72 | + user = self.user_ids.filtered( |
| 73 | + lambda u: u.cross_connect_client_user_id == access_request.id |
| 74 | + ) |
| 75 | + vals = { |
| 76 | + "login": f"{self.id}_{access_request.id}_{access_request.login}", |
| 77 | + "email": access_request.email, |
| 78 | + "name": access_request.name, |
| 79 | + "lang": access_request.lang, |
| 80 | + "groups_id": [(6, 0, groups.ids)], |
| 81 | + "cross_connect_client_id": self.id, |
| 82 | + "cross_connect_client_user_id": access_request.id, |
| 83 | + } |
| 84 | + # Create user if not exists |
| 85 | + if not user: |
| 86 | + user = self.env["res.users"].create(vals) |
| 87 | + else: |
| 88 | + user.write(vals) |
| 89 | + |
| 90 | + return jwt.encode( |
| 91 | + { |
| 92 | + "exp": datetime.now(tz=timezone.utc) + timedelta(minutes=2), |
| 93 | + "aud": str(self.id), |
| 94 | + "id": user.id, |
| 95 | + "redirect_url": access_request.redirect_url or "/web", |
| 96 | + }, |
| 97 | + self.endpoint_id.cross_connect_secret_key, |
| 98 | + algorithm="HS256", |
| 99 | + ) |
| 100 | + |
| 101 | + def _log_from_token(self, token): |
| 102 | + try: |
| 103 | + obj = jwt.decode( |
| 104 | + token, |
| 105 | + self.endpoint_id.cross_connect_secret_key, |
| 106 | + audience=str(self.id), |
| 107 | + options={"require": ["exp", "aud", "id"]}, |
| 108 | + algorithms=["HS256"], |
| 109 | + ) |
| 110 | + except jwt.PyJWTError as e: |
| 111 | + raise AccessDenied(_("Invalid Token")) from e |
| 112 | + |
| 113 | + user = self.env["res.users"].browse(obj["id"]) |
| 114 | + |
| 115 | + if not user: |
| 116 | + raise AccessDenied(_("Invalid Token")) |
| 117 | + |
| 118 | + return user, obj["redirect_url"] |
0 commit comments