Skip to content

Commit 024bcf6

Browse files
committed
[IMP] auth_oauth_multi_token: make it compatible with odoo.sh "login as"
1 parent f5a8de0 commit 024bcf6

File tree

3 files changed

+91
-30
lines changed

3 files changed

+91
-30
lines changed

auth_oauth_multi_token/models/auth_oauth_multi_token.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ def _oauth_validate_multi_token(self):
4646
user_tokens = self._oauth_user_tokens(token.user_id.id)
4747
max_token = token.user_id.oauth_access_max_token
4848
if user_tokens and len(user_tokens) > max_token:
49-
# clear last token
50-
user_tokens[max_token - 1]._oauth_clear_token()
49+
# clear tokens beyond the max
50+
user_tokens[max_token:]._oauth_clear_token()
5151

5252
def _oauth_clear_token(self):
5353
"""Disable current token records."""

auth_oauth_multi_token/models/res_users.py

+33-21
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55

66
from odoo import api, exceptions, fields, models
77

8-
from odoo.addons import base
9-
10-
base.models.res_users.USER_PRIVATE_FIELDS.append("oauth_master_uuid")
11-
128

139
class ResUsers(models.Model):
1410
_inherit = "res.users"
@@ -27,10 +23,9 @@ def _generate_oauth_master_uuid(self):
2723
oauth_access_max_token = fields.Integer(
2824
string="Max Number of Simultaneous Connections", default=10, required=True
2925
)
30-
oauth_master_uuid = fields.Char(
31-
string="Master UUID",
32-
copy=False,
33-
readonly=True,
26+
27+
# use the oauth_access_token field as oauth_master_uuid
28+
oauth_access_token = fields.Char(
3429
required=True,
3530
default=lambda self: self._generate_oauth_master_uuid(),
3631
)
@@ -39,45 +34,62 @@ def _generate_oauth_master_uuid(self):
3934
def multi_token_model(self):
4035
return self.env["auth.oauth.multi.token"]
4136

37+
@api.model
38+
def _generate_signup_values(self, provider, validation, params):
39+
"""Because access_token was replaced in
40+
_auth_oauth_signin we need to replace it here."""
41+
res = super()._generate_signup_values(provider, validation, params)
42+
res["oauth_access_token"] = params["access_token_multi"]
43+
return res
44+
4245
@api.model
4346
def _auth_oauth_signin(self, provider, validation, params):
4447
"""Override to handle sign-in with multi token."""
45-
res = super()._auth_oauth_signin(provider, validation, params)
48+
params["access_token_multi"] = params["access_token"]
4649

47-
oauth_uid = validation["user_id"]
4850
# Lookup for user by oauth uid and provider
51+
oauth_uid = validation["user_id"]
4952
user = self.search(
5053
[("oauth_uid", "=", oauth_uid), ("oauth_provider_id", "=", provider)]
5154
)
55+
56+
# Because access_token is automatically written to the user, we need to replace
57+
# this by the existing oauth_access_token which acts as oauth_master_uuid
58+
params["access_token"] = user.oauth_access_token
59+
res = super()._auth_oauth_signin(provider, validation, params)
60+
5261
if not user:
5362
raise exceptions.AccessDenied()
5463
user.ensure_one()
5564
# user found and unique: create a token
5665
self.multi_token_model.create(
57-
{"user_id": user.id, "oauth_access_token": params["access_token"]}
66+
{"user_id": user.id, "oauth_access_token": params["access_token_multi"]}
5867
)
5968
return res
6069

6170
def action_oauth_clear_token(self):
6271
"""Inactivate current user tokens."""
6372
self.mapped("oauth_access_token_ids")._oauth_clear_token()
6473
for res in self:
65-
res.oauth_access_token = False
66-
res.oauth_master_uuid = self._generate_oauth_master_uuid()
74+
res.oauth_access_token = self._generate_oauth_master_uuid()
6775

6876
@api.model
6977
def _check_credentials(self, password, env):
7078
"""Override to check credentials against multi tokens."""
7179
try:
7280
return super()._check_credentials(password, env)
7381
except exceptions.AccessDenied:
74-
res = self.multi_token_model.sudo().search(
75-
[("user_id", "=", self.env.uid), ("oauth_access_token", "=", password)]
82+
passwd_allowed = (
83+
env["interactive"] or not self.env.user._rpc_api_keys_only()
7684
)
77-
if not res:
78-
raise
85+
if passwd_allowed and self.env.user.active:
86+
res = self.multi_token_model.sudo().search(
87+
[
88+
("user_id", "=", self.env.uid),
89+
("oauth_access_token", "=", password),
90+
]
91+
)
92+
if res:
93+
return
7994

80-
def _get_session_token_fields(self):
81-
res = super()._get_session_token_fields()
82-
res.remove("oauth_access_token")
83-
return res | {"oauth_master_uuid"}
95+
raise

auth_oauth_multi_token/tests/test_multi_token.py

+56-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
33

44
import json
5+
import uuid
56

67
from odoo import exceptions
8+
from odoo.tests import users
79
from odoo.tests.common import TransactionCase
810

911

@@ -26,9 +28,10 @@ def setUpClass(cls):
2628
)
2729

2830
def _fake_params(self, **kw):
31+
random = uuid.uuid4().hex
2932
params = {
30-
"state": json.dumps({"t": "FAKE_TOKEN"}),
31-
"access_token": "FAKE_ACCESS_TOKEN",
33+
"state": json.dumps({"t": f"FAKE_TOKEN_{random}"}),
34+
"access_token": f"FAKE_ACCESS_TOKEN_{random}",
3235
}
3336
params.update(kw)
3437
return params
@@ -48,8 +51,10 @@ def _test_one_token(self):
4851
"user_id": "oauth_uid_johndoe",
4952
}
5053
params = self._fake_params()
51-
login = self.user_model._auth_oauth_signin(
52-
self.provider_google.id, validation, params
54+
login = (
55+
self.env["res.users"]
56+
.sudo()
57+
._auth_oauth_signin(self.provider_google.id, validation, params)
5358
)
5459
self.assertEqual(login, "johndoe")
5560

@@ -80,10 +85,54 @@ def test_access_multi_token(self):
8085
len(self.user.oauth_access_token_ids), self.user.oauth_access_max_token
8186
)
8287

