From 87cabaa1783af5b9b241dbaff9fc939c7ca26e56 Mon Sep 17 00:00:00 2001 From: Maciej Wawro Date: Mon, 12 Nov 2018 23:48:29 +0100 Subject: [PATCH] [openid_connect] --- galicea_openid_connect/__init__.py | 5 + galicea_openid_connect/__manifest__.py | 49 +++ galicea_openid_connect/api.py | 82 ++++ .../controllers/__init__.py | 4 + .../controllers/ext_web_login.py | 17 + galicea_openid_connect/controllers/main.py | 355 ++++++++++++++++++ galicea_openid_connect/models/__init__.py | 4 + galicea_openid_connect/models/access_token.py | 64 ++++ galicea_openid_connect/models/client.py | 65 ++++ galicea_openid_connect/random_tokens.py | 14 + galicea_openid_connect/requirements.txt | 2 + galicea_openid_connect/security/__init__.py | 23 ++ galicea_openid_connect/security/init.yml | 4 + .../security/ir.model.access.csv | 5 + galicea_openid_connect/security/security.xml | 35 ++ .../static/description/icon.png | Bin 0 -> 3659 bytes .../description/images/client_screenshot.png | Bin 0 -> 48086 bytes .../description/images/login_screenshot.png | Bin 0 -> 23019 bytes .../description/images/master_screenshot.png | Bin 0 -> 17123 bytes .../static/description/index.html | 133 +++++++ galicea_openid_connect/system_checks.py | 24 ++ galicea_openid_connect/views/templates.xml | 24 ++ galicea_openid_connect/views/views.xml | 65 ++++ 23 files changed, 974 insertions(+) create mode 100644 galicea_openid_connect/__init__.py create mode 100644 galicea_openid_connect/__manifest__.py create mode 100644 galicea_openid_connect/api.py create mode 100644 galicea_openid_connect/controllers/__init__.py create mode 100644 galicea_openid_connect/controllers/ext_web_login.py create mode 100644 galicea_openid_connect/controllers/main.py create mode 100644 galicea_openid_connect/models/__init__.py create mode 100644 galicea_openid_connect/models/access_token.py create mode 100644 galicea_openid_connect/models/client.py create mode 100644 galicea_openid_connect/random_tokens.py create mode 100644 galicea_openid_connect/requirements.txt create mode 100644 galicea_openid_connect/security/__init__.py create mode 100644 galicea_openid_connect/security/init.yml create mode 100644 galicea_openid_connect/security/ir.model.access.csv create mode 100644 galicea_openid_connect/security/security.xml create mode 100755 galicea_openid_connect/static/description/icon.png create mode 100644 galicea_openid_connect/static/description/images/client_screenshot.png create mode 100644 galicea_openid_connect/static/description/images/login_screenshot.png create mode 100644 galicea_openid_connect/static/description/images/master_screenshot.png create mode 100644 galicea_openid_connect/static/description/index.html create mode 100644 galicea_openid_connect/system_checks.py create mode 100644 galicea_openid_connect/views/templates.xml create mode 100644 galicea_openid_connect/views/views.xml diff --git a/galicea_openid_connect/__init__.py b/galicea_openid_connect/__init__.py new file mode 100644 index 0000000..26e263a --- /dev/null +++ b/galicea_openid_connect/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models +from . import system_checks diff --git a/galicea_openid_connect/__manifest__.py b/galicea_openid_connect/__manifest__.py new file mode 100644 index 0000000..9b4976d --- /dev/null +++ b/galicea_openid_connect/__manifest__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Galicea OpenID Connect Provider", + + 'summary': """OpenID Connect Provider and OAuth2 resource server""", + + 'author': "Maciej Wawro", + 'maintainer': "Galicea", + 'website': "http://galicea.pl", + + 'category': 'Technical Settings', + 'version': '10.0.1.0', + + 'depends': ['web', 'galicea_environment_checkup'], + + 'external_dependencies': { + 'python': ['jwcrypto', 'cryptography'] + }, + + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'security/init.yml', + + 'views/views.xml', + 'views/templates.xml' + ], + + 'environment_checkup': { + 'dependencies': { + 'python': [ + { + 'name': 'jwcrypto', + 'install': "pip install 'jwcrypto==0.5.0'" + }, + { + 'name': 'cryptography', + 'version': '>=2.3', + 'install': "pip install 'cryptography>=2.3'" + } + ] + } + }, + + 'images': [ + 'static/description/images/custom_screenshot.png', + 'static/description/images/dependencies_screenshot.png' + ] +} diff --git a/galicea_openid_connect/api.py b/galicea_openid_connect/api.py new file mode 100644 index 0000000..36b7743 --- /dev/null +++ b/galicea_openid_connect/api.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +import json +import logging +from functools import wraps + +from odoo import http +import werkzeug + +_logger = logging.getLogger(__name__) + +class ApiException(Exception): + INVALID_REQUEST = 'invalid_request' + + def __init__(self, message, code=None): + super(Exception, self).__init__(message) + self.code = code if code else self.INVALID_REQUEST + +def resource(path, method, auth='user'): + assert auth in ['user', 'client'] + + def endpoint_decorator(func): + @http.route(path, auth='public', type='http', methods=[method], csrf=False) + @wraps(func) + def func_wrapper(self, req, **query): + try: + access_token = None + if 'Authorization' in req.httprequest.headers: + authorization_header = req.httprequest.headers['Authorization'] + if authorization_header[:7] == 'Bearer ': + access_token = authorization_header.split(' ', 1)[1] + if access_token is None: + access_token = query.get('access_token') + if not access_token: + raise ApiException( + 'access_token param is missing', + 'invalid_request', + ) + if auth == 'user': + token = req.env['galicea_openid_connect.access_token'].sudo().search( + [('token', '=', access_token)] + ) + if not token: + raise ApiException( + 'access_token is invalid', + 'invalid_request', + ) + req.uid = token.user_id.id + elif auth == 'client': + token = req.env['galicea_openid_connect.client_access_token'].sudo().search( + [('token', '=', access_token)] + ) + if not token: + raise ApiException( + 'access_token is invalid', + 'invalid_request', + ) + req.uid = token.client_id.system_user_id.id + + ctx = req.context.copy() + ctx.update({'client_id': token.client_id.id}) + req.context = ctx + + response = func(self, req, **query) + return json.dumps(response) + except ApiException as e: + return werkzeug.Response( + response=json.dumps({'error': e.code, 'error_message': e.message}), + status=400, + ) + except: + _logger.exception('Unexpected exception while processing API request') + return werkzeug.Response( + response=json.dumps({ + 'error': 'server_error', + 'error_message': 'Unexpected server error', + }), + status=500, + ) + + return func_wrapper + return endpoint_decorator diff --git a/galicea_openid_connect/controllers/__init__.py b/galicea_openid_connect/controllers/__init__.py new file mode 100644 index 0000000..00fd0df --- /dev/null +++ b/galicea_openid_connect/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import ext_web_login +from . import main diff --git a/galicea_openid_connect/controllers/ext_web_login.py b/galicea_openid_connect/controllers/ext_web_login.py new file mode 100644 index 0000000..60bcde9 --- /dev/null +++ b/galicea_openid_connect/controllers/ext_web_login.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import time + +from odoo import http +from odoo.addons import web + +class Home(web.controllers.main.Home): + + @http.route('/web/login', type='http', auth="none") + def web_login(self, redirect=None, **kw): + result = super(Home, self).web_login(redirect, **kw) + if result.is_qweb and 'force_auth_and_redirect' in kw: + result.qcontext['redirect'] = kw['force_auth_and_redirect'] + if http.request.params.get('login_success'): + http.request.session['auth_time'] = int(time.time()) + return result diff --git a/galicea_openid_connect/controllers/main.py b/galicea_openid_connect/controllers/main.py new file mode 100644 index 0000000..a7ec142 --- /dev/null +++ b/galicea_openid_connect/controllers/main.py @@ -0,0 +1,355 @@ +# -*- coding: utf-8 -*- + +import json +import time +import os +import base64 + +from odoo import http +import werkzeug + +from .. api import resource + +try: + from jwcrypto import jwk, jwt + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import hashes +except ImportError: + pass + +def jwk_from_json(json_key): + key = jwk.JWK() + key.import_key(**json.loads(json_key)) + return key + +def jwt_encode(claims, key): + token = jwt.JWT( + header={'alg': key._params['alg'], 'kid': key._params['kid']}, + claims=claims + ) + token.make_signed_token(key) + return token.serialize() + +def jwt_decode(serialized, key): + token = jwt.JWT(jwt=serialized, key=key) + return json.loads(token.claims) + +RESPONSE_TYPES_SUPPORTED = [ + 'code', + 'token', + 'id_token token', + 'id_token' +] + +class OAuthException(Exception): + INVALID_REQUEST = 'invalid_request' + INVALID_CLIENT = 'invalid_client' + UNSUPPORTED_RESPONSE_TYPE = 'unsupported_response_type' + INVALID_GRANT = 'invalid_grant' + UNSUPPORTED_GRANT_TYPE = 'unsupported_grant_type' + + def __init__(self, message, type): + super(Exception, self).__init__(message) + self.type = type + +class Main(http.Controller): + def __get_authorization_code_jwk(self, req): + return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param( + 'galicea_openid_connect.authorization_code_jwk' + )) + + def __get_id_token_jwk(self, req): + return jwk_from_json(req.env['ir.config_parameter'].sudo().get_param( + 'galicea_openid_connect.id_token_jwk' + )) + + def __validate_client(self, req, **query): + if 'client_id' not in query: + raise OAuthException( + 'client_id param is missing', + OAuthException.INVALID_CLIENT, + ) + client_id = query['client_id'] + client = req.env['galicea_openid_connect.client'].sudo().search( + [('client_id', '=', client_id)] + ) + if not client: + raise OAuthException( + 'client_id param is invalid', + OAuthException.INVALID_CLIENT, + ) + return client + + def __validate_redirect_uri(self, client, req, **query): + if 'redirect_uri' not in query: + raise OAuthException( + 'redirect_uri param is missing', + OAuthException.INVALID_GRANT, + ) + + redirect_uri = query['redirect_uri'] + if client.auth_redirect_uri != redirect_uri: + raise OAuthException( + 'redirect_uri param doesn\'t match the pre-configured redirect URI', + OAuthException.INVALID_GRANT, + ) + + return redirect_uri + + def __validate_client_secret(self, client, req, **query): + if 'client_secret' not in query or query['client_secret'] != client.secret: + raise OAuthException( + 'client_secret param is not valid', + OAuthException.INVALID_CLIENT, + ) + + @http.route('/.well-known/openid-configuration', auth='public', type='http') + def metadata(self, req, **query): + base_url = http.request.httprequest.host_url + data = { + 'issuer': base_url, + 'authorization_endpoint': base_url + 'oauth/authorize', + 'token_endpoint': base_url + 'oauth/token', + 'userinfo_endpoint': base_url + 'oauth/userinfo', + 'jwks_uri': base_url + 'oauth/jwks', + 'scopes_supported': ['openid'], + 'response_types_supported': RESPONSE_TYPES_SUPPORTED, + 'grant_types_supported': ['authorization_code', 'implicit'], + 'subject_types_supported': ['public'], + 'id_token_signing_alg_values_supported': ['RS256'], + 'token_endpoint_auth_methods_supported': ['client_secret_post'] + } + return json.dumps(data) + + @http.route('/oauth/jwks', auth='public', type='http') + def jwks(self, req, **query): + keyset = jwk.JWKSet() + keyset.add(self.__get_id_token_jwk(req)) + return keyset.export(private_keys=False) + + @resource('/oauth/userinfo', method='GET') + def userinfo(self, req, **query): + user = req.env.user + values = { + 'sub': str(user.id), + # Needed in case the client is another Odoo instance + 'user_id': str(user.id), + 'name': user.name, + } + if user.email: + values['email'] = user.email + return values + + @resource('/oauth/clientinfo', method='GET', auth='client') + def clientinfo(self, req, **query): + client = req.env['galicea_openid_connect.client'].browse(req.context['client_id']) + return { + 'name': client.name + } + + @http.route('/oauth/authorize', auth='public', type='http', csrf=False) + def authorize(self, req, **query): + # First, validate client_id and redirect_uri params. + try: + client = self.__validate_client(req, **query) + redirect_uri = self.__validate_redirect_uri(client, req, **query) + except OAuthException as e: + # If those are not valid, we must not redirect back to the client + # - instead, we display a message to the user + return req.render('galicea_openid_connect.error', {'exception': e}) + + scopes = query['scope'].split(' ') if query.get('scope') else [] + is_openid_request = 'openid' in scopes + + # state, if present, is just mirrored back to the client + response_params = {} + if 'state' in query: + response_params['state'] = query['state'] + + response_mode = query.get('response_mode') + try: + if response_mode and response_mode not in ['query', 'fragment']: + response_mode = None + raise OAuthException( + 'The only supported response_modes are \'query\' and \'fragment\'', + OAuthException.INVALID_REQUEST + ) + + if 'response_type' not in query: + raise OAuthException( + 'response_type param is missing', + OAuthException.INVALID_REQUEST, + ) + response_type = query['response_type'] + if response_type not in RESPONSE_TYPES_SUPPORTED: + raise OAuthException( + 'The only supported response_types are: {}'.format(', '.join(RESPONSE_TYPES_SUPPORTED)), + OAuthException.UNSUPPORTED_RESPONSE_TYPE, + ) + except OAuthException as e: + response_params['error'] = e.type + response_params['error_description'] = e.message + return self.__redirect(redirect_uri, response_params, response_mode or 'query') + + if not response_mode: + response_mode = 'query' if response_type == 'code' else 'fragment' + + user = req.env.user + # In case user is not logged in, we redirect to the login page and come back + needs_login = user.login == 'public' + # Also if they didn't authenticate recently enough + if 'max_age' in query and http.request.session.get('auth_time', 0) + int(query['max_age']) < time.time(): + needs_login = True + if needs_login: + params = { + 'force_auth_and_redirect': '/oauth/authorize?{}'.format(werkzeug.url_encode(query)) + } + return self.__redirect('/web/login', params, 'query') + + response_types = response_type.split() + + extra_claims = { + 'sid': http.request.httprequest.session.sid, + } + if 'nonce' in query: + extra_claims['nonce'] = query['nonce'] + + if 'code' in response_types: + # Generate code that can be used by the client server to retrieve + # the token. It's set to be valid for 60 seconds only. + # TODO: The spec says the code should be single-use. We're not enforcing + # that here. + payload = { + 'redirect_uri': redirect_uri, + 'client_id': client.client_id, + 'user_id': user.id, + 'scopes': scopes, + 'exp': int(time.time()) + 60 + } + payload.update(extra_claims) + key = self.__get_authorization_code_jwk(req) + response_params['code'] = jwt_encode(payload, key) + if 'token' in response_types: + access_token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( + user.id, + client.id + ).token + response_params['access_token'] = access_token + response_params['token_type'] = 'bearer' + + digest = hashes.Hash(hashes.SHA256(), backend=default_backend()) + digest.update(access_token.encode('ascii')) + at_hash = digest.finalize() + extra_claims['at_hash'] = base64.urlsafe_b64encode(at_hash[:16]).strip('=') + if 'id_token' in response_types: + response_params['id_token'] = self.__create_id_token(req, user.id, client, extra_claims) + + return self.__redirect(redirect_uri, response_params, response_mode) + + @http.route('/oauth/token', auth='public', type='http', methods=['POST'], csrf=False) + def token(self, req, **query): + try: + if 'grant_type' not in query: + raise OAuthException( + 'grant_type param is missing', + OAuthException.INVALID_REQUEST, + ) + if query['grant_type'] == 'authorization_code': + return json.dumps(self.__handle_grant_type_authorization_code(req, **query)) + elif query['grant_type'] == 'client_credentials': + return json.dumps(self.__handle_grant_type_client_credentials(req, **query)) + else: + raise OAuthException( + 'Unsupported grant_type param: \'{}\''.format(query['grant_type']), + OAuthException.UNSUPPORTED_GRANT_TYPE, + ) + except OAuthException as e: + body = json.dumps({'error': e.type, 'error_description': e.message}) + return werkzeug.Response(response=body, status=400) + + def __handle_grant_type_authorization_code(self, req, **query): + client = self.__validate_client(req, **query) + redirect_uri = self.__validate_redirect_uri(client, req, **query) + self.__validate_client_secret(client, req, **query) + + if 'code' not in query: + raise OAuthException( + 'code param is missing', + OAuthException.INVALID_GRANT, + ) + try: + payload = jwt_decode(query['code'], self.__get_authorization_code_jwk(req)) + except jwt.JWTExpired: + raise OAuthException( + 'Code expired', + OAuthException.INVALID_GRANT, + ) + except ValueError: + raise OAuthException( + 'code malformed', + OAuthException.INVALID_GRANT, + ) + if payload['client_id'] != client.client_id: + raise OAuthException( + 'client_id doesn\'t match the authorization request', + OAuthException.INVALID_GRANT, + ) + if payload['redirect_uri'] != redirect_uri: + raise OAuthException( + 'redirect_uri doesn\'t match the authorization request', + OAuthException.INVALID_GRANT, + ) + + # Retrieve/generate access token. We currently only store one per user/client + token = req.env['galicea_openid_connect.access_token'].sudo().retrieve_or_create( + payload['user_id'], + client.id + ) + response = { + 'access_token': token.token, + 'token_type': 'bearer' + } + if 'openid' in payload['scopes']: + extra_claims = { name: payload[name] for name in payload if name in ['sid', 'nonce'] } + response['id_token'] = self.__create_id_token(req, payload['user_id'], client, extra_claims) + return response + + def __handle_grant_type_client_credentials(self, req, **query): + client = self.__validate_client(req, **query) + self.__validate_client_secret(client, req, **query) + token = req.env['galicea_openid_connect.client_access_token'].sudo().retrieve_or_create(client.id) + return { + 'access_token': token.token, + 'token_type': 'bearer' + } + + def __create_id_token(self, req, user_id, client, extra_claims): + claims = { + 'iss': http.request.httprequest.host_url, + 'sub': str(user_id), + 'aud': client.client_id, + 'iat': int(time.time()), + 'exp': int(time.time()) + 15 * 60 + } + auth_time = extra_claims.get('sid') and http.root.session_store.get(extra_claims['sid']).get('auth_time') + if auth_time: + claims['auth_time'] = auth_time + if 'nonce' in extra_claims: + claims['nonce'] = extra_claims['nonce'] + if 'at_hash' in extra_claims: + claims['at_hash'] = extra_claims['at_hash'] + + key = self.__get_id_token_jwk(req) + return jwt_encode(claims, key) + + def __redirect(self, url, params, response_mode): + location = '{}{}{}'.format( + url, + '?' if response_mode == 'query' else '#', + werkzeug.url_encode(params) + ) + return werkzeug.Response( + headers={'Location': location}, + response=None, + status=302, + ) diff --git a/galicea_openid_connect/models/__init__.py b/galicea_openid_connect/models/__init__.py new file mode 100644 index 0000000..c5f0513 --- /dev/null +++ b/galicea_openid_connect/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import client +from . import access_token diff --git a/galicea_openid_connect/models/access_token.py b/galicea_openid_connect/models/access_token.py new file mode 100644 index 0000000..36869e9 --- /dev/null +++ b/galicea_openid_connect/models/access_token.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api +from .. import random_tokens + +class AccessTokenBase(models.AbstractModel): + _name = 'galicea_openid_connect.access_token_base' + + token = fields.Char( + readonly=True, + required=True, + default=lambda _: random_tokens.alpha_numeric(64), + index=True, + ) + client_id = fields.Many2one( + 'galicea_openid_connect.client', + readonly=True, + index=True, + required=True, + ondelete='cascade' + ) + +class AccessToken(models.Model): + _inherit = 'galicea_openid_connect.access_token_base' + _name = 'galicea_openid_connect.access_token' + _description = 'Acccess token representing user-client pair' + + user_id = fields.Many2one( + 'res.users', + required=True, + readonly=True, + index=True, + ondelete='cascade' + ) + + @api.model + def retrieve_or_create(self, user_id, client_id): + existing_tokens = self.search( + [ + ('user_id', '=', user_id), + ('client_id', '=', client_id), + ] + ) + if existing_tokens: + return existing_tokens[0] + else: + return self.create({'user_id': user_id, 'client_id': client_id}) + +class ClientAccessToken(models.Model): + _inherit = 'galicea_openid_connect.access_token_base' + _name = 'galicea_openid_connect.client_access_token' + _description = 'Access token representing client credentials' + + @api.model + def retrieve_or_create(self, client_id): + existing_tokens = self.search( + [ + ('client_id', '=', client_id), + ] + ) + if existing_tokens: + return existing_tokens[0] + else: + return self.create({'client_id': client_id}) diff --git a/galicea_openid_connect/models/client.py b/galicea_openid_connect/models/client.py new file mode 100644 index 0000000..470c8db --- /dev/null +++ b/galicea_openid_connect/models/client.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api +from .. import random_tokens + +class Client(models.Model): + _name = 'galicea_openid_connect.client' + _description = 'OpenID Connect client' + + name = fields.Char(required=True) + auth_redirect_uri = fields.Char('Redirect URI for user login') + client_id = fields.Char( + string='Client ID', + required=True, + readonly=True, + index=True, + default=lambda _: random_tokens.lower_case(16), + ) + secret = fields.Char( + string='Client secret', + required=True, + readonly=True, + default=lambda _: random_tokens.alpha_numeric(32), + groups='galicea_openid_connect.group_admin' + ) + system_user_id = fields.Many2one( + 'res.users', + 'Artificial user representing the client in client credentials requests', + readonly=True, + required=True, + ondelete='restrict' + ) + + @api.model + def __system_user_name(self, client_name): + return '{} - API system user'.format(client_name) + + @api.model + def create(self, values): + if 'name' in values: + system_user = self.env['res.users'].create({ + 'name': self.__system_user_name(values['name']), + 'login': random_tokens.lower_case(8), + 'groups_id': [(4, self.env.ref('galicea_openid_connect.group_system_user').id)] + }) + # Do not include in the "Pending invitations" list + system_user.sudo(system_user.id)._update_last_login() + values['system_user_id'] = system_user.id + return super(Client, self).create(values) + + @api.multi + def write(selfs, values): + super(Client, selfs).write(values) + if 'name' in values: + selfs.mapped(lambda client: client.system_user_id).write({ + 'name': selfs.__system_user_name(values['name']) + }) + return True + + @api.multi + def unlink(selfs): + users_to_unlink = selfs.mapped(lambda client: client.system_user_id) + ret = super(Client, selfs).unlink() + users_to_unlink.unlink() + return ret diff --git a/galicea_openid_connect/random_tokens.py b/galicea_openid_connect/random_tokens.py new file mode 100644 index 0000000..d448b42 --- /dev/null +++ b/galicea_openid_connect/random_tokens.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from random import SystemRandom + +def random_token(length, byte_filter): + allowed_bytes = ''.join(c for c in map(chr, range(256)) if byte_filter(c)) + random = SystemRandom() + return ''.join([random.choice(allowed_bytes) for _ in range(length)]) + +def alpha_numeric(length): + return random_token(length, str.isalnum) + +def lower_case(length): + return random_token(length, str.islower) diff --git a/galicea_openid_connect/requirements.txt b/galicea_openid_connect/requirements.txt new file mode 100644 index 0000000..073e997 --- /dev/null +++ b/galicea_openid_connect/requirements.txt @@ -0,0 +1,2 @@ +cryptography>=2.3 +jwcrypto==0.5.0 diff --git a/galicea_openid_connect/security/__init__.py b/galicea_openid_connect/security/__init__.py new file mode 100644 index 0000000..90f35cf --- /dev/null +++ b/galicea_openid_connect/security/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from .. import random_tokens +try: + from jwcrypto import jwk +except ImportError: + pass + +def init_keys(IrConfigParameter): + keys = { + 'galicea_openid_connect.authorization_code_jwk': lambda: \ + jwk.JWK.generate(kty='oct', size=256, kid=random_tokens.alpha_numeric(16), use='sig', alg='HS256').export(), + 'galicea_openid_connect.id_token_jwk': lambda: \ + jwk.JWK.generate(kty='RSA', size=2054, kid=random_tokens.alpha_numeric(16), use='sig', alg='RS256').export() + } + + for key, gen in keys.iteritems(): + if not IrConfigParameter.search([('key', '=', key)]): + IrConfigParameter.create({ + 'key': key, + 'value': gen(), + 'group_ids': [(4, IrConfigParameter.env.ref('base.group_erp_manager').id)] + }) diff --git a/galicea_openid_connect/security/init.yml b/galicea_openid_connect/security/init.yml new file mode 100644 index 0000000..e827682 --- /dev/null +++ b/galicea_openid_connect/security/init.yml @@ -0,0 +1,4 @@ +- + !python {model: ir.config_parameter}: | + from odoo.addons.galicea_openid_connect.security import init_keys + init_keys(self) diff --git a/galicea_openid_connect/security/ir.model.access.csv b/galicea_openid_connect/security/ir.model.access.csv new file mode 100644 index 0000000..8fa3730 --- /dev/null +++ b/galicea_openid_connect/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_client,client,model_galicea_openid_connect_client,galicea_openid_connect.group_admin,1,1,1,1 +access_client_system_user,client_system_user,model_galicea_openid_connect_client,galicea_openid_connect.group_system_user,1,0,0,0 +access_access_token,access_token,model_galicea_openid_connect_access_token,galicea_openid_connect.group_admin,1,0,0,1 +access_client_access_token,client_access_token,model_galicea_openid_connect_client_access_token,galicea_openid_connect.group_admin,1,0,0,1 diff --git a/galicea_openid_connect/security/security.xml b/galicea_openid_connect/security/security.xml new file mode 100644 index 0000000..e07fa87 --- /dev/null +++ b/galicea_openid_connect/security/security.xml @@ -0,0 +1,35 @@ + + + + OpenID Connect Provider + + + + OpenID Client's system user + + + + + + OpenID Connect Provider Administrator + + + + + + + + + OpenID system users can only see corresponding clients + + + + [('system_user_id', '=', user.id)] + + + + + + + + diff --git a/galicea_openid_connect/static/description/icon.png b/galicea_openid_connect/static/description/icon.png new file mode 100755 index 0000000000000000000000000000000000000000..88d2f513128cdee1fc701a3cfb9339321ce67907 GIT binary patch literal 3659 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 literal 0 HcmV?d00001 diff --git a/galicea_openid_connect/static/description/images/login_screenshot.png b/galicea_openid_connect/static/description/images/login_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..98bc7353941c0c292c19258665d3ecba410e2a3a GIT binary patch literal 23019 zcmcG0WmFtdux8@}fiNM2V8IEl!5snwcX!ty!CeA`;O-6qf;$9v2<~nn_z)ntGuwIZ z?Ecu@AA8>J_76DI(|zaOs=8HQeO29IN(vvaFbFXK0Kk%#5?28LL@fY7kbxkAXB5sF zzkvUcoW-QoAP~sPrs6N~BY}&AmW!%`g^RnflR02%?_g)nZf@^v<=}FH)F}i2 zWPr4|sG3LSpEYksRm*Jdn~n2K$~QytJ_$Z?Q8gb$RzFIaaKGVS>y~bjS47NmU)Ik+ z7gWzHvwUyE;NlwcXI^B&Uh2r6UHv8d30Fylw3|h`T07Rv$;z?s3{~2%1L%94glc@m z-zG$?9Y)VS4kcIIHw)emQlc;I{w7)!@0=j*6N(@AjE>J_naE-t0LX)(!)h*h&jO%g z5GE>2v0$he0ag$+&pU$c%Nrcw92>$65wl`1e?j5ma7svyLWT@q z51TZhVs=4*-pz-LQ)In)vwpaMko)8N&vu)nITP)DG4kNIy`A|K925B9{Zfoqr-P&) zdN!q~5P`P|Lm#=|ze^@V1^MN$5JpKBOo@;|F+!F* z?qn8RA696@A}~Vt(M)R=`=g^zjbfE#Ae-$$HCp9WP2BjHCYI#?{TdoQ0g@Q; zf?CGnU4{LY%0n+079_7930&Omhxl_Um2+wpVIwF-{UZ3sMRAq+*pULf7YiI8k{9_! z^ea@1w!j!l0Sd_KM=hqeWv;Zo1d;4$1lLmNStg!Kqc3?zz2-GAb&pyhv;r+S0X7%5>(%Yh0YcT`!2r+ z=QwEM#ZE{q&7LOLG%6)Ov&&H6qFHTYMT;9QS#au+$%Yz?kA<2rl$ezzPba^|-uS8j4HsN$B0g&m zX}KEK$RhE#^n=9i+MGN&2(qFLSB2rwn;-DXKlD4+~WV~5xK0)Ropsf>D9xwm+gAgCd|NUgPP~8?9}>`Qrgyg= zPMxvZTo#T1;0L7%B&uScDtB%;KlxZ{zMc@4pW%?-I+1oj+Rl@rqYmD0vFFObLJjIS zDbZO6$8i1DNH29L=EZ&z>Q@+2b&vVpg%iuOI0VMzLNij3m^t|^+&v}((52jFz?{W zWE=_4hkWj;8kdfzf*?ZUv$ERVE@9!}7-e&Ot!0$;0j!WIiUK4hrJ&RG7-j?eQ*p^b z^{oD#R0VtlG4!dxsru4vRJqA8NqmI2DpNK;ry;txYU;O!LW$}oSqCq*)zxDL&&QVP z!t;Yaivm(7Ia@`IB1O?w=@uM>63QHDa+H*mC9|1+P(%?9!plry|I;263FY6X9*lUh z4bcjBYNhwFY!*<7gr$$r{?gLY2HS=7vE73W&TkS6i#I>^M$@8YQvx*aaYJz8Tjsy{ zDP>0hM(k;z0Oo9btsd`oMpDk_b%IH7!TnNW6p4U<*mpg~Inmh|bsZ(%@IAjIamwr+ z9l+Y*MgXr$7O`2yL@*-UM~0ZszD>j8O%84Ht@>e1U>ey~uOmG;I4BJN9nsz0?Z{Jp z$X9jImAKuy24@%e?xO!1Srk{oz+2Y$s#5f4j9!P&?cqxricLojJXLkhSG}q%d~{)+ zfpmp<5!C!Nd)}9qg@uJP!~^jyeIXIAJD=`sJakO4dRo?j0EE16aK+c-@RY4QEsj!20^>wwF^KH0A;qK$w()u~+E5Ba zcmSUjiETH%f&s2-*FQ1;E*am+t5x=;*q1L~bYe($T2a{tkVOONUi>^1IY6TsV~~0j zpxZkp4lc=2^gkaMEViG#<=sge&6zbdHO+Y=$j8S=_&ju|LelT7xUJg=3T~ZlvT%w*T-rG`SJ=_F zmdWXuzmC+wHghQ6)`(}kOx#@T-k=y+W7#Y!_Ar`0{MRMh9JI0%V} zC;F zAp3THZRwrS*-UbIDEBfob{_|+$2OR6DvS)8MNl$D_BCHb!X_6WG$m7d!+{Z2%#M^D zC5|RY`=K{Q^7o5|??rN!ceC?Sz$EU@mZA^=0YR8J1*Wzk)~g4R~OP)sDon6$L2-q<&`` zu_A~;Em3FP(M5liWTkhCCL!^YO$+HXR#FX>>@TAkCV)&~B~E0zc^Jx1yb~QyTKS9e z)AS3mX#dB{{Y5aE$T(~VZwF;hNISN!?J%bZvP_tfjOT+tHDoJuaK#LH%*J zsZv!&F^i8=fCD#590;(KD+StWrmQ$*ES!1pIlh``XyCmFw&GzdqbKh(sf$8G^m(xH zvaKM4v{c4KMK$u}*346WuwVW00y9KPC;e>)G4OeWzj_pbfxL?$vx)P`q{{Jqq~F zM@YILQXG7ij%h3oAOH_F0egm>)l4Hz>dT@tqCa>M+N!Efrjr=*jAAvt-l~6ca&|sH zy86KVk(QY`Tg^!nN+n&Z#aMafbUmD8=y#dq@4-X-eB#$SK|!LI!|GoTfvs*bQuB8u zTVqD7TD7(18i%D|*kIPTY1EMrgA}$@GKBv=zrCx}Km4|h4-5Q^Wz6DyHhZMba>Ah5i<2axFr4E6mQTx>yeUbH)$Liks z<>i%|yZd=3vK)+g=ba)&+|NBjzuQs6hyK|wdck!WgZI9A43V_PjdE=EYNu|%@Nrwr9}$(QfdIF1};M{aI-NKN?YXyo3iD!5&_AA;@| zCL=35W>H5AQG?PJwgqy~&{$e)FNN=n5SYeEiL0tJX>+z|zf)Bg#RbCy0|UcMP%IP& zBSeVbivus}Y_lXmCNn81DJ$W8J3(=Neij!G?~E?U=M8}56|dlNb9&XhzkA%6^DTqVV~SZ34v%`eVw<2Mm_W>FH{F8?(7GRe@poKU@x`m8jUcxQY}A zM4elOdMag8m<@hFWBL~puQSGWYin!eqoQPGWc%y$N>x9E%fzc`Xjn8XfiY%!@2Pmb zfHs_QggGTbky#GQM$mb8v>`Ciii6KO8X4i|@UV`C^66{gySO#`&K=xnwRxS2c`#HH zQ4etol-uaoEK9@25~rs`j0#umSn*k(P5lm*-N_G$!~afE<_mWS{U+s8L`M3i_wX}b z5O33}^7Wymljf_>1F7R*V`aF+evZX`6A45>ih4Dh=(p$psI2g*lIp8Z9;eiE={W*? ziA~rCOHS?3ZO8cRY!Xpinxt?x3-*jPdv`m#Bc{P1xuoId!OrXU@H3~nC4sELcE*uC z0TR?)MeCYGD(j(mYGDr;odJRT8pkSq{4J3Nqi&=1OwsD9UcpqV7w0LpZ93yfap&#Z z%NH`%Jj5M~Q!mg%X+nv~cgxG?Z07||w-e&2M0J`-rIB8?QZ1-!a(`S+&B|g5H}S%$ z+m_j}9M47xIPhVDtoeheB@hZAQtB_h(z>Hmp=nXhO%_;jhwsT-6h~QOB8%;3 zW#=J7AjmF14bnS}_hM@6?9_673?$C581u!evJRiQt^QgdBqD0{x}m zE2(}72_hoG8w$&VsxysA2F=RI$VlLC7T+%$#TdpBMqVk-B)jDD^6B{aK^W}Dk?4o6 z7d6Zu7||^MURjzxJ0l(cEWypyb*L8eyoQ)p?EHBUlnFEc9e{6lt%FQy8kUzG{I@wk zjm)JV{*%N`Wf{*2 zjXPes`1;`^E=%*c?Yo3S7*~;cxOm<|lDVlut4~O&)j*oXof_wVHZd)q-n;Pd$ zYDT}4$1Qvsk&$K4W3KCGFHw=An{#b%s=@{WRP+~E3|soj>`P#|m52GXAnJ@F;yyc5 zgkRlub}TP1pPe~9%@rEtCk||#f(KD=Z$J`=(2Sw^Azk=Nr5|3Mg!suT?!NLzkxDii zuApV~w_%zgD0o*Uq7=xk?=GxrryFmIhjox7226^Owk=i-;sOwqARq4Ti!VYQk(aOr zTjS@hDW!8|6M|zJm1$-a%X#FyHDG#YX|CR)2WE=%%ELq! zLEU+q#uCaG^`EjuK!9ZkpL}0e#&uc-;+v)wLF*-3v9CqeK}bkGwOd-OR_tkHX>KKM zYg!yFeve+MKZXV_s-6S={rw}!EIG5X!`>L}234h{H+`ZJ&?n0el7r23ZU1^YbaEv^NOFuAhS< zW<8S05reQoF-^wfeG15G=}hfrjhucP>4d(jdV}$q zj1V(+)x~{YTp2k)R|Df#eyDbo+f(Yzo~;MppQ~oQlpLk_Z~4D}OP^nPKCb={dKYmCzQ-DGbU^kt*^$P;Z|&b-c(KmgunS&szq@nqR&I6d`e$IPa%9=*?VF-W@ z2OzwpaMnjmQRIs(tI^$~GX-->0)py`H!1k0cx9Y~5#5>hHNQ`J3}Zivl|b13K4t)z zl9XvYfiWi;FYt+inj0bzbAs|pWAXO56{R2menLf2$yOUz`G$>VN!qllzOs6D`AZqj zYgI}Y2P0@c3Z+h@Ozn6Nnj1`QA(d;EKbL{qR`y+gs3Ch+)a?gX{bDLMDo%;&bfAki z4aLj(TK|_b4b{-eb#G?D{#_JN!W|svS#Y|Xk3jL15tN3xrsANbsMNWQPr9;X_xcuB zU4HDvc6%PkCFrDEvHm4wh2vI55=IT2DnWMjh%|7i-;Nv{PW^ReANaQ2CLN6T3Iw&l z&6Z((8t$aHCu zjO7#u6Ap%}UfcJ>x=7>osh_VMGFmVQ^Xk9n2B77E%t~o`M=W=tTJjEvCQ6*~b%rvu zdZ)hoyCga|=XgouN=T@W#UlgScoAd}m*2^X;rV)jdjH{7+~e$;fc+x;ojubXQ89pJgKoTm!Nnd z3iY@r$0m_bmW$uM^s;n!m6|`gyT2bBA}=Y4c=j1&8*Uu&BbFF35Yv8KVZ|5=)tNA( zR_Z6jzq`A=_4GIXq#XAZ+*Q>7A=5-uwhcW!4GO@RPu7l65nY}0a(L>Q4DI+Hd`!wD z&06B*X2x1VHL&Ul#Rmh0jYuxKu-~Hi74WCoO?H)DTBY>4c>KLorl5B+ODtDw_*98Q zN3+Bsg(;ya2*V$mGBb%=*T%28WPE6X{|07l_?(ZGmznzr)kTQIg+)-zZ zm=-mT@+D+n8JD641K5l{sS})>8$QhRyV)ZHfO@krmF|yb zk>)#QOorOliQg`;=F!QgtX!--ZY~aH-eDp6;Y=*mfkd%S!SePPPKTGawKV!hSr(eX zK|HmOFYKZ$lrk=`yBjo z72}e;6vQgAU%l|YGy6^F?*c>=yJc61J`G}+*I?_@QE>jdD4Srag*8;5N6cpS)(Qm( zc#iyY`jzY}3F*0Yl`R=u310tAF+~Rl7_VQGuC$!rkK@a1A$Ns%Dxr+0-H{gNufI#x z%fyxCva0jj1bj94g@Y=sOG_Jb_+C8^wZz4L$n5p=9}IcsbMOOwONoD?TM)q_3~kmOpCtqxpuD6^j3}1qVxF8D2%n<_|&6rwKZHUW^olP z@ROVQklsAeKO9OHtc9#Plz#SS?s-6;|BWntN@8-NocZDS zw+81H^3l-gm5mLjZ7D|=m+A1iU}(ZpW=4XlvU2zIl_Lv1{qWeto~B~XKyE2)adB}o zIeE52Vn*6TOp1s2>Gxud5>IBwmH_<6(no_16A4jK(bD(z_4O`3A0w%I*)LN9qu!&b^S(tj~qrTXc1l{!Da`=E^$TsY|fjRJ1NaDio2vko>#7 zk`wLX@m?kvfFd$_D(Isr=N>lB?^H7W7VUQ*ud}dpH*Ai#84jv%PCFkT0i5S4iHTpWiC)*yt%wy?uCFY(Z%_D>V57d0Ah*6>dnpDopJOJs zPg^k&s&6$Y>ZQQw67n9v-%eZ#Rk%Dvkg#xqk@?3D%V~VB$=iCAoU)=D%ae>#j56}U z_Le$UQv6Dy-u9tY@tB|8ZSJ*N53&aiRU^4sms2F(ApPF*Ygm|w3ce}t($c09E$(xg z>94V~>>Q;<{l7nTZus90zY;4kW5th@OuzbS85tE5rQhz=03J>MDl$cibA<5?3=b!5rdyDW5ez74UU%>(r_qyp(}q#G+0fgGwrbSwfkQpHB!RU8km| zE^y`Amn`a<*N#CJn81{y;wmjCXT$flw$%E7Q%XTO@zluC$cT)EWg>ocE%uw8bPl}@ z53$hRX}#~IIxVrn#xK@Ol8FAZpS{>DbWB<41rR2XtC1&#tTBL~t3bck@X^5^BN(X# zHL}{w`>k;#8yTt$)@-b*Mb$^GBQ0GUy0I5dI0b*_Vu^`}Nrg_@?`|bFc8I#_f0TZs zK_C}5O%KFBcXlFKtK%Ki@2X05^DUws4kh&$aM_X+Ae^HS8uW|@S@v*>8CT0sp8|W0 zYjg+-H;o(@Pe_T0acR}nIYC92#+M*`PcPE0(D-1y8I$8_0mJ1pU^_ZG$9 z<_EJ)S>Yn*Rp%Okab+;Ml4vIn4h*yzoPd?ExVRS*$~V6YOU}-PsikX|KFSN< zhAn-xc$q>%9qjWsr@1$tLrO&2DIxyCB%UfT)_f#co!KxuCBwEsN7a)!N)qHT?(gsU z`S>ZPtaxx+%zfDy7|Qw%d_ZP}rUHnb%>N8VGeT35pGk1Pf%7WE;8e`?ft(aZZNZr^ z<^ZEy)B-ZpB*#zM(ryNg>b1dJaW!2x)e9N@qx7njIK1uE5wifaJ7?PjfA45M$0 z&GcP<>}XI;P0f2ySa^^=(;8{sbdQdAk|-dP6H7mN6pZ`Aj zk~}*EetJ;+=<5q|b1$DPtB{{vRaGc+HvWcnrpk0J@yQ+-3QnXsbsT1V^q0rn}@DTEs8(hhdCcuP z*x`?T4el#k5WrByEWa`)R~zsO07V9FMx5FF&lskL68Z+0mNvPj2`CdS8=|^;XBFAzY_NQao2y6 z(0XJq1u+J|+QyEPglUZuZQl%AL-R4y`%gB=uz%&d3XQ`S7z|tl=;vBDyqu7&Jv}{X z@i9Zh7!yoLOre5;@A+7MU6m}orR&oeU{E!S$0GO$YKzXB=N!I zU>P~6K4d{9NnpVHB|4=D*+eSkPTB%IW`m{NxiH6*&qcq}>k<~0Kts9dNQJXGLrz=R zv|1TMx;^)yV9F}?n^^1PW+*8$3rnc(8268(3YrWK7QMEc%LPN*bS@#G>{1p5SgPFW zZDKRAfNQyiZT8yJ{uiu5YU&wAB)E}Y@@dGV6 zVxBhYJD~hV^s;u3B%T-yc@&|<84b3wsYPk)4IAdtr~N0_!wCQY{^Xz3$Y zQczv(HI{BZk}~n>{qj^zje|lK?BED=vV>7*5k*eSk68~8Y+0ESUIy7*7kyV(b@f-I zo9r`Gr~!e2fiuO5*qF}|0Yh$qIqEdO%h{{`2Wqea1G;bNjgkue8WR-zp$HO2ke)l8 za_t~-$=&kW=yccPcjaWq!};2kTDeb%#FQj~8xTOI)GdM>LZ`iye0EY!>+coi8iHhG z@v1wqPYb(aT}!~v)Z@qf@T;l?egT2e3=F!$u2coc3>OeD%GM@sl8CU+xzvT7hEgiH{Wdi}G%IxKY}6GL#u=Hj=mG4;jt-%T z+jGs<*4rNLhv|a}43a>f^Gy$dV_I6RcAGhwCVmEnXBIeJ5GGV`8Ei6DL;av*h&JB1 zwSBVfeF`M1G0HqZ1|U}AxR@3N1@+ar<<+^R9Ny-6C9_-yTjyfYgS~N5jCc|KCVb?% z*5c67cr*62QHys~e%*FD&-YBg(h^u;a&&ZD+&jm@I<2AiAr%#qN*>y#rKM$LWYn+i zGj5u2fUpfn4i5PA_8R!P=CI&J6r#2DTQJbm(lXQ2)6szpz9Jll6=f>uA96PcBfu?* za^5P%`Fij|Le&+>#r&#F7H{=4L0r}3(P}_Nc6=1u;J(fF(cbUAciK|)FQw0lySlpQ z3>j~LL_F0xEB`^zxzptZWi2BE==?BdT!CK^-wC25rQ@eh2q~e0xu8Ec8N{dT0`{|} zqYwhde<{?x+12?%_wJeMhvT&wVa|vxW(Op)bNxtZ=a(lBT#+IdBE4kd*q8(O#=*+T zLRPpgVn2h`XF8IKnC}r)H7?s0P*Pox6R>@4!S0fOEhIc6Xyn04lHg4c^28}t$O|!u zImiF#xA}kjmEFAPVoPg>Td5ry>Yi3^wy!)0##S3p_Dr6W3*L{PNU+Fj(JzxVcj9x!mOq950^@GdHQ#HFTq#HVS6=-EJC9kOkP< z?}&nev9O31)w*7`Y&xWLY1Rh-u=54Tv`;|>xLT0`b|64aMb+@tN8WnNJ>CgkMF&V1 z?T9l2rWk~r!}R(iN#2W_F2yi79YI3qOP`8X|GPTPjgKIG{pEE|Ro(gnGI*T}e6&WA z$F_TGtirDvZgvv5#tC&;Yj+474g{NxddJuxD;rDm_3+)Q_zL5_r#CYtiv&k*L;-LQ z=W}Dj>Yes^XaE~Y1Z_K< zVeHjnM?~M}LlSezLI7mFU#WCbf>$~xM!w2+d*GHqXO*Pe^i1?}Rai(09yzjN_X|TG9`Yfwt+{eU2S)x?KPyz6b=U0ZdwHm+8WE1xq_G-(X}&K;^rv zz;Thew4$H^f1|K1KrY8r8(d@_?f@-0AcC%hfrt>7~+;!abk`HLI42f z;fSMxs3&|{Z#m^8XVRYMTIE_X7QvprK6QuuqUYB)lzLWI&5H|aUXTOV*z(K8^UkL~ zgc*Vn>Ebrfuc813nG*XT`8#)Pv1DSpQW$u1WlidEw0=}N$`MPlY(DMyd*XizX&S^= zRliIaIsGb0TJ@7XOdRK{C>e!(BpJ1Hsw&S|H+$HNypq%W*P%O|o{Ln{IE6oS=s-z% zD?o!ov+jg*q6IawQ#-qEXw!H#2Z~;sMvCSe{N%{8j>HOWm?owy`lpTiAQhc-Q;=*U zG)*6me|NoWe0tbQLr~?&J}@{!RfHq9toGrb41|6)7tq}O!71UmK4qV&oQ~_OW%Kzp znpp{2C1fnQ=$!#WP{v65*iilF*wZgsVvgDWMn_4}f~6NvXPR20X8ABGQ_3+_M}gO` z`BO2)>~@cSXoY^B``5|%EM#mX`K7hvA`N)c|J2flAN+3%5dQDF{r+oTy~7q8fY^^l zfZHh$N_Jk&&($(WLMiDhRh zU(|}<;2T>|Vr$mPQ6-ID|g4!3$L-weMEXHBw8YJmG zn1i6l7>J?2pnUTO>BM4EFzV8et8O%I2;-DLcm9|cNG@AU1?YLV4f9N^iCd4!nAhW zEGkO;!lA7u+Sr^Y((W55^y56li0YQE>UHbkiG0aGd^$FWcrKYzPiVy`HAeA^U`%|# zRL(LN=`W+2OOch_jOd)`T-tc*INY6rp0EQryqi_&o_hM@`fY%ftSrCY19+K0J`nOSe=nbXl}5QR3G_{t7!Un z59+73Voou!fQ1WuSp^C+5r`57A35WcTRJVe3@0ZW;06wqB`cEK)2!gs2lAl7yGpT2@oD9_u>X@1lY`Kq#6TkXWd^h-sPYDr}7Po|C){e=UvN;DOyNYYx;1Th|GsE ztNP_KB11pB>!SEux&^^)+x4dc?v*tb_+}T^f>;X5Ka0&UjheJ#0S|Y+DCk)u{B&XE z+UxhZDl<5j{f<;q=zLpSXLIxIWZt~+pVB%+58d>MF~#P8rgm@AaR-s*;wncDFq(Xg z7SKK4&Y#XTw=h3H?_jfB)8q;TfZti6S07iVl?-)tH>%AKkCC#;R6V_$JC6@fk2G497kMYD)MMDD?|Mk&I zM@fg@%yF+M#OwK=)s~Wu4!@DB_7C3G^Wk8uw$A>{D&$e$@Dljw7K$8%=5`|FvFcE< z)aF_6%qxa;^<2T0xa)kptjd4Uw9MLTGh}RTa^~!+%%Kr=lWWk<6>AO<99;1QgeFc& zfWJBC7eL=cgWK@0@)GBM{_97l!4=ZRd%I)8l(Chk!G)$yuUs1SZ)aUeay07bt#*>X)buLEK$>M^fs%!Q}%#-c~E&<6v5NZ0=Qsv6R-vZcnuSN_tb zs!>R&*dOmtSN2fR9-)5@P7!&sI9+ucXIs-7>u>Gso3QNN^-4>e-?#4FG<#4IpCp2k zTNdozd$ijVGjjN-MeuQLX_cDD)`0hKMg=q7dNX`a*uVI({dzfl{rjHzJFq}<)WP%| zFg(91Ag{Lf+3H$HCAxWjbaQ$*xWm8p3RLGP+aO`ypOd4bqsu$fN{jA%(lGl^Y#8p=flCv%M1hGs<|yR^k0pGVBWR^wQ=Bg zuUvb{t}!>KWv=M$t*P5eTJDOaJ~`vZjI6Y_xE~LQ=qxSnBSKsouc=0M%S+q2Cr|WU^8V5Khhd5qT<2BoyECJ86?V-l`UbOW^jftRQXZVDF%j4y>&SN8N z&v5k*D!@vJK&*T9xY<yzDgy|0Nx5yjL&Q!`H0A zK^xorf5KpVKRTZQzn{pYK23B7k>2UHjC0N}1|1DP8o4Z}^NPS)E%UULo7)u`OgxJB_ z^D$*$O#SS1w;=Df(;D3J*x$XCC>gDn8_K4zvV$Btd~-+I?VNe5elNk?+wc8_3@tAk zNqpc&5eo*_Jwy(6y6BFrTL9VItU3`wZcB#@;`h=LcEkX0(m1_PG=ED$D{ zFYTR2MMn>q)PNM;zg!?}$ZU9I#F>W}?0z^t$D2=+E(S5`6sBYqCu9$bJ*Hi2l!X|GfQEZlVrjvNs#O z`IYn+ru=`Ax7a(8U#sx_|NjgWe1zn`p3-KRL(}Dov>3TMCH|#l$7FaBX>vuL!bglj ztY8d*LPi~w{{w~K6A_J!jAUHTjI%@DJpVw8OGr!{DOqr?`!C_KiU-@yczEzJQQtft zY|F`H9k95T)=u^n+SuQxZH)UTyJE26&%hwNcaxPc>fG;a2@QbQ z^1X@@PxVb-SCmtARd>_cKQXZGV+37D-&n2n#hT(ccBrf9u$K~;Q+*}FP)TMfKe_)| zH)#%vuU>1E9U^>wp*qFWDbXza-Rj}2r%Nma5e&;FY$IeBF&{{CTa5T9zCNLSQ+)}Z z_5D9y{t*dUp4P*BHfcwzF$s;%qQUJPw!f}JcIQiqV0*=|MiQZhBhn{5_@N?f%4_*16OQd!6J@fI(e&fD2nyTMvWLH(Cc zO~)iU_%r}~W88m##`40t87}F4=lp5WdzHJ@X>=Ec-GXMX(&qD^HFz4rj4YOBygj+( zvG(q=m6QztM3j3~th|alo2#7mRA@hG+UqV(GogN2Wy){@J5*4{?T9RIl?y0~`M9Q? z)NLJ~J^cKIlGC}kn`w?`k%{t5Y!9a1%EfDoK?R8>bvX1mU;Ii6bhVuxz~01c^GGy| z%*?1ZuKHcn8D?wD2w8gXXvS#d-2C3N8_@Jq2YXeM*D71|l%Gevx%-vT#4_~Eg&X3_ zfG(+gjUBob%W#S`KHT2vNcao@1W1XONc7gO78C+J?v*Eq1|{3b)P+?H*4K76Zf4hA zHy`01J#`k%vvX@O)u`s%m2CaaifPu(i-NV4r@E$7%^15eBmiIFZby-Y5B^uhd?+O0 zGPHkhq=zXbpIu7*()Rk}XlIP8V*mNhZTsMt7BM3RHv#zQvRx7mAi^>E(0zN&{3Yn^ z$S%#fxA1e04xI&L-<1tmh4=0Kik#re24Sh!vd{4%mLlYj`MCZ>MTH`ZOSQwju4b9R z@{eBhT+EZ(`>QKgqUVAc*uW9EnhG_zwLJVKDgM=w)AfVu%Dqs)t|C#3jm`o>z;9kJ zn-ey}82Xkn&128TS@TpCGd6cqiHpZN%1`bHEVhUJ9%C)}6 zI-jCG^j*a)`#-U~7K|QdLg{&nI~QhjPRpMQewXnoMnY0Ay;^^1o6Y&MKq=8bX8T*fxDHx zaI=Zq_H1-Z`Fzepr>&Gq_tOnY2jVwlcI8rn>jj~H{$Bn+YxW9H{Lf%tQnEI$Gg<|J z$%Te{skX|4DdD!eeOvp&+w0f51uv_Fmd|1Lf}+>U8yzjDU3ItXlr#=c5kHRY*wl}G z?T@&wk#as)i-<}c!m<=jcGmm}tvZF=+Q#bk#M^A%Yrop&u&chz{M+W~^{otkx6;|U zTDe-->-P80&NZ`yIp?yM&7#AY+k~vFpcw7Y<>D9Nhn}_6Vq)P>n{)0l_Fm^Bg2e+g zLScg$D~Odeq{}sLS9TbeuQFQ|+HTe84ENSs)DVNz;MKh(81C}I<8mSV*>p~Th{3_Q zm-9XP$uFO^Y5hp@^cx%Z$vdJ~QbGl1RIq7RJSn}7TA#CHjsn)jxqpj``ZAnMz#NR-*MUEiw&W7m}8G)CSj zJy}u^_GhN#=q^=rc<&HiKYKMfL)u~9T!>*A#Wy{T2K+iCgwZ+Af6JQg@Vd<|O5#MZ z_+W6H>=pVB?@gqb0510DJgN!3_!Rw(o5_wGrYam7z4p3_g)P0O<)Zu~Pm6{0xmFVH zCjYdD*W|lGa5?L!*6(}{8iwZ-b!4uzLrDyG?V|p_HQURC86B#Blxt{KdYv0OlT8u; z3t8g@w@AwC`1EBhm_~hs;NzWgj%qD#8ZB<0cR}oI-TGi~E?sTEn(!wAk$UwdbZy+M zp(|N8;n|Sy`1js80sA7Z$?cMc?<+$MKjKN5=vawjqOO6&(OZjS1@vp=?<;BHyw2)r zxEEi1)A;Pm5%+UKLL@)8lo5 z_t7YA1)Jkfxte1s2;W{+7Oko}JnboNIB(Ba(5=_z&9dc9sUHwT4{NeK+%!b@A*iRw z89+8{o22O zJ=m3%<1^ES#nPsa?N2O%8f$+J%QtB~M`|dqmOQe$M*aM_+aiBUt$BIPjIt6sD63KR zX|%T=>+;ZP<6(M5)xqx4g1eG+byO5z_^%?(Iy(62)2x8XUC)=pswW`~!06{jjj5^X zwNlyMz*OCvm6ba~&)b>`6z&OHq=qVk`{Q@K*1E?8f5J%jk(#kL>=>nHtlDUDm$MpY zU&EN|77;f|T-#{G_05MmEwt)1$7ie_K(Rvj9GCS zdv00ztS8${*K(C;KdpbG2kX|=T*iB)i>+c|ZBDD$r-0}8 z$M>G5A=)YhM>H)N?{E3J&5GOUTCIbQe#JRl?Nr#&e{J%c_0*dy=6}|etZyIQ=(DT} zR};E$?R1vrE-IxgJ;~Z78GIW$=Y2W$Qs#Yvvk**9_4|~we(%$4)mz)K=|4qYy3QkM z?Vh$dECu>@d)J<0E7?qd?v7b$66~ipsowSb@zE;F@7h_9cR7_rnpN&d zQc1j(!uor3oe`2dBr0u1{!Mb3T9R?2s7V!$B5fzS7fvPoVREUYHGJ_qgx>U zL%Mh$$AZjTi@^nHY`hq>n`6;PegC+A6|1(xfoR2DWnj1~s- zqsa%@^Xlydx(TOCYWg+ZMV%D+sp`Ow>NExQ`Tek4xhj0G+2~`wtVGv$`Z7WQ{lW1+ z;}z?-AJ;V4V7#scYubDgK)k&RAL39CIbgBx--0cFAG5<*we>$Zu3dyLS7{RI%3mMR_z|g8MbUzGyA|x8;j#3{cVa@X^T)i5&Xi{O1&D#%ZLdqR+ex!{@DVL*w)$U_EWwZgYtZ0 z_{eKf)$!dA;Ug>gbyh?!Ki=5m$7yWK0!p!l=Iwl1%37SdeXY~ry8|r#y%*qCfBp{Z z7EQRLn({62I%~m9#$l;lt)`V8+v(vj!^R~Taw!c3PuUjVG)F0F%r;=UJhzokDqFX@ z`182m7bDif$z8m85bCah@v^d`;;iEJucH{grcUahA(f`hO?{#|2j>GcBIC%h-=>HD zxBTx*t)17u9?w0`aQ`ltiv*;~_@6h~!%w0+{H(LI?S;fGGDFnzRi!N1ec>HEzO zmMC-q=;+CoHkPZz~rW01Shi1ofZ%t+@q@GoAd3Lg*8Uudi-xy5@c{uI5J+Dx4-o7f9 z;rbd*< zgM7ulSGhMoU)=d_vFWj*Or6DUbrV_X%k_h=_k9aiX!aOdz!i4%AB|FYeGxni8}r`4 zmH6g9+AZL!Nr?Ap75h32$n}WMUiKbcCC3$Bvxe~m^Z9nh3WVSsxiKl}AK+iBu z3U@Zonh)1({S?Mj9Mah9jZWNXUFW89S?{Nq^~Gz^g;332(J8%l``gOm1j~-K)B38i z^TTro(sNJ5mWm&L`*SMooCxjD-$b9l2VZ7$CaGvDmMJ#yW>!7aGrG*4ulHL`n%UZT znHB4%A9l7Z_53s57VE!r9FNvDq|Ijh2$fQ;?=Q2Y8HO+V%TR?1a= zMcsY-FD0R*F!0bR0xBR#4@gS4Gy(zwl2QXhN`tf@f^q2lktD&gbmCKM}Nq)WuXSoJoormT?S}bkSNs^LJex84BpCLGB7Mc$JxceAmRxZn0g3A&^dF9xjpB_9sTs9?pXQm zUdtzR6Ja>x@mRf#RnsfClmtZ+79I@~9{o7`XUXH)T(3;3Y1wAL%Wvg9J$n^H5xhvE zY2^kgE(Jn^fn@K=t^BVpAA&8}cPy8Qr|2T9UR>avn^{t^il}^>e`G2n^Orr)`CYaz z;rdXs5Wn6z$XVA0C$E;yHj}^K1)3%>66Pn-j&orblE+TN^gzrfn5zx^)!eXlIaBR% zA`lgBjOK@Ais_peCYqZlu{_1C=Wg~b5{xG#V1sUVsum`aB;DmMj;T%BfE%(+Fgc&dL`T#-gVD#-OmM1_H*;PY@w;b z0oco|uUHjM^-1p+@1F2F-^q~<)>+Ls|+4tl{D9y@=Vlz zvA4m)En2=*$7J~m+-U0js__9=J06AhL#+*!SW}g#BKGM0BnF$mXw$r0eR=?}G?p$i z7(t^`wR!0;cr0elNaf-=+ZH%A{zPK4dj_ty;6_9*DUiSSsG@L2=Vq@0 z+ObHQ7bMknrcyfZ5F=AvBij`DeWF}_Cd?zFM(c^=1J29&NtIy}_BCQBChJ%N*BWB; zk$9UoVCFHJkc*~DzAaOJ*$du?!E&RAazrhR?CF~6Zq*6g3>c_cd*>FO zKBFhgpPZ&-{=tIrW4*4+Ce57P!!~1S+-Xov$MxkTleO&r=+Qy@qNMSPTO2+0*pS-W z3F)3Cr(Up-A7X!bmXN{In?n?NgwG%8?qs~&+<5vWXihG_cYnf*}};KgOHZgbJa z8y%_FncB)a+n@Ito?g}64`8pc)pE=!Ky%FI>wCAWr!K|Jq zx5dn>q}ke@2DaHyMdY64$0%U~@1xb;7 z#Xzrsa4~59*;>Radv|LHJ^ZpO*5P6|4HN8uv2ev)$Gu9h===M;SbedY-{L&BWclE1 z#J2BiiORY3#@*75*=ck}aavXM-W~@_gVqB?x~S;&VBM??F*uMXkXTrt9wuL`G4yW()Pef8KnoZ{V_0qhFz3;AKYp(I3MkoEf+Qw`kI#?bvp%hd< zN~wxv8Js&8nN6itY%AMhr+Y$lf@%G!&SesfRI|nOjYxpRgKH3c!Be=ZYHwA-{E9z1 zHZJETwxL#oqP1+ZtPk{SM$3Lqa<|pTx>lpAzCi9M8FlsQQ@?sEf)R-y70sK zi5X-u?bl#m!eVi6nmI!s1zp&tpsa6F|JI2gL-pr$%KHy|qmIlouZzCaMQiJ-mTNd- zN{+W{Lpaj*An|`8`dG3yiC23^H+|D)C*9t5%%j};2}NmcptcI#eY09=BVqkF#tGnu z8siL%8Z2=Zk2!{J0SF(I2J0#2b|>Wo3kUhBxci~#{;Gd4wf?Ayf41;#W2G_l5Ly05ikoYD;q72EJOuzY(9CWoCBJq^){@KcuG2r) z;sflhNHX*o1w&fmnvTTC3$~_{oQP3Ua%lo&{@uX*(-%C1Jt9fe)GRz1XvX^ryhofo zsAI7}?*SG9pMBAv7FlKD?CR!JFFU}?_SIE`G;Gep^(LN0>;T|0G5=xprvNJQrLsdQ z2({P97(C@3wCFR*Sx-l+J=472U473BR9GGqz57EVQc8-Ob2B;LKuvZFB89mBR~M)A zA)rNKxf;@ufAd8tRGfQk-hn}6Zx_x6ybYZXx+685^V6-wi&Bpjc|ehNhtlr5Y=oV| z8#^)+Egy8j;w~jdW)jzNok0&0@bhdSL$i`C1rKSZzd`UH#+aA=x=w%8f)50fpBQoT zYejOn|NR-`Nf=jhEiiR?oizJ;J=;Va01m(`fr2(uv*y<}+AmfzcFB1&5}YM2^)|QG zsm^wY(3Ou_+1@gEddA`#CF&dQ#^SEQ10E}KJI%&xLw#x^?$IQYs3fqF?E%V z@;0-FEf`4IHa<Jr};ovk7=IK&-Paf5v>u9Vl7@jxYoI?dp1bsA2$!9@OV&N}tcW&}an<;3?JeLD>BW_`}|EHMWHu-A& z*SW$pu@XE+pfjW%Onh+7Bn4C~WD)UhHtQ03LxOB+-VZTW?t+zQt5>-v5tP8n$H26T zzgc|-HN4NyPWkVB*@^(Sl=N@JTDJI?^bi6`vgpmOq8*DC*F6``mV*0EQm?rNibN8Cn zhrY9RS(+S&UcLb~PuqwIQQ~V0<9!ePC!iSC?5IM1t!B^~yRsn_01%)Etw78_pwIV;IP$c*3YP52qe(0S+hW&!an62yd0Qb7TT_&!Hy zk9N!RNpMSF%6DpPPlks*BHh<@Qa0xmxNahbA%Z(mV)KTfHYm2Nbar8CuSP+ZM=TF2 zU7IeSHfmOi&~ipN-OZKteIY4&^vb2oDx+?>oI`eLT_zo5OrIWsi;SPB-{t;WIz+5@ zVpaN~WN?V=8UUKDD?aCMi`p^QQ{j~!Z0Q9Nuu=7_j!Yd56P5UV7B~I^D>%|*<`sd# zBs91SonP6TiV4SvjJ!@na#X|(xgJz`=WR#P7f&qeYF~m%T{kBRLrG;0o=P%5a%W)~ zTT^gvC#mj~!zMEld1%~?eQi2EW^JO*K5}z1qakps@fUuj#vMsmv&o+Ba}Rjy!ouRE zakJCkvi5c*`n!HNuGBGE8U+hpoDFlKA(HcPw=Y#uF;flO9Y>RL8RYxZ(D=WERlS+Zll~QPvPHARB7uWM? zodaR1lvqFyL-sihm_%HOiP~VyzSXsAsD7q``*j?E9AYGQ19R;{*kFUWt66Y?HT)gE zzF#vHT_Xo8B#WO}~uE>A~0wW+2%R$!vCQw@#uwyZof>AKT*&2#07#)CVhE_1jS{9 zt1O!?Q@sigMx$3ePt|y1Zxw$o6MOrN23=-sYT{pms)I=Xc^q&(9`P#(-3O02U zR$0cy=2ngBubENr+DO!SZe8A|MF-n6cJ|#IYbZ8y%N!b$h_{nC(ScL}qO$Xr5fg+} zuClVSKMoM=b5ubewzbEuIUdPcDb)8Y{#gTRsnNo>!3n*-8AE+E6ne(>c>csxtzVD{ zq&MR#uEsup0gCs_vuk&ytPQWu22J-!O=b}b70eAY*Gz-N@Qx3gn!dd!**?MLPXm+- z&HF1h$|CJu%HGdb*6V+NaY#f<2n^fXt`y~Z+*ZfU(qLQvq-U=>F69zACf9mjh`YPZ z@Bbh`+F@-kSyj+5FQwEq-##@xENUDK7M)_Qn33W51^D}6*9Za3%?E2W7(CC3&@JgP z(7rvk_^8Pow^(lS$UF5^40iu2@^)*RoRmzRtUJ}Kl3^y}?94>}6P#e9Mr8Y+F%qV* z-R#q4X=!_MTtyW?VVsNPe69Z}xz(tu@-11GYaXhqLg>3nuvwitBT3b$jW4~+@=AM! z2f9f5ADDW>?cv;-K~!eWB076rwnd&08j#?C?|@F$8axWUCwat?<~ohuBR%CAZ- za5xnS2vZZDk^i_Ojhl{eCwR3t2tvGk>soxDd$qF47akHleD>jvzNZ6I|ywh|1g8W!z#vtRhfTt^Ho-cW6 zlK+e4Og=+~D_L)BTRJxvCQgDkARgXM4MA$b;gH^-mu-!-(H-w|RS8t%;ae${CJ z;}QekPd5@e!atUh55AJ+Er&y4bs}9}S(`DKj?}u(gdfd#g#O{1-R2oU{7q84B>Y`j z`NpIDdVP$m6elc3g4gZrCRX&>W0-*1Oi41J{I%L(V-g!Vb*xCCtN%vtb`bt;j+&g* z^)gT1bzYPFgx|hdy%)G026f?r#;%u+Y-XW{XSBYA$HmS3}2ymG;Rj9XrhuZ%Gp7 zodli?h%Pqo&}%7vXoHN@r{MaCRo?o?p5?JEYWAD%yl+G0nN z{j|mlGmgcx=`75TZdv&Ez~efh%AilB*g;mUbM-yibb>A&Q(TjM>k#&aijf{Gd`}rU tfd9Ks|2J6p|E>!ApLX&8%YwBSLNbvwll5+TV{BYAprWX$0GG22`49Zw$PoYl literal 0 HcmV?d00001 diff --git a/galicea_openid_connect/static/description/images/master_screenshot.png b/galicea_openid_connect/static/description/images/master_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..923c47f47d4bf15b1ede707f96a69f3c5bf38c45 GIT binary patch literal 17123 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 TR^Y5T0FaST6t5OF4*q`th_&^t literal 0 HcmV?d00001 diff --git a/galicea_openid_connect/static/description/index.html b/galicea_openid_connect/static/description/index.html new file mode 100644 index 0000000..366e1e6 --- /dev/null +++ b/galicea_openid_connect/static/description/index.html @@ -0,0 +1,133 @@ +
+
+
+

Galicea OpenID Connect Provider

+

+ OpenID Connect Provider for Odoo & OAuth2 resource server +

+

+This add-on allows Odoo to become an OpenID Connect Identity Provider (or just OAuth2 authorization server). The supported use-case is to allow several company-owned applications (possibly other Odoo instances) to reuse identities provided by Odoo, by becoming its OpenID Connect Clients. There is no technical reason not to allow third-party clients, but keep in mind that as is, there is no support for custom scopes (other than openid) and no permission is required from the user to share their identity with the client.

+ +

The add-on also provides OAuth2 token validation for use in custom API endpoints. This allows the clients to securely fetch data from Odoo.

+ +

Prerequisites

+
+pip install -r galicea_openid_connect/requirements.txt
+
+

Client configuration

+

+ Simply go to OpenID Connect Provider menu to register a new client. Make sure that the Redirect URI exactly matches redirect_uri parameter your client is going to send. Copy generated Client ID and Client secret to configure your client. +

+

+ You can use OpenID Connect Discovery to set up your client. The discovery document URL will be located at <odoo-base-url>/.well-known/openid-configuration and it looks like this:

+{
+    "authorization_endpoint": "<odoo-base-url>/oauth/authorize",
+    "grant_types_supported": [
+        "authorization_code",
+        "implicit"
+    ],
+    "id_token_signing_alg_values_supported": [
+        "RS256"
+    ],
+    "issuer": "<odoo-base-url>/",
+    "jwks_uri": "<odoo-base-url>/oauth/jwks",
+    "response_types_supported": [
+        "code",
+        "token",
+        "id_token token",
+        "id_token"
+    ],
+    "scopes_supported": [
+        "openid"
+    ],
+    "subject_types_supported": [
+        "public"
+    ],
+    "token_endpoint": "<odoo-base-url>/oauth/token",
+    "token_endpoint_auth_methods_supported": [
+        "client_secret_post"
+    ],
+    "userinfo_endpoint": "<odoo-base-url>/oauth/userinfo"
+}
+
+ +

Configuring other Odoo instance/DB to be the client

+

Let's say that you want to allow users registered in your master.odoo.com Odoo instance to be able to log into client.odoo.com instance, without having to create a separate account.

+ +

To do that, simply install this module on master.odoo.com and add the client, using /auth_oauth/signin as a redirect_uri:

+ + +

Now, in client.odoo.com: +

    +
  • install the auth_oauth add-on,
  • +
  • enable developer mode,
  • +
  • make sure that Allow external users to sign up option is enabled in General settings
  • +
  • add the following OAuth Provider data in the settings:
  • + +

    +Now, the users of client.odoo.com will be able to login using new Login with Master link. + +In case they are already logged into master.odoo.com, all they need to do is to click it. Otherwise, they will be redirected to master.odoo.com to provide their credentials. + +

    Creating JSON APIs with OAuth2 authorization

    +

    Along with the ID token, it's possible for the OpenID Connect Client to request access token, that can be used to authorize access to a custom JSON API.

    +

    You can create such API in a way that is similar to creating regular Odoo controllers:

    +
    +# -*- coding: utf-8 -*-
    +
    +from odoo import http
    +from odoo.addons.galicea_openid_connect.api import resource
    +
    +class ExternalAPI(http.Controller):
    +    @resource('/oauth/userinfo', method='GET')
    +    def userinfo(self, req, **query):
    +        user = req.env.user
    +        return {
    +            'sub': str(user.id),
    +            'name': user.name,
    +            'email': user.email
    +        }
    +
    +(note that this particular endpoint is bundled into galicea_openid_connect add-on). The client can then call this endpoint with either a header that looks like Authorization: Bearer <token> or &access_token=<token> query parameter. +
    +$ curl --header 'Authorization: Bearer 9Dkv2W...gzpz' '<odoo-base-url>/oauth/userinfo'
    +
    +{"email": false, "sub": "1", "name": "Administrator"}
    +
    + +

    API authorized with client credentials tokens

    +It's also possible to create APIs for server-to-server requests from the Client. +
    +# -*- coding: utf-8 -*-
    +
    +from odoo import http
    +from odoo.addons.galicea_openid_connect.api import resource
    +
    +class ExternalAPI(http.Controller):
    +    @resource('/oauth/clientinfo', method='GET', auth='client')
    +    def clientinfo(self, req, **query):
    +        client = req.env['galicea_openid_connect.client'].browse(req.context['client_id'])
    +        return {
    +            'name': client.name
    +        }
    +
    +(note that this particular endpoint is bundled into galicea_openid_connect add-on as well). In order to receive the access token, the client needs to call the /oauth/token endpoint with &grant_type=client_credentials parameter: +
    +$ curl -X POST '<odoo-base-url>/oauth/token?grant_type=client_credentials&client_id=dr...ds&client_secret=DL...gO'
    +
    +{"access_token": "WWy74uJIIRA4bonJHdVUeY3N8Jn2vuMecIfQntLf5FvCj3C3nNJY9tRER0qcoHRw", "token_type": "bearer"}
    +
    +Such token can then be used to access the resource: +
    +$ curl --header 'Authorization: Bearer WWy...coHRw' '<odoo-base-url>/oauth/clientinfo'
    +
    +{"name": "Test Client"}
    +
    +

    Additional notes

    +
      +
    • In order to support OpenID Connect features related to authentication time, this also adds time of the user log-in to Odoo session.
    • +
    • For each client, a special kind of public user ("system user") is created to be impersonated during the server-server API requests.
    • +
    +
+
+
diff --git a/galicea_openid_connect/system_checks.py b/galicea_openid_connect/system_checks.py new file mode 100644 index 0000000..40fdf61 --- /dev/null +++ b/galicea_openid_connect/system_checks.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.galicea_environment_checkup import \ + custom_check, CheckWarning, CheckSuccess, CheckFail + +from odoo import http + +@custom_check +def check_single_db(env): + if not http.request: + raise CheckWarning('Could not detect DB settings.') + + dbs = http.db_list(True, http.request.httprequest) + if len(dbs) == 1: + return CheckSuccess('Odoo runs in a single-DB mode.') + + details = ( + '

Odoo runs in a multi-DB mode, which will cause API request routing to fail.

' + '

Run Odoo with --dbfilter or --database flag.

' + ) + return CheckFail( + 'Odoo runs in a multi-DB mode.', + details=details + ) diff --git a/galicea_openid_connect/views/templates.xml b/galicea_openid_connect/views/templates.xml new file mode 100644 index 0000000..facb29a --- /dev/null +++ b/galicea_openid_connect/views/templates.xml @@ -0,0 +1,24 @@ + + + + + diff --git a/galicea_openid_connect/views/views.xml b/galicea_openid_connect/views/views.xml new file mode 100644 index 0000000..519c15c --- /dev/null +++ b/galicea_openid_connect/views/views.xml @@ -0,0 +1,65 @@ + + + + galicea_openid_connect.client + 10 + +
+ + + + + +
+ + + galicea_openid_connect.client + + + + + + + + + + + + + +
+