Skip to content

Commit aed03f9

Browse files
[ADD] auth_saml: handle redirect parameter in the URI
1 parent 5fe354f commit aed03f9

File tree

5 files changed

+99
-12
lines changed

5 files changed

+99
-12
lines changed

auth_saml/controllers/main.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# Copyright (C) 2020 GlodoUK <https://www.glodo.uk/>
2-
# Copyright (C) 2010-2016, 2022 XCG Consulting <https://xcg-consulting.fr/>
2+
# Copyright (C) 2010-2016, 2022-2023 XCG Consulting <https://xcg-consulting.fr/>
33
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
44

55
import functools
66
import json
77
import logging
88

99
import werkzeug.utils
10+
from werkzeug.exceptions import BadRequest
1011
from werkzeug.urls import url_quote_plus
1112

1213
import odoo
@@ -53,7 +54,7 @@ def wrapper(self, req, **kw):
5354

5455

5556
class SAMLLogin(Home):
56-
def _list_saml_providers_domain(self):
57+
def _list_saml_providers_domain(self): # pylint: disable=no-self-use
5758
return []
5859

5960
def list_saml_providers(self, with_autoredirect: bool = False) -> models.Model:
@@ -66,7 +67,8 @@ def list_saml_providers(self, with_autoredirect: bool = False) -> models.Model:
6667
if with_autoredirect:
6768
domain.append(("autoredirect", "=", True))
6869
providers = request.env["auth.saml.provider"].sudo().search_read(domain)
69-
70+
for provider in providers:
71+
provider["auth_link"] = self._auth_saml_request_link(provider)
7072
return providers
7173

7274
def _saml_autoredirect(self):
@@ -78,11 +80,21 @@ def _saml_autoredirect(self):
7880
)
7981
if autoredirect_providers and not disable_autoredirect:
8082
return werkzeug.utils.redirect(
81-
"/auth_saml/get_auth_request?pid=%d" % autoredirect_providers[0]["id"],
83+
self._auth_saml_request_link(autoredirect_providers[0]),
8284
303,
8385
)
8486
return None
8587

