Browse Source

Merge pull request #1032 from simahawk/add-auth_oauth_multi_token

[add] auth_oauth_multi_token
pull/1288/head
Guewen Baconnier 7 years ago
committed by GitHub
parent
commit
5ba814a998
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      auth_oauth_multi_token/README.rst
  2. 3
      auth_oauth_multi_token/__init__.py
  3. 22
      auth_oauth_multi_token/__manifest__.py
  4. 4
      auth_oauth_multi_token/models/__init__.py
  5. 63
      auth_oauth_multi_token/models/auth_oauth_multi_token.py
  6. 91
      auth_oauth_multi_token/models/res_users.py
  7. 2
      auth_oauth_multi_token/security/ir.model.access.csv
  8. 1
      auth_oauth_multi_token/tests/__init__.py
  9. 103
      auth_oauth_multi_token/tests/test_multi_token.py
  10. 28
      auth_oauth_multi_token/views/res_users.xml

60
auth_oauth_multi_token/README.rst

@ -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.

3
auth_oauth_multi_token/__init__.py

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

22
auth_oauth_multi_token/__manifest__.py

@ -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,
}

4
auth_oauth_multi_token/models/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import auth_oauth_multi_token
from . import res_users

63
auth_oauth_multi_token/models/auth_oauth_multi_token.py

@ -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
})

91
auth_oauth_multi_token/models/res_users.py

@ -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'}

2
auth_oauth_multi_token/security/ir.model.access.csv

@ -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

1
auth_oauth_multi_token/tests/__init__.py

@ -0,0 +1 @@
from . import test_multi_token

103
auth_oauth_multi_token/tests/test_multi_token.py

@ -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)

28
auth_oauth_multi_token/views/res_users.xml

@ -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>
Loading…
Cancel
Save