83-
def test_remove_oauth_access_token(self):
88+
@users("johndoe")
89+
def test_access_multi_token_first_removed(self):
90+
# no token yet
91+
self.assertFalse(self.user.oauth_access_token_ids)
92+
93+
# login the first token
94+
validation = {
95+
"user_id": "oauth_uid_johndoe",
96+
}
97+
params = self._fake_params()
98+
login = (
99+
self.env["res.users"]
100+
.sudo()
101+
._auth_oauth_signin(self.provider_google.id, validation, params)
102+
)
103+
self.assertEqual(login, "johndoe")
104+
105+
# login is working
106+
self.env["res.users"]._check_credentials(
107+
params["access_token_multi"], {"interactive": False}
108+
)
109+
110+
# use as many token as max allowed
111+
for token_count in range(2, self.user.oauth_access_max_token + 1):
112+
self._test_one_token()
113+
self.assertEqual(len(self.user.oauth_access_token_ids), token_count)
114+
self.assertEqual(
115+
len(self.token_model._oauth_user_tokens(self.user.id)), token_count
116+
)
117+
118+
# exceed the number, token removed and login blocked
119+
self._test_one_token()
120+
with self.assertRaises(exceptions.AccessDenied):
121+
self.env["res.users"]._check_credentials(
122+
params["access_token_multi"], {"interactive": False}
123+
)
124+
125+
# token count does not exceed max number
126+
self.assertEqual(
127+
len(self.user.oauth_access_token_ids), self.user.oauth_access_max_token
128+
)
129+
130+
def test_oauth_access_token_odoo_sh(self):
131+
# do not change the _get_session_token_fields result to stay compatible
132+
# with odoo.sh
84133
res = self.user._get_session_token_fields()
85-
self.assertFalse("oauth_access_token" in res)
86-
self.assertTrue("oauth_master_uuid" in res)
134+
self.assertTrue("oauth_access_token" in res)
135+
self.assertFalse("oauth_master_uuid" in res)
87136

88137
def test_action_oauth_clear_token(self):
89138
self.user.action_oauth_clear_token()

0 commit comments

Comments
 (0)