88+
def _auth_saml_request_link(self, provider: models.Model):
89+
"""Return the auth request link for the provided provider"""
90+
params = {
91+
"pid": provider["id"],
92+
}
93+
redirect = request.params.get("redirect")
94+
if redirect:
95+
params["redirect"] = redirect
96+
return "/auth_saml/get_auth_request?%s" % werkzeug.urls.url_encode(params)
97+
8698
@http.route()
8799
def web_client(self, s_action=None, **kw):
88100
ensure_db()
@@ -143,7 +155,6 @@ def _get_saml_extra_relaystate(self):
143155
The provider will automatically set things like the dbname, provider
144156
id, etc.
145157
"""
146-
147158
redirect = request.params.get("redirect") or "web"
148159
if not redirect.startswith(("//", "http://", "https://")):
149160
redirect = "{}{}".format(
@@ -198,6 +209,8 @@ def signin(self, req, **kw):
198209
state = json.loads(kw["RelayState"])
199210
provider = state["p"]
200211
dbname = state["d"]
212+
if not http.db_filter([dbname]):
213+
return BadRequest()
201214
context = state.get("c", {})
202215
registry = registry_get(dbname)
203216

@@ -215,8 +228,15 @@ def signin(self, req, **kw):
215228
)
216229
action = state.get("a")
217230
menu = state.get("m")
231+
redirect = (
232+
werkzeug.urls.url_unquote_plus(state["r"])
233+
if state.get("r")
234+
else False
235+
)
218236
url = "/"
219-
if action:
237+
if redirect:
238+
url = redirect
239+
elif action:
220240
url = "/#action=%s" % action
221241
elif menu:
222242
url = "/#menu_id=%s" % menu

auth_saml/models/auth_saml_provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def _get_cert_key_path(self, field="sp_pem_public"):
191191

192192
return keys_path
193193

194-
def _get_config_for_provider(self, base_url: str = None):
194+
def _get_config_for_provider(self, base_url: str = None) -> Saml2Config:
195195
"""
196196
Internal helper to get a configured Saml2Client
197197
"""
@@ -237,7 +237,7 @@ def _get_config_for_provider(self, base_url: str = None):
237237
sp_config.allow_unknown_attributes = True
238238
return sp_config
239239

240-
def _get_client_for_provider(self, base_url: str = None):
240+
def _get_client_for_provider(self, base_url: str = None) -> Saml2Client:
241241
sp_config = self._get_config_for_provider(base_url)
242242
saml_client = Saml2Client(config=sp_config)
243243
return saml_client
@@ -336,7 +336,7 @@ def _store_outstanding_request(self, reqid):
336336
{"saml_provider_id": self.id, "saml_request_id": reqid}
337337
)
338338

339-
def _metadata_string(self, valid=None, base_url=None):
339+
def _metadata_string(self, valid=None, base_url: str = None):
340340
self.ensure_one()
341341

342342
sp_config = self._get_config_for_provider(base_url)

auth_saml/readme/HISTORY.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
15.0.1.2.0
2+
~~~~~~~~~~
3+
4+
Handle redirect after authentification.
5+
16
15.0.1.1.0
27
~~~~~~~~~~
38

auth_saml/tests/test_pysaml.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
12
import base64
3+
import html
24
import os
5+
from unittest.mock import patch
36

47
from odoo.exceptions import AccessDenied, UserError, ValidationError
58
from odoo.tests import HttpCase, tagged
@@ -85,6 +88,21 @@ def test_ensure_provider_appears_on_login(self):
8588
self.assertIn("Login with Authentic", response.text)
8689
self.assertIn(self.url_saml_request, response.text)
8790

91+
def test_ensure_provider_appears_on_login_with_redirect_param(self):
92+
"""Test that SAML provider is listed in the login page keeping the redirect"""
93+
response = self.url_open(
94+
"/web/login?redirect=%2Fweb%23action%3D37%26model%3Dir.module.module%26view"
95+
"_type%3Dkanban%26menu_id%3D5"
96+
)
97+
self.assertIn("Login with Authentic", response.text)
98+
self.assertIn(
99+
"/auth_saml/get_auth_request?pid={}&amp;redirect=%2Fweb%23action%3D37%26mod"
100+
"el%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5".format(
101+
self.saml_provider.id
102+
),
103+
response.text,
104+
)
105+
88106
def test_ensure_metadata_present(self):
89107
response = self.url_open(
90108
"/auth_saml/metadata?p=%d&d=%s"
@@ -96,7 +114,7 @@ def test_ensure_metadata_present(self):
96114

97115
def test_ensure_get_auth_request_redirects(self):
98116
response = self.url_open(
99-
"/auth_saml/get_auth_request?pid=%d" % (self.saml_provider.id),
117+
"/auth_saml/get_auth_request?pid=%d" % self.saml_provider.id,
100118
allow_redirects=False,
101119
)
102120
self.assertTrue(response.ok)
@@ -160,14 +178,15 @@ def test_login_with_saml(self):
160178
self.assertEqual(200, response.status_code)
161179
unpacked_response = response._unpack()
162180

163-
(_database, login, token) = (
181+
(database, login, token) = (
164182
self.env["res.users"]
165183
.sudo()
166184
.auth_saml(
167185
self.saml_provider.id, unpacked_response.get("SAMLResponse"), None
168186
)
169187
)
170188

189+
self.assertEqual(database, self.env.cr.dbname)
171190
self.assertEqual(login, self.user.login)
172191

173192
# We should not be able to log in with the wrong token
@@ -273,3 +292,46 @@ def test_disallow_user_admin_can_have_password(self):
273292
).value = "False"
274293
# Test base.user_admin exception
275294
self.env.ref("base.user_admin").password = "nNRST4j*->sEatNGg._!"
295+
296+
def test_db_filtering(self):
297+
# change filter to only allow our db.
298+
with patch("odoo.http.db_filter", new=lambda *args, **kwargs: []):
299+
self.add_provider_to_user()
300+
301+
redirect_url = self.saml_provider._get_auth_request()
302+
response = self.idp.fake_login(redirect_url)
303+
unpacked_response = response._unpack()
304+
305+
for key in unpacked_response:
306+
unpacked_response[key] = html.unescape(unpacked_response[key])
307+
response = self.url_open("/auth_saml/signin", data=unpacked_response)
308+
self.assertFalse(response.ok)
309+
self.assertIn(response.status_code, [400, 404])
310+
311+
def test_redirect_after_login(self):
312+
"""Test that providing a redirect will be kept after SAML login."""
313+
self.add_provider_to_user()
314+
315+
redirect_url = self.saml_provider._get_auth_request(
316+
{
317+
"r": "%2Fweb%23action%3D37%26model%3Dir.module.module%26view_type%3Dkanban%26menu_id%3D5"
318+
}
319+
)
320+
response = self.idp.fake_login(redirect_url)
321+
unpacked_response = response._unpack()
322+
323+
for key in unpacked_response:
324+
unpacked_response[key] = html.unescape(unpacked_response[key])
325+
response = self.url_open(
326+
"/auth_saml/signin",
327+
data=unpacked_response,
328+
allow_redirects=True,
329+
timeout=300,
330+
)
331+
self.assertTrue(response.ok)
332+
self.assertEqual(response.status_code, 200)
333+
self.assertEqual(
334+
response.url,
335+
self.base_url()
336+
+ "/web#action=37&model=ir.module.module&view_type=kanban&menu_id=5",
337+
)

auth_saml/views/auth_saml.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
t-foreach="saml_providers"
1212
t-as="p"
1313
class="list-group-item list-group-item-action py-2"
14-
t-att-href="'/auth_saml/get_auth_request?pid=%s'%p['id']"
14+
t-att-href="p['auth_link']"
1515
>
1616
<i t-att-class="p['css_class']" />
1717
<t t-esc="p['body']" />

0 commit comments

Comments
 (0)