Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion auth_oidc/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"summary": "Allow users to login through OpenID Connect Provider",
"external_dependencies": {"python": ["jose"]},
"depends": ["auth_oauth"],
"data": ["views/auth_oauth_provider.xml"],
"data": [
"security/ir.model.access.csv",
"views/auth_oauth_provider.xml",
],
"demo": ["demo/local_keycloak.xml"],
}
5 changes: 5 additions & 0 deletions auth_oidc/demo/local_keycloak.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@
name="jwks_uri"
>http://localhost:8080/auth/realms/master/protocol/openid-connect/certs</field>
</record>
<record id="local_keycloak_group_line" model="auth.oauth.provider.group_line">
<field name="provider_id" ref="local_keycloak" />
<field name="group_id" ref="base.group_no_one" />
<field name="expression">token['name'] == 'test'</field>
</record>
</odoo>
40 changes: 39 additions & 1 deletion auth_oidc/models/auth_oauth_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
# Copyright 2021 ACSONE SA/NV <https://acsone.eu>
# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import collections
import logging
import secrets

import requests

from odoo import fields, models, tools
from odoo import api, exceptions, fields, models, tools

try:
from jose import jwt
Expand Down Expand Up @@ -45,6 +46,9 @@ class AuthOauthProvider(models.Model):
string="Token URL", help="Required for OpenID Connect authorization code flow."
)
jwks_uri = fields.Char(string="JWKS URL", help="Required for OpenID Connect.")
group_line_ids = fields.One2many(
"auth.oauth.provider.group_line", "provider_id", string="Group mappings",
)

@tools.ormcache("self.jwks_uri", "kid")
def _get_key(self, kid):
Expand Down Expand Up @@ -80,3 +84,37 @@ def _parse_id_token(self, id_token, access_token):

res.update(self._map_token_values(res))
return res


class AuthOauthProviderGroupLine(models.Model):
_name = 'auth.oauth.provider.group_line'

provider_id = fields.Many2one('auth.oauth.provider', required=True)
group_id = fields.Many2one('res.groups', required=True)
expression = fields.Char(required=True, help="Variables: user, token")

@api.constrains('expression')
def _check_expression(self):
for this in self:
try:
this._eval_expression(self.env.user, {})
except (AttributeError, KeyError, NameError) as e:
raise exceptions.ValidationError('\n'.join(e.args))

def _eval_expression(self, user, token):
self.ensure_one()

class Defaultdict2(collections.defaultdict):
"""Class keeping yielding defaultdicts"""
def __init__(self, *args, **kwargs):
super().__init__(Defaultdict2, *args, **kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this Defaultdict2 ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it yields defaultdicts again that in turn yield defaultdicts. This way the user can write expressions in a quite carefree manner like token['key1']['key2']['key3'] == 'something' and it doesn't fail when key1 doesn't exist

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did I do something stupid here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, just too clever for me :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just added a comment there


def __eq__(self, other):
return False

return tools.safe_eval(
self.expression, {
'user': user,
'token': Defaultdict2(token),
}
)
25 changes: 25 additions & 0 deletions auth_oidc/models/res_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ def auth_oauth(self, provider, params):
_logger.error("No id_token in response.")
raise AccessDenied()
validation = oauth_provider._parse_id_token(id_token, access_token)
if oauth_provider.data_endpoint:
data = requests.get(
oauth_provider.data_endpoint,
headers={'Authorization': 'Bearer %s' % access_token}
).json()
validation.update(data)
# required check
if not validation.get("user_id"):
_logger.error("user_id claim not found in id_token (after mapping).")
Expand All @@ -74,3 +80,22 @@ def auth_oauth(self, provider, params):
raise AccessDenied()
# return user credentials
return (self.env.cr.dbname, login, access_token)

@api.model
def _auth_oauth_signin(self, provider, validation, params):
login = super()._auth_oauth_signin(provider, validation, params)
user = self.search([('login', '=', login)])
if user:
group_updates = []
for group_line in self.env['auth.oauth.provider'].browse(
provider
).group_line_ids:
if group_line._eval_expression(user, validation):
if group_line.group_id not in user.groups_id:
group_updates.append((4, group_line.group_id.id))
else:
if group_line.group_id in user.groups_id:
group_updates.append((3, group_line.group_id.id))
if group_updates:
user.write({'groups_id': group_updates})
return login
2 changes: 2 additions & 0 deletions auth_oidc/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_auth_oauth_provider_group_line,auth_oauth_provider,model_auth_oauth_provider_group_line,base.group_system,1,1,1,1
2 changes: 1 addition & 1 deletion auth_oidc/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Authentication OpenID Connect</title>
<style type="text/css">

Expand Down
6 changes: 6 additions & 0 deletions auth_oidc/tests/test_auth_oidc_auth_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ def test_auth_link(self):
self.assertTrue(params["state"])
self.assertEqual(params["redirect_uri"], [BASE_URL + "/auth_oauth/signin"])

def test_group_expression(self):
"""Test that group expressions evaluate correctly"""
group_line = self.env.ref('auth_oidc.local_keycloak').group_line_ids[:1]
group_line.expression = 'token["test"]["test"] == 1'
self.assertFalse(group_line._eval_expression(self.env.user, {}))

@responses.activate
def test_login(self):
"""Test that login works"""
Expand Down
10 changes: 10 additions & 0 deletions auth_oidc/views/auth_oauth_provider.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
<field name="token_endpoint" />
<field name="jwks_uri" />
</field>
<xpath expr="//sheet/group[last()]" position="after">
<group name="mappings">
<field name="group_line_ids">
<tree editable="bottom">
<field name="group_id" />
<field name="expression" placeholder="token['scope']['value'] == 'hello world'" />
</tree>
</field>
</group>
</xpath>
</field>
</record>
</odoo>