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