diff --git a/fastapi_auth_partner/__manifest__.py b/fastapi_auth_partner/__manifest__.py
index 7be7cc7f7..11146f290 100644
--- a/fastapi_auth_partner/__manifest__.py
+++ b/fastapi_auth_partner/__manifest__.py
@@ -24,6 +24,7 @@
"views/fastapi_auth_directory_view.xml",
"views/res_partner_view.xml",
"wizards/wizard_partner_auth_reset_password_view.xml",
+ "wizards/wizard_partner_auth_impersonate_view.xml",
],
"demo": [
"demo/fastapi_auth_directory_demo.xml",
diff --git a/fastapi_auth_partner/models/fastapi_auth_directory.py b/fastapi_auth_partner/models/fastapi_auth_directory.py
index a6151d19e..d7110b93a 100644
--- a/fastapi_auth_partner/models/fastapi_auth_directory.py
+++ b/fastapi_auth_partner/models/fastapi_auth_directory.py
@@ -14,6 +14,9 @@ class FastApiAuthDirectory(models.Model):
set_password_token_duration = fields.Integer(
default=1440, help="In minute, default 1440 minutes => 24h", required=True
)
+ impersonating_token_duration = fields.Integer(
+ default=1, help="In minute, default 1 minute", required=True
+ )
request_reset_password_template_id = fields.Many2one(
"mail.template", "Mail Template Forget Password", required=True
)
@@ -33,6 +36,24 @@ class FastApiAuthDirectory(models.Model):
)
count_partner = fields.Integer(compute="_compute_count_partner")
+ fastapi_endpoint_ids = fields.One2many(
+ "fastapi.endpoint",
+ "directory_id",
+ string="FastAPI Endpoints",
+ )
+ impersonating_user_ids = fields.Many2many(
+ "res.users",
+ "fastapi_auth_directory_impersonating_user_rel",
+ "directory_id",
+ "user_id",
+ string="Impersonating Users",
+ help="These odoo users can impersonate any partner of this directory",
+ default=lambda self: (
+ self.env.ref("base.user_root") | self.env.ref("base.user_admin")
+ ).ids,
+ groups="fastapi_auth_partner.group_partner_auth_manager",
+ )
+
def _compute_count_partner(self):
data = self.env["fastapi.auth.partner"].read_group(
[
diff --git a/fastapi_auth_partner/models/fastapi_auth_partner.py b/fastapi_auth_partner/models/fastapi_auth_partner.py
index 1eed35b3d..6f4c51ee2 100644
--- a/fastapi_auth_partner/models/fastapi_auth_partner.py
+++ b/fastapi_auth_partner/models/fastapi_auth_partner.py
@@ -8,6 +8,7 @@
from odoo import _, api, fields, models, tools
from odoo.exceptions import AccessDenied, UserError, ValidationError
+from odoo.http import request
from odoo.addons.auth_signup.models.res_partner import random_token
@@ -15,6 +16,7 @@
# https://passlib.readthedocs.io
# https://passlib.readthedocs.io/en/stable/narr/quickstart.html#choosing-a-hash
# be carefull odoo requirements use an old version of passlib
+# TODO: replace with a JWT token
DEFAULT_CRYPT_CONTEXT = passlib.context.CryptContext(["pbkdf2_sha512"])
DEFAULT_CRYPT_CONTEXT_TOKEN = passlib.context.CryptContext(
["pbkdf2_sha512"], pbkdf2_sha512__salt_size=0
@@ -38,11 +40,20 @@ class FastApiAuthPartner(models.Model):
directory_id = fields.Many2one(
"fastapi.auth.directory", "Directory", required=True, index=True
)
+ user_can_impersonate = fields.Boolean(
+ compute="_compute_user_can_impersonate",
+ help="Technical field to check if the user can impersonate",
+ )
+ impersonating_user_ids = fields.Many2many(
+ related="directory_id.impersonating_user_ids",
+ )
login = fields.Char(compute="_compute_login", store=True, required=True, index=True)
password = fields.Char(compute="_compute_password", inverse="_inverse_password")
encrypted_password = fields.Char(index=True)
token_set_password_encrypted = fields.Char()
token_expiration = fields.Datetime()
+ token_impersonating_encrypted = fields.Char()
+ token_impersonating_expiration = fields.Datetime()
nbr_pending_reset_sent = fields.Integer(
index=True,
help=(
@@ -133,6 +144,100 @@ def log_in(self, directory, login, password):
raise AccessDenied()
return self.browse(_id)
+ def local_impersonate(self):
+ """Local impersonate for dev mode"""
+ self.ensure_one()
+ if not self.env.user._is_admin():
+ raise AccessDenied(_("Only admin can impersonate locally"))
+
+ if not hasattr(request, "future_response"):
+ raise UserError(
+ _("Please install base_future_response for local impersonate to work")
+ )
+ self._set_auth_cookie(request.future_response)
+ return {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "title": _("Impersonation successful"),
+ "message": _("You are now impersonating %s\n%%s") % self.login,
+ "links": [
+ {
+ "label": f"{endpoint.app.title()} api docs",
+ "url": endpoint.docs_url,
+ }
+ for endpoint in self.directory_id.fastapi_endpoint_ids
+ ],
+ "type": "success",
+ "sticky": False,
+ },
+ }
+
+ def impersonate(self):
+ self.ensure_one()
+ if self.env.user not in self.impersonating_user_ids:
+ raise AccessDenied(_("You are not allowed to impersonate this user"))
+
+ endpoint_id = self.env.context.get("fastapi_endpoint_id")
+ if endpoint_id:
+ endpoint = self.env["fastapi.endpoint"].browse(endpoint_id)
+ if not endpoint:
+ return
+ else:
+ endpoints = self.directory_id.fastapi_endpoint_ids
+ if len(endpoints) == 1:
+ endpoint = endpoints
+ else:
+ wizard = self.env["ir.actions.act_window"]._for_xml_id(
+ "fastapi_auth_partner.fastapi_auth_partner_action_impersonate"
+ )
+ wizard["context"] = {"default_fastapi_auth_partner_id": self.id}
+ return wizard
+
+ base = endpoint.public_url or (
+ self.env["ir.config_parameter"].sudo().get_param("web.base.url")
+ + endpoint.root_path
+ )
+
+ token = random_token()
+ expiration = datetime.now() + timedelta(
+ minutes=self.directory_id.impersonating_token_duration
+ )
+ self.write(
+ {
+ "token_impersonating_encrypted": self._encrypt_token(token),
+ "token_impersonating_expiration": expiration,
+ }
+ )
+ url = f"{base}/auth/impersonate/{self.id}/{token}"
+ return {
+ "type": "ir.actions.act_url",
+ "url": url,
+ "target": "self",
+ }
+
+ @api.depends_context("uid")
+ def _compute_user_can_impersonate(self):
+ for record in self:
+ record.user_can_impersonate = self.env.user in record.impersonating_user_ids
+
+ def impersonating(self, directory, fastapi_partner_id, token):
+ hashed_token = self._encrypt_token(token)
+ partner_auth = self.search(
+ [
+ ("id", "=", fastapi_partner_id),
+ ("token_impersonating_encrypted", "=", hashed_token),
+ ("directory_id", "=", directory.id),
+ ]
+ )
+ if (
+ partner_auth
+ and partner_auth.token_impersonating_expiration > datetime.now()
+ ):
+ return partner_auth
+ else:
+ raise UserError(_("The token is not valid, please request a new one"))
+
def _get_template_request_reset_password(self, directory):
return directory.request_reset_password_template_id
diff --git a/fastapi_auth_partner/models/fastapi_endpoint.py b/fastapi_auth_partner/models/fastapi_endpoint.py
index 2a0818778..20ec6f270 100644
--- a/fastapi_auth_partner/models/fastapi_endpoint.py
+++ b/fastapi_auth_partner/models/fastapi_endpoint.py
@@ -21,12 +21,21 @@ class FastapiEndpoint(models.Model):
selection_add=[
("auth_partner", "Partner Auth"),
],
- string="Authenciation method",
+ string="Authentication method",
)
directory_id = fields.Many2one("fastapi.auth.directory")
+ is_partner_auth = fields.Boolean(
+ compute="_compute_is_partner_auth",
+ help="Technical field to know if the auth method is partner",
+ )
+
def _get_fastapi_routers(self) -> List[APIRouter]:
routers = super()._get_fastapi_routers()
- if self.app == "demo":
+ if self.app == "demo" and self.demo_auth_method == "auth_partner":
routers.append(auth_router)
return routers
+
+ def _compute_is_partner_auth(self):
+ for rec in self:
+ rec.is_partner_auth = auth_router in rec._get_fastapi_routers()
diff --git a/fastapi_auth_partner/routers/auth.py b/fastapi_auth_partner/routers/auth.py
index 74a0601bb..6e0976906 100644
--- a/fastapi_auth_partner/routers/auth.py
+++ b/fastapi_auth_partner/routers/auth.py
@@ -18,6 +18,7 @@
from odoo.addons.fastapi.models import FastapiEndpoint
from fastapi import APIRouter, Depends, Response
+from fastapi.responses import RedirectResponse
from ..dependencies import auth_partner_authenticated_partner
from ..models.fastapi_auth_partner import COOKIE_AUTH_NAME
@@ -107,6 +108,23 @@ def profile(
return AuthPartnerResponse.from_auth_partner(partner_auth)
+@auth_router.get("/auth/impersonate/{fastapi_partner_id}/{token}")
+def impersonate(
+ fastapi_partner_id: int,
+ token: str,
+ env: Annotated[Environment, Depends(odoo_env)],
+ endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
+) -> RedirectResponse:
+ partner_auth = (
+ env["fastapi.auth.service"]
+ .sudo()
+ ._impersonate(endpoint.directory_id, fastapi_partner_id, token)
+ )
+ response = RedirectResponse(url="/")
+ partner_auth._set_auth_cookie(response)
+ return response
+
+
class AuthService(models.AbstractModel):
_name = "fastapi.auth.service"
_description = "Fastapi Auth Service"
@@ -143,6 +161,13 @@ def _login(self, directory, data):
else:
raise AccessError(_("Invalid Login or Password"))
+ def _impersonate(self, directory, fastapi_partner_id, token):
+ return (
+ self.env["fastapi.auth.partner"]
+ .sudo()
+ .impersonating(directory, fastapi_partner_id, token)
+ )
+
def _logout(self, directory, response):
response.set_cookie(COOKIE_AUTH_NAME, max_age=0)
diff --git a/fastapi_auth_partner/security/ir.model.access.csv b/fastapi_auth_partner/security/ir.model.access.csv
index eeadf5f68..88a042b5e 100644
--- a/fastapi_auth_partner/security/ir.model.access.csv
+++ b/fastapi_auth_partner/security/ir.model.access.csv
@@ -5,3 +5,4 @@ access_fastapi_auth_partner,fastapi_auth_partner_manager,model_fastapi_auth_part
api_access_fastapi_auth_partner,fastapi_auth_partner_api,model_fastapi_auth_partner,group_partner_auth_api,1,1,0,0
api_access_fastapi_res_partner,fastapi_res_partner_api,base.model_res_partner,group_partner_auth_api,1,0,0,0
api_access_fastapi_wizard_partner_auth_reset_password,fastapi_wizard_partner_auth_reset_password,model_wizard_partner_auth_reset_password,group_partner_auth_manager,1,1,1,1
+api_access_fastapi_wizard_partner_auth_impersonate,fastapi_wizard_partner_auth_impersonate,model_wizard_partner_auth_impersonate,group_partner_auth_manager,1,1,1,1
diff --git a/fastapi_auth_partner/views/fastapi_auth_directory_view.xml b/fastapi_auth_partner/views/fastapi_auth_directory_view.xml
index 3a60376fc..d7589cbe6 100644
--- a/fastapi_auth_partner/views/fastapi_auth_directory_view.xml
+++ b/fastapi_auth_partner/views/fastapi_auth_directory_view.xml
@@ -42,6 +42,16 @@
+
+
+
+
diff --git a/fastapi_auth_partner/views/fastapi_auth_partner_view.xml b/fastapi_auth_partner/views/fastapi_auth_partner_view.xml
index f33513b05..a43175c00 100644
--- a/fastapi_auth_partner/views/fastapi_auth_partner_view.xml
+++ b/fastapi_auth_partner/views/fastapi_auth_partner_view.xml
@@ -19,10 +19,26 @@
fastapi.auth.partner