Browse Source
Merge pull request #1032 from simahawk/add-auth_oauth_multi_token
Merge pull request #1032 from simahawk/add-auth_oauth_multi_token
[add] auth_oauth_multi_tokenpull/1288/head
Guewen Baconnier
7 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 377 additions and 0 deletions
-
60auth_oauth_multi_token/README.rst
-
3auth_oauth_multi_token/__init__.py
-
22auth_oauth_multi_token/__manifest__.py
-
4auth_oauth_multi_token/models/__init__.py
-
63auth_oauth_multi_token/models/auth_oauth_multi_token.py
-
91auth_oauth_multi_token/models/res_users.py
-
2auth_oauth_multi_token/security/ir.model.access.csv
-
1auth_oauth_multi_token/tests/__init__.py
-
103auth_oauth_multi_token/tests/test_multi_token.py
-
28auth_oauth_multi_token/views/res_users.xml
@ -0,0 +1,60 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
================= |
||||
|
OAuth multi token |
||||
|
================= |
||||
|
|
||||
|
This module adds the possibility to connect with the same account |
||||
|
on more than one device at the same time. |
||||
|
|
||||
|
All providers are supported (Google, Facebook, Odoo, etc). |
||||
|
|
||||
|
Configuration and usage |
||||
|
======================= |
||||
|
|
||||
|
On users' form you can set the number of maximum simultaneous connections. |
||||
|
|
||||
|
By default 5 connections are allowed. |
||||
|
|
||||
|
From there you can also clear / inactivate existing tokens. |
||||
|
|
||||
|
Nothing changes on login action: just select your provider and try to log in. |
||||
|
|
||||
|
Bug Tracker |
||||
|
=========== |
||||
|
|
||||
|
Bugs are tracked on `GitHub Issues |
||||
|
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please |
||||
|
check there if your issue has already been reported. If you spotted it first, |
||||
|
help us smash it by providing detailed and welcomed feedback. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Images |
||||
|
------ |
||||
|
|
||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Florent de Labarre <florent@iguanayachts.com |
||||
|
* Simone Orsi <simone.orsi@camptocamp.com> |
||||
|
|
||||
|
Maintainer |
||||
|
---------- |
||||
|
|
||||
|
.. image:: https://odoo-community.org/logo.png |
||||
|
:alt: Odoo Community Association |
||||
|
:target: https://odoo-community.org |
||||
|
|
||||
|
This module is maintained by the OCA. |
||||
|
|
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose |
||||
|
mission is to support the collaborative development of Odoo features and |
||||
|
promote its widespread use. |
||||
|
|
||||
|
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,3 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import models |
@ -0,0 +1,22 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Florent de Labarre |
||||
|
# Copyright 2017 Camptocamp |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
||||
|
|
||||
|
{ |
||||
|
'name': 'OAuth Multi Token', |
||||
|
'version': '10.0.1.0.0', |
||||
|
'license': 'AGPL-3', |
||||
|
'author': 'Florent de Labarre, ' |
||||
|
'Camptocamp, ' |
||||
|
'Odoo Community Association (OCA)', |
||||
|
'summary': """Allow multiple connection with the same OAuth account""", |
||||
|
'category': 'Tool', |
||||
|
'website': 'https://github.com/OCA/server-tools', |
||||
|
'depends': ['auth_oauth'], |
||||
|
'data': [ |
||||
|
'views/res_users.xml', |
||||
|
'security/ir.model.access.csv', |
||||
|
], |
||||
|
'installable': True, |
||||
|
} |
@ -0,0 +1,4 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
|
||||
|
from . import auth_oauth_multi_token |
||||
|
from . import res_users |
@ -0,0 +1,63 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Florent de Labarre |
||||
|
# Copyright 2017 Camptocamp |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
||||
|
|
||||
|
from odoo import api, fields, models |
||||
|
|
||||
|
|
||||
|
class AuthOauthMultiToken(models.Model): |
||||
|
"""Define a set of tokens.""" |
||||
|
|
||||
|
_name = 'auth.oauth.multi.token' |
||||
|
_description = 'OAuth2 token' |
||||
|
_order = 'id desc' |
||||
|
|
||||
|
EMPTY_OAUTH_TOKEN = '****************************' |
||||
|
|
||||
|
oauth_access_token = fields.Char( |
||||
|
string='OAuth Access Token', |
||||
|
readonly=True, |
||||
|
copy=False |
||||
|
) |
||||
|
user_id = fields.Many2one( |
||||
|
comodel_name='res.users', |
||||
|
string='User', |
||||
|
required=True |
||||
|
) |
||||
|
active_token = fields.Boolean('Active') |
||||
|
|
||||
|
@api.model |
||||
|
def create(self, vals): |
||||
|
"""Override to validate tokens.""" |
||||
|
token = super(AuthOauthMultiToken, self).create(vals) |
||||
|
token._oauth_validate_multi_token() |
||||
|
return token |
||||
|
|
||||
|
@api.model |
||||
|
def _oauth_user_tokens(self, user_id, active=True): |
||||
|
"""Retrieve tokens for given user. |
||||
|
|
||||
|
:param user_id: Odoo ID of the user |
||||
|
:param active: retrieve active or inactive tokens |
||||
|
""" |
||||
|
return self.search([ |
||||
|
('user_id', '=', user_id), |
||||
|
('active_token', '=', active) |
||||
|
]) |
||||
|
|
||||
|
def _oauth_validate_multi_token(self): |
||||
|
"""Check current user's token and clear them if max number reached.""" |
||||
|
user_tokens = self._oauth_user_tokens(self.user_id.id) |
||||
|
max_token = self.user_id.oauth_access_max_token |
||||
|
if user_tokens and len(user_tokens) > max_token: |
||||
|
# clear last token |
||||
|
user_tokens[max_token - 1]._oauth_clear_token() |
||||
|
|
||||
|
@api.multi |
||||
|
def _oauth_clear_token(self): |
||||
|
"""Disable current token records.""" |
||||
|
self.write({ |
||||
|
'oauth_access_token': self.EMPTY_OAUTH_TOKEN, |
||||
|
'active_token': False |
||||
|
}) |
@ -0,0 +1,91 @@ |
|||||
|
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Florent de Labarre |
||||
|
# Copyright 2017 Camptocamp |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
||||
|
import uuid |
||||
|
from odoo import api, fields, models, exceptions |
||||
|
from odoo.addons import base |
||||
|
|
||||
|
|
||||
|
base.res.res_users.USER_PRIVATE_FIELDS.\ |
||||
|
append('oauth_master_uuid') |
||||
|
|
||||
|
|
||||
|
class ResUsers(models.Model): |
||||
|
_inherit = 'res.users' |
||||
|
|
||||
|
oauth_access_token_ids = fields.One2many( |
||||
|
comodel_name='auth.oauth.multi.token', |
||||
|
inverse_name='user_id', |
||||
|
string='OAuth tokens', |
||||
|
copy=False |
||||
|
) |
||||
|
oauth_access_max_token = fields.Integer( |
||||
|
string='Max number of simultaneous connections', |
||||
|
default=10, |
||||
|
required=True |
||||
|
) |
||||
|
oauth_master_uuid = fields.Char( |
||||
|
string='Master UUID', |
||||
|
copy=False, |
||||
|
readonly=True, |
||||
|
required=True, |
||||
|
default=lambda self: self._generate_oauth_master_uuid(), |
||||
|
) |
||||
|
|
||||
|
def _generate_oauth_master_uuid(self): |
||||
|
return uuid.uuid4().hex |
||||
|
|
||||
|
@property |
||||
|
def multi_token_model(self): |
||||
|
return self.env['auth.oauth.multi.token'] |
||||
|
|
||||
|
@api.model |
||||
|
def _auth_oauth_signin(self, provider, validation, params): |
||||
|
"""Override to handle sign-in with multi token.""" |
||||
|
res = super(ResUsers, self)._auth_oauth_signin( |
||||
|
provider, validation, params) |
||||
|
|
||||
|
oauth_uid = validation['user_id'] |
||||
|
# Lookup for user by oauth uid and provider |
||||
|
user = self.search([ |
||||
|
('oauth_uid', '=', oauth_uid), |
||||
|
('oauth_provider_id', '=', provider)] |
||||
|
) |
||||
|
if not user: |
||||
|
raise exceptions.AccessDenied() |
||||
|
user.ensure_one() |
||||
|
# user found and unique: create a token |
||||
|
self.multi_token_model.create({ |
||||
|
'user_id': user.id, |
||||
|
'oauth_access_token': params['access_token'], |
||||
|
'active_token': True, |
||||
|
}) |
||||
|
return res |
||||
|
|
||||
|
@api.multi |
||||
|
def action_oauth_clear_token(self): |
||||
|
"""Inactivate current user tokens.""" |
||||
|
self.mapped('oauth_access_token_ids')._oauth_clear_token() |
||||
|
for res in self: |
||||
|
res.oauth_master_uuid = self._generate_oauth_master_uuid() |
||||
|
|
||||
|
@api.model |
||||
|
def check_credentials(self, password): |
||||
|
"""Override to check credentials against multi tokens.""" |
||||
|
try: |
||||
|
return super(ResUsers, self).check_credentials(password) |
||||
|
except exceptions.AccessDenied: |
||||
|
res = self.multi_token_model.sudo().search([ |
||||
|
('user_id', '=', self.env.uid), |
||||
|
('oauth_access_token', '=', password), |
||||
|
('active_token', '=', True), |
||||
|
]) |
||||
|
if not res: |
||||
|
raise |
||||
|
|
||||
|
def _get_session_token_fields(self): |
||||
|
res = super(ResUsers, self)._get_session_token_fields() |
||||
|
res.remove('oauth_access_token') |
||||
|
return res | {'oauth_master_uuid'} |
@ -0,0 +1,2 @@ |
|||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
||||
|
access_auth_oauth_multi_token_admin,auth_oauth_multi_token admin,model_auth_oauth_multi_token,base.group_system,1,1,1,1 |
@ -0,0 +1 @@ |
|||||
|
from . import test_multi_token |
@ -0,0 +1,103 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Camptocamp |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) |
||||
|
|
||||
|
from odoo.tests.common import SavepointCase |
||||
|
from odoo import exceptions |
||||
|
import json |
||||
|
|
||||
|
|
||||
|
class TestMultiToken(SavepointCase): |
||||
|
|
||||
|
post_install = True |
||||
|
at_install = False |
||||
|
|
||||
|
@classmethod |
||||
|
def setUpClass(cls): |
||||
|
super(TestMultiToken, cls).setUpClass() |
||||
|
cls.token_model = cls.env['auth.oauth.multi.token'] |
||||
|
cls.provider_google = cls.env.ref('auth_oauth.provider_google') |
||||
|
cls.user_model = cls.env['res.users'].with_context({ |
||||
|
'tracking_disable': True, |
||||
|
'no_reset_password': True, |
||||
|
}) |
||||
|
cls.user = cls.user_model.create({ |
||||
|
'name': 'John Doe', |
||||
|
'login': 'johndoe', |
||||
|
'oauth_uid': 'oauth_uid_johndoe', |
||||
|
'oauth_provider_id': cls.provider_google.id, |
||||
|
}) |
||||
|
|
||||
|
def _fake_params(self, **kw): |
||||
|
params = { |
||||
|
'state': json.dumps({'t': 'FAKE_TOKEN'}), |
||||
|
'access_token': 'FAKE_ACCESS_TOKEN', |
||||
|
} |
||||
|
params.update(kw) |
||||
|
return params |
||||
|
|
||||
|
def test_no_provider_no_access(self): |
||||
|
validation = { |
||||
|
'user_id': 'oauth_uid_no_one', |
||||
|
} |
||||
|
params = self._fake_params() |
||||
|
with self.assertRaises(exceptions.AccessDenied): |
||||
|
self.user_model._auth_oauth_signin( |
||||
|
self.provider_google.id, validation, params |
||||
|
) |
||||
|
|
||||
|
def _test_one_token(self): |
||||
|
validation = { |
||||
|
'user_id': 'oauth_uid_johndoe', |
||||
|
} |
||||
|
params = self._fake_params() |
||||
|
login = self.user_model._auth_oauth_signin( |
||||
|
self.provider_google.id, validation, params |
||||
|
) |
||||
|
self.assertEqual(login, 'johndoe') |
||||
|
|
||||
|
def test_access_one_token(self): |
||||
|
# no token yet |
||||
|
self.assertFalse(self.user.oauth_access_token_ids) |
||||
|
self._test_one_token() |
||||
|
token_count = 1 |
||||
|
self.assertEqual( |
||||
|
len(self.user.oauth_access_token_ids), |
||||
|
token_count) |
||||
|
self.assertEqual( |
||||
|
len(self.token_model._oauth_user_tokens(self.user.id)), |
||||
|
token_count) |
||||
|
|
||||
|
def test_access_multi_token(self): |
||||
|
# no token yet |
||||
|
self.assertFalse(self.user.oauth_access_token_ids) |
||||
|
# use as many token as max allowed |
||||
|
for token_count in range(1, self.user.oauth_access_max_token + 1): |
||||
|
self._test_one_token() |
||||
|
self.assertEqual( |
||||
|
len(self.user.oauth_access_token_ids), |
||||
|
token_count) |
||||
|
self.assertEqual( |
||||
|
len(self.token_model._oauth_user_tokens(self.user.id)), |
||||
|
token_count) |
||||
|
# exceed the number |
||||
|
self._test_one_token() |
||||
|
# token count match max number + 1 |
||||
|
self.assertEqual( |
||||
|
len(self.user.oauth_access_token_ids), |
||||
|
self.user.oauth_access_max_token + 1) |
||||
|
# but active tokens don't |
||||
|
self.assertEqual( |
||||
|
len(self.token_model._oauth_user_tokens(self.user.id)), |
||||
|
self.user.oauth_access_max_token) |
||||
|
|
||||
|
def test_remove_oauth_access_token(self): |
||||
|
res = self.user._get_session_token_fields() |
||||
|
self.assertFalse('oauth_access_token' in res) |
||||
|
self.assertTrue('oauth_master_uuid' in res) |
||||
|
|
||||
|
def test_action_oauth_clear_token(self): |
||||
|
self.user.action_oauth_clear_token() |
||||
|
active_token = self.user.oauth_access_token_ids.filtered( |
||||
|
lambda x: x.active_token) |
||||
|
self.assertEqual(len(active_token), 0) |
@ -0,0 +1,28 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<odoo> |
||||
|
|
||||
|
<record id="user_oauth_multi_token_form" model="ir.ui.view"> |
||||
|
<field name="name">auth_oauth_multi_token user form</field> |
||||
|
<field name="model">res.users</field> |
||||
|
<field name="type">form</field> |
||||
|
<field name="inherit_id" ref="auth_oauth.view_users_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="oauth_uid" position="after"> |
||||
|
<field name="oauth_access_max_token" /> |
||||
|
</field> |
||||
|
<xpath expr="//field[@name='oauth_provider_id']/.." position="after"> |
||||
|
<group name="multi_token_info" string="Latest tokens"> |
||||
|
<field name="oauth_access_token_ids" nolabel="1" options="{'no_create': True, 'no_open': True}"> |
||||
|
<tree limit="10"> |
||||
|
<field name="create_date"/> |
||||
|
<field name="oauth_access_token"/> |
||||
|
<field name="active_token"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
<button string="Clear Tokens" type="object" name="action_oauth_clear_token" class="oe_highlight"/> |
||||
|
</group> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue