Skip to content

Commit

Permalink
fastapi_auth_partner: Finish first version of the module
Browse files Browse the repository at this point in the history
- add view, wizard (to send reminder in migration case)
- add end2end test
- fix cookies and api
  • Loading branch information
sebastienbeau committed Sep 6, 2023
1 parent b8f79a8 commit 21282e0
Show file tree
Hide file tree
Showing 17 changed files with 397 additions and 37 deletions.
1 change: 1 addition & 0 deletions fastapi_auth_partner/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import models
from . import routers
from . import schemas
from . import wizards
4 changes: 4 additions & 0 deletions fastapi_auth_partner/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"security/ir_rule.xml",
"data/email_data.xml",
"views/fastapi_endpoint_view.xml",
"views/fastapi_auth_directory_view.xml",
"views/fastapi_auth_partner_view.xml",
"views/res_partner_view.xml",
"wizards/wizard_partner_auth_reset_password_view.xml",
],
"demo": [
"demo/fastapi_auth_directory_demo.xml",
Expand Down
5 changes: 3 additions & 2 deletions fastapi_auth_partner/data/email_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="model_fastapi_auth_partner" />
<field name="auto_delete" eval="True" />
<field name="lang">fr_FR</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
Hi ${object.partner_id.name},
Expand All @@ -29,7 +29,7 @@
<field name="partner_to">${object.partner_id.id}</field>
<field name="model_id" ref="model_fastapi_auth_partner" />
<field name="auto_delete" eval="True" />
<field name="lang">fr_FR</field>
<field name="lang">${object.partner_id.lang}</field>
<field name="body_html" type="html">
<div>
Hi ${object.partner_id.name},
Expand All @@ -43,5 +43,6 @@
</div>
</field>
</record>

</data>
</odoo>
4 changes: 2 additions & 2 deletions fastapi_auth_partner/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ def __call__(
return env["res.partner"].with_user(env.ref("base.public_user")).browse()

elif fastapi_auth_partner:
directory = endpoint.directory_id
directory = endpoint.sudo().directory_id
vals = URLSafeTimedSerializer(directory.cookie_secret_key).loads(
fastapi_auth_partner, max_age=directory.cookie_duration * 60
)
if vals["did"] == directory.id and vals["pid"]:
partner = env["res.partner"].browse(vals["pid"]).exists()
if partner:
auth = partner.partner_auth_ids.filtered(
auth = partner.sudo().partner_auth_ids.filtered(
lambda s: s.directory_id == directory
)
if auth:
Expand Down
28 changes: 23 additions & 5 deletions fastapi_auth_partner/models/fastapi_auth_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,40 @@ class FastApiAuthDirectory(models.Model):

name = fields.Char(required=True)
set_password_token_duration = fields.Integer(
default=1440,
help="In minute, default 1440 minutes => 24h",
default=1440, help="In minute, default 1440 minutes => 24h", required=True
)
forget_password_template_id = fields.Many2one(
"mail.template",
"Mail Template Forget Password",
"mail.template", "Mail Template Forget Password", required=True
)
invite_set_password_template_id = fields.Many2one(
"mail.template",
"Mail Template New Password",
required=True,
)
cookie_secret_key = fields.Char(
required=True,
groups="base.group_system",
)
cookie_secret_key = fields.Char()
cookie_duration = fields.Integer(
default=525600,
help="In minute, default 525600 minutes => 1 year",
required=True,
)
count_partner = fields.Integer(compute="_compute_count_partner")

def _compute_count_partner(self):
data = self.env["fastapi.auth.partner"].read_group(
[
("directory_id", "in", self.ids),
],
["directory_id"],
groupby=["directory_id"],
lazy=False,
)
res = {item["directory_id"][0]: item["__count"] for item in data}

for record in self:
record.count_partner = res.get(record.id, 0)

@property
def _server_env_fields(self):
Expand Down
43 changes: 33 additions & 10 deletions fastapi_auth_partner/models/fastapi_auth_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
class FastApiAuthPartner(models.Model):
_name = "fastapi.auth.partner"
_description = "FastApi Auth Partner"
_rec_name = "login"

partner_id = fields.Many2one("res.partner", "Partner", required=True)
directory_id = fields.Many2one("fastapi.auth.directory", "Directory", required=True)
Expand All @@ -38,6 +39,20 @@ class FastApiAuthPartner(models.Model):
encrypted_password = fields.Char()
token_set_password_encrypted = fields.Char()
token_expiration = fields.Datetime()
nbr_pending_reset_sent = fields.Integer(
help=(
"Number of pending reset sent from your customer."
"This field is usefull when after a migration from an other system "
"you ask all you customer to reset their password and you send"
"different mail depending on the number of reminder"
)
)
date_last_request_reset_pwd = fields.Datetime(
help="Date of the last password reset request"
)
date_last_sucessfull_reset_pwd = fields.Datetime(
help="Date of the last sucessfull password reset"
)

_sql_constraints = [
(
Expand Down Expand Up @@ -119,13 +134,16 @@ def _get_template_forgot_password(self, directory):
def _get_template_invite_set_password(self, directory):
return directory.invite_set_password_template_id

def _generate_token(self):
def _generate_token(self, force_expiration=None):
expiration = force_expiration or (
datetime.now()
+ timedelta(minutes=self.directory_id.set_password_token_duration)
)
token = random_token()
self.write(
{
"token_set_password_encrypted": self._encrypt_token(token),
"token_expiration": datetime.now()
+ timedelta(minutes=self.directory_id.set_password_token_duration),
"token_expiration": expiration,
}
)
return token
Expand All @@ -135,7 +153,7 @@ def _encrypt_token(self, token):

def set_password(self, directory, token_set_password, password):
hashed_token = self._encrypt_token(token_set_password)
partner_auth = self.env["partner.auth"].search(
partner_auth = self.search(
[
("token_set_password_encrypted", "=", hashed_token),
("directory_id", "=", directory.id),
Expand All @@ -153,6 +171,15 @@ def set_password(self, directory, token_set_password, password):
else:
raise UserError(_("The token is not valid, please request a new one"))

def send_reset_password(self, template, force_expiration=None):
self.ensure_one()
token = self._generate_token(force_expiration=force_expiration)
template.sudo().with_context(token=token).send_mail(self.id)
self.date_last_request_reset_pwd = fields.Datetime.now()
self.date_last_sucessfull_reset_pwd = None
self.nbr_pending_reset_sent += 1
return "Instruction sent by email"

def forget_password(self, directory, login):
# forget_password is called from a job so we return the result as a string
auth = self.search(
Expand All @@ -169,9 +196,7 @@ def forget_password(self, directory, login):
directory.name
)
)
token = auth._generate_token()
template.sudo().with_context(token=token).send_mail(auth.id)
return "Partner Auth reset password token sent"
return auth.send_reset_password(template)
else:
return "No Partner Auth found, skip"

Expand All @@ -186,9 +211,7 @@ def send_invite(self):
"Invitation to Set Password template is missing for directory {}"
).format(self.directory_id.name)
)
token = self._generate_token()
template.with_context(token=token).send_mail(self.id)
return True
return self.send_reset_password(template)

def _prepare_cookie_payload(self):
# use short key to reduce cookie size
Expand Down
15 changes: 15 additions & 0 deletions fastapi_auth_partner/models/res_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,18 @@ class ResPartner(models.Model):
partner_auth_ids = fields.One2many(
"fastapi.auth.partner", "partner_id", "Partner Auth"
)
count_partner_auth = fields.Integer(compute="_compute_count_partner_auth")

def _compute_count_partner_auth(self):
data = self.env["fastapi.auth.partner"].read_group(
[
("partner_id", "in", self.ids),
],
["partner_id"],
groupby=["partner_id"],
lazy=False,
)
res = {item["partner_id"][0]: item["__count"] for item in data}

for record in self:
record.count_partner_auth = res.get(record.id, 0)
25 changes: 15 additions & 10 deletions fastapi_auth_partner/routers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
else:
from typing_extensions import Annotated

from odoo import _, models
from odoo import _, fields, models
from odoo.api import Environment

from odoo.addons.base.models.res_partner import Partner
Expand Down Expand Up @@ -39,8 +39,8 @@ def register(
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
response: Response,
) -> AuthPartnerResponse:
partner_auth = env["fastapi.auth.service"]._register_auth(
endpoint.directory_id, data
partner_auth = (
env["fastapi.auth.service"].sudo()._register_auth(endpoint.directory_id, data)
)
partner_auth._set_auth_cookie(response)
return AuthPartnerResponse.from_orm(partner_auth)
Expand All @@ -53,7 +53,9 @@ def login(
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
response: Response,
) -> AuthPartnerResponse:
partner_auth = env["fastapi.auth.service"]._login(endpoint.directory_id, data)
partner_auth = (
env["fastapi.auth.service"].sudo()._login(endpoint.directory_id, data)
)
partner_auth._set_auth_cookie(response)
return AuthPartnerResponse.from_orm(partner_auth)

Expand All @@ -64,7 +66,7 @@ def logout(
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
response: Response,
):
env["fastapi.auth.service"]._logout(endpoint.directory_id, response)
env["fastapi.auth.service"].sudo()._logout(endpoint.directory_id, response)


@auth_router.post("/auth/forget_password")
Expand All @@ -73,7 +75,7 @@ def forget_password(
env: Annotated[Environment, Depends(odoo_env)],
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
):
env["fastapi.auth.service"]._forget_password(endpoint.directory_id, data)
env["fastapi.auth.service"].sudo()._forget_password(endpoint.directory_id, data)


@auth_router.post("/auth/set_password")
Expand All @@ -83,8 +85,8 @@ def set_password(
endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
response: Response,
) -> AuthPartnerResponse:
partner_auth = env["fastapi.auth.service"]._set_password(
endpoint.directory_id, data
partner_auth = (
env["fastapi.auth.service"].sudo()._set_password(endpoint.directory_id, data)
)
partner_auth._set_auth_cookie(response)
return AuthPartnerResponse.from_orm(partner_auth)
Expand All @@ -97,7 +99,7 @@ def profile(
partner: Annotated[Partner, Depends(auth_partner_authenticated_partner)],
) -> AuthPartnerResponse:
partner_auth = partner.partner_auth_ids.filtered(
lambda s: s.directory_id == endpoint.directory_id
lambda s: s.directory_id == endpoint.sudo().directory_id
)
return AuthPartnerResponse.from_orm(partner_auth)

Expand Down Expand Up @@ -145,8 +147,11 @@ def _set_password(self, directory, data):
partner_auth = (
self.env["fastapi.auth.partner"]
.sudo()
.set_password(directory, data.token_set_password, data.password)
.set_password(directory, data.token, data.password)
)
if partner_auth:
partner_auth.date_last_sucessfull_reset_pwd = fields.Datetime.now()
partner_auth.nbr_pending_reset_sent = 0
return partner_auth

def _forget_password(self, directory, data):
Expand Down
6 changes: 3 additions & 3 deletions fastapi_auth_partner/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fastapi_auth_directory,fastapi_auth_directory_manager,model_fastapi_auth_directory,base.group_system,1,1,1,1
access_fastapi_auth_partner,fastapi_auth_partner_manager,model_fastapi_auth_partner,group_partner_auth_manager,1,1,1,1
api_access_fastapi_auth_partner,fastapi_auth_partner_api,model_fastapi_auth_partner,group_partner_auth_api,1,1,1,0
api_access_fastapi_auth_directory,fastapi_auth_directory_api,model_fastapi_auth_directory,group_partner_auth_api,1,0,0,0
api_access_fastapi_res_partner,fastapi_res_partner_api,base.model_res_partner,group_partner_auth_api,1,0,1,0
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
19 changes: 16 additions & 3 deletions fastapi_auth_partner/security/ir_rule.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@
<odoo>

<record model="ir.rule" id="res_partner_api_rule">
<field name="name">Res Partner Register API</field>
<field name="name">Fast API auth API (res_partner)</field>
<field name="model_id" ref="base.model_res_partner" />
<field name="groups" eval="[(4, ref('group_partner_auth_api'))]" />
<field name="domain_force">[('partner_auth_ids','!=', False)]</field>
<field name="domain_force">[('id','=', authenticated_partner_id)]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_write" eval="False" />
<field name="perm_unlink" eval="False" />
</record>

<record model="ir.rule" id="fastapi_auth_partner_api_rule">
<field name="name">Fast API auth API (fastapi_partner_auth)</field>
<field name="model_id" ref="base.model_res_partner" />
<field name="groups" eval="[(4, ref('group_partner_auth_api'))]" />
<field
name="domain_force"
>[('partner_id','=', authenticated_partner_id)]</field>
<field name="perm_read" eval="True" />
<field name="perm_create" eval="False" />
<field name="perm_write" eval="False" />
<field name="perm_unlink" eval="False" />
</record>
Expand Down
7 changes: 5 additions & 2 deletions fastapi_auth_partner/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ def test_forget_password(self):
content=json.dumps({"login": "[email protected]"}),
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
mail = self.env["mail.mail"].search([], limit=1, order="id desc")
token = mail.body.split("token=")[1].split('" targe')[0]
response: Response = test_client.post(
"/auth/set_password",
content=json.dumps(
{
"login": "[email protected]",
"token": "TODO",
"password": "megasecret",
"token": token,
}
),
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
Loading

0 comments on commit 21282e0

Please sign in to comment.