diff --git a/cross_connect_server/README.rst b/cross_connect_server/README.rst new file mode 100644 index 0000000000..4dea6cac7d --- /dev/null +++ b/cross_connect_server/README.rst @@ -0,0 +1,110 @@ +==================== +Cross Connect Server +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:03b7d29200b2b3b6c3cd0ea4077f895238659de2a6b18a3c644d1761129897de + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/18.0/cross_connect_server + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-cross_connect_server + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows other odoo instances, where the +``cross_connect_client`` module is installed and configured, users to +connect directly on this odoo instance. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +First of all after installing the module, you need to configure a +fastapi endpoint. + +In order to do that, you need to go to the menu +``FastAPI > FastAPI Endpoints`` and create a new endpoint for the client +to connect to. + +Fill the fields with the endpoint's information : + +- App: ``cross_connect`` +- Cross Connect Allowed Groups: The groups that will be allowed to be + selected for the clients groups. + +Then for each client, you will have to add an entry in the +``Cross Connect Clients`` table. + +An api key will be automatically generated for each client, this is the +key that you will have to provide to the client in order for them to +connect to the server. You will also have to choose the groups that this +client will be able to give to its users. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Florian Mounier florian.mounier@akretion.com + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px + :target: https://github.com/paradoxxxzero + :alt: paradoxxxzero + +Current `maintainer `__: + +|maintainer-paradoxxxzero| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/cross_connect_server/__init__.py b/cross_connect_server/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/cross_connect_server/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/cross_connect_server/__manifest__.py b/cross_connect_server/__manifest__.py new file mode 100644 index 0000000000..cbdb5b22e3 --- /dev/null +++ b/cross_connect_server/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Cross Connect Server", + "version": "18.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Cross Connect Server allows Cross Connect Client to connect to it.", + "category": "Tools", + "depends": ["extendable_fastapi", "server_environment"], + "website": "https://github.com/OCA/server-auth", + "data": [ + "security/res_groups.xml", + "security/ir_model_access.xml", + "views/fastapi_endpoint_views.xml", + ], + "maintainers": ["paradoxxxzero"], + "demo": [], + "installable": True, + "license": "AGPL-3", + "external_dependencies": { + "python": ["pyjwt"], + }, +} diff --git a/cross_connect_server/dependencies.py b/cross_connect_server/dependencies.py new file mode 100644 index 0000000000..6f2ce7c3af --- /dev/null +++ b/cross_connect_server/dependencies.py @@ -0,0 +1,9 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from .models.cross_connect_client import CrossConnectClient + + +def authenticated_cross_connect_client() -> CrossConnectClient: + pass diff --git a/cross_connect_server/i18n/cross_connect_server.pot b/cross_connect_server/i18n/cross_connect_server.pot new file mode 100644 index 0000000000..ae6fe29da5 --- /dev/null +++ b/cross_connect_server/i18n/cross_connect_server.pot @@ -0,0 +1,221 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cross_connect_server +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__api_key +msgid "API Key" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__app +msgid "App" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__bypass_user_mail_re +msgid "Bypass Users Email Regexes" +msgstr "" + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/routers/cross_connect.py:0 +#, python-format +msgid "Client not found" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__create_uid +msgid "Created by" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__create_date +msgid "Created on" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__allowed_group_ids +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_allowed_group_ids +msgid "Cross Connect Allowed Groups" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_cross_connect_client +#: model:ir.model.fields,field_description:cross_connect_server.field_res_users__cross_connect_client_id +msgid "Cross Connect Client" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_res_users__cross_connect_client_user_id +msgid "Cross Connect Client User ID" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_client_ids +msgid "Cross Connect Clients" +msgstr "" + +#. module: cross_connect_server +#: model_terms:ir.ui.view,arch_db:cross_connect_server.fastapi_endpoint_form_view +msgid "Cross Connect Configuration" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields.selection,name:cross_connect_server.selection__fastapi_endpoint__app__cross_connect +msgid "Cross Connect Endpoint" +msgstr "" + +#. module: cross_connect_server +#: model:res.groups,name:cross_connect_server.group_cross_connect_manager +msgid "Cross Connect Manager" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_secret_key +msgid "Cross Connect Secret Key" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__user_count +msgid "Cross Connected User Count" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__display_name +msgid "Display Name" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__endpoint_id +msgid "Endpoint" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__group_ids +msgid "Groups" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__id +msgid "ID" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__bypass_user_mail_re +msgid "" +"If set, users with an email matching one of these regex will bypass the " +"token user/login creation. The regexes are comma separated." +msgstr "" + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/models/cross_connect_client.py:0 +#: code:addons/cross_connect_server/models/cross_connect_client.py:0 +#, python-format +msgid "Invalid Token" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client____last_update +msgid "Last Modified on" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__write_date +msgid "Last Updated on" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__name +msgid "Name" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__api_key +msgid "The API key to give to configure on the client." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_client_ids +msgid "The clients that can access this endpoint." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_res_users__cross_connect_client_id +msgid "The cross connect client that created this user." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__allowed_group_ids +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_allowed_group_ids +msgid "The groups that can access the cross connect clients of this endpoint." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__group_ids +msgid "The groups that this client belongs to." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__user_count +msgid "The number of users created by this cross connection." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_secret_key +msgid "The secret key used for cross connection." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_res_users__cross_connect_client_user_id +msgid "The user ID on the cross connect client." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__user_ids +msgid "The users created by this cross connection." +msgstr "" + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_res_users +msgid "User" +msgstr "" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__user_ids +msgid "Users" +msgstr "" + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/models/cross_connect_client.py:0 +#, python-format +msgid "You are not allowed to access this endpoint." +msgstr "" diff --git a/cross_connect_server/i18n/it.po b/cross_connect_server/i18n/it.po new file mode 100644 index 0000000000..070e3f76f8 --- /dev/null +++ b/cross_connect_server/i18n/it.po @@ -0,0 +1,227 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cross_connect_server +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-02-11 09:31+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__api_key +msgid "API Key" +msgstr "Chiave API" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__app +msgid "App" +msgstr "Applicazione" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__bypass_user_mail_re +msgid "Bypass Users Email Regexes" +msgstr "Baypassa espressione regolare e-mail utenti" + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/routers/cross_connect.py:0 +#, python-format +msgid "Client not found" +msgstr "Client non trovato" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__allowed_group_ids +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_allowed_group_ids +msgid "Cross Connect Allowed Groups" +msgstr "Gruppi abilitati cross-connect" + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_cross_connect_client +#: model:ir.model.fields,field_description:cross_connect_server.field_res_users__cross_connect_client_id +msgid "Cross Connect Client" +msgstr "Client cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_res_users__cross_connect_client_user_id +msgid "Cross Connect Client User ID" +msgstr "ID utente cliente cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_client_ids +msgid "Cross Connect Clients" +msgstr "Client cross-connect" + +#. module: cross_connect_server +#: model_terms:ir.ui.view,arch_db:cross_connect_server.fastapi_endpoint_form_view +msgid "Cross Connect Configuration" +msgstr "Configurazione cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields.selection,name:cross_connect_server.selection__fastapi_endpoint__app__cross_connect +msgid "Cross Connect Endpoint" +msgstr "Endopoint cross-connect" + +#. module: cross_connect_server +#: model:res.groups,name:cross_connect_server.group_cross_connect_manager +msgid "Cross Connect Manager" +msgstr "Gestore cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_fastapi_endpoint__cross_connect_secret_key +msgid "Cross Connect Secret Key" +msgstr "Chiave segreta cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__user_count +msgid "Cross Connected User Count" +msgstr "Conteggio utenti cross-connect" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__endpoint_id +msgid "Endpoint" +msgstr "Endpoint" + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_fastapi_endpoint +msgid "FastAPI Endpoint" +msgstr "Endpoint FastAPI" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__group_ids +msgid "Groups" +msgstr "Gruppi" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__id +msgid "ID" +msgstr "ID" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__bypass_user_mail_re +msgid "" +"If set, users with an email matching one of these regex will bypass the " +"token user/login creation. The regexes are comma separated." +msgstr "" +"Se impostato, gli utenti con un indirizzo e-mail corrispondente a una di " +"queste espressioni regolari ignoreranno la creazione del token utente/" +"accesso. Le espressioni regolari sono separate da virgole." + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/models/cross_connect_client.py:0 +#, python-format +msgid "Invalid Token" +msgstr "Token non valido" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__name +msgid "Name" +msgstr "Nome" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__server_env_defaults +msgid "Server Env Defaults" +msgstr "Predefiniti ambiente server" + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__api_key +msgid "The API key to give to configure on the client." +msgstr "Chiave API da fornire per configurare il client." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_client_ids +msgid "The clients that can access this endpoint." +msgstr "I client che possono accedere questo endpoint." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_res_users__cross_connect_client_id +msgid "The cross connect client that created this user." +msgstr "Il client cross-connect che ha creato questo utente." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__allowed_group_ids +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_allowed_group_ids +msgid "The groups that can access the cross connect clients of this endpoint." +msgstr "" +"I gruppi che possono accedere i client cross-connect di questo endpoint." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__group_ids +msgid "The groups that this client belongs to." +msgstr "I gruppi a cui appartiene questo client." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__user_count +msgid "The number of users created by this cross connection." +msgstr "Il numero di utenti creati da questa cross-connect." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_fastapi_endpoint__cross_connect_secret_key +msgid "The secret key used for cross connection." +msgstr "La chiave segreta usata per la cross-connect." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_res_users__cross_connect_client_user_id +msgid "The user ID on the cross connect client." +msgstr "L'ID utente nel client cross-connect." + +#. module: cross_connect_server +#: model:ir.model.fields,help:cross_connect_server.field_cross_connect_client__user_ids +msgid "The users created by this cross connection." +msgstr "Gli utenti creati da questa cross-connect." + +#. module: cross_connect_server +#: model:ir.model,name:cross_connect_server.model_res_users +msgid "User" +msgstr "Utente" + +#. module: cross_connect_server +#: model:ir.model.fields,field_description:cross_connect_server.field_cross_connect_client__user_ids +msgid "Users" +msgstr "Utenti" + +#. module: cross_connect_server +#. odoo-python +#: code:addons/cross_connect_server/models/cross_connect_client.py:0 +#, python-format +msgid "You are not allowed to access this endpoint." +msgstr "Non si è autorizzati ad accedere a questo endpoint." diff --git a/cross_connect_server/models/__init__.py b/cross_connect_server/models/__init__.py new file mode 100644 index 0000000000..108ba9f5c2 --- /dev/null +++ b/cross_connect_server/models/__init__.py @@ -0,0 +1,3 @@ +from . import cross_connect_client +from . import fastapi_endpoint +from . import res_users diff --git a/cross_connect_server/models/cross_connect_client.py b/cross_connect_server/models/cross_connect_client.py new file mode 100644 index 0000000000..23637bec37 --- /dev/null +++ b/cross_connect_server/models/cross_connect_client.py @@ -0,0 +1,149 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import re +from datetime import datetime, timedelta, timezone +from secrets import token_urlsafe + +import jwt + +from odoo import api, fields, models +from odoo.exceptions import AccessDenied + + +class CrossConnectClient(models.Model): + _name = "cross.connect.client" + _description = "Cross Connect Client" + _inherit = "server.env.mixin" + + name = fields.Char(required=True) + + endpoint_id = fields.Many2one( + "fastapi.endpoint", + required=True, + string="Endpoint", + ) + + api_key = fields.Char( + required=True, + string="API Key", + help="The API key to give to configure on the client.", + default=lambda self: self._generate_api_key(), + ) + + allowed_group_ids = fields.Many2many( + related="endpoint_id.cross_connect_allowed_group_ids", + ) + + bypass_user_mail_re = fields.Char( + string="Bypass Users Email Regexes", + help=( + "If set, users with an email matching one of these regex will bypass " + "the token user/login creation. The regexes are comma separated." + ), + ) + + group_ids = fields.Many2many( + "res.groups", + string="Groups", + help="The groups that this client belongs to.", + domain="[('id', 'in', allowed_group_ids)]", + ) + + user_ids = fields.One2many( + "res.users", + "cross_connect_client_id", + string="Users", + help="The users created by this cross connection.", + ) + user_count = fields.Integer( + compute="_compute_user_count", + string="Cross Connected User Count", + help="The number of users created by this cross connection.", + ) + + @api.model + def _generate_api_key(self): + # generate random ~64 chars secret key + return token_urlsafe(64) + + @api.depends("user_ids") + def _compute_user_count(self): + for record in self: + record.user_count = len(record.user_ids) + + def _request_access(self, access_request): + if self.bypass_user_mail_re and any( + re.search(mail_re.strip(), access_request.email) + for mail_re in self.bypass_user_mail_re.split(",") + ): + return "bypass" + + # check groups + groups = self.env["res.groups"].browse(access_request.groups) + if groups - self.group_ids or not groups.exists(): + raise AccessDenied( + self.env._("You are not allowed to access this endpoint.") + ) + + user = self.user_ids.filtered( + lambda u: u.cross_connect_client_user_id == access_request.id + ) + + # Fallback to default lang if not installed + if access_request.lang not in [ + code for code, _name in self.env["res.lang"].get_installed() + ]: + access_request.lang = "en_US" + + vals = { + "login": f"{self.id}_{access_request.id}_{access_request.login}", + "email": access_request.email, + "name": access_request.name, + "lang": access_request.lang, + "groups_id": [(6, 0, groups.ids)], + "cross_connect_client_id": self.id, + "cross_connect_client_user_id": access_request.id, + } + # Create user if not exists + if not user: + user = ( + self.env["res.users"].with_context(no_reset_password=True).create(vals) + ) + else: + user.write(vals) + + return jwt.encode( + { + "exp": datetime.now(tz=timezone.utc) + timedelta(minutes=2), + "aud": str(self.id), + "id": user.id, + }, + self.endpoint_id.cross_connect_secret_key, + algorithm="HS256", + ) + + def _log_from_token(self, token): + try: + obj = jwt.decode( + token, + self.endpoint_id.cross_connect_secret_key, + audience=str(self.id), + options={"require": ["exp", "aud", "id"]}, + algorithms=["HS256"], + ) + except jwt.PyJWTError as e: + raise AccessDenied(self.env._("Invalid Token")) from e + + user = self.env["res.users"].browse(obj["id"]) + + if not user: + raise AccessDenied(self.env._("Invalid Token")) + + return user + + def _get_final_redirect_url(self, **params): + """Get the final redirect url after login. + Override this method to customize the local landing action. + """ + return "/web" diff --git a/cross_connect_server/models/fastapi_endpoint.py b/cross_connect_server/models/fastapi_endpoint.py new file mode 100644 index 0000000000..2f212a36bb --- /dev/null +++ b/cross_connect_server/models/fastapi_endpoint.py @@ -0,0 +1,105 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections.abc import Callable +from secrets import token_urlsafe +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import APIKeyHeader + +from odoo import api, fields, models +from odoo.api import Environment + +from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env + +from ..dependencies import authenticated_cross_connect_client +from ..routers import cross_connect_router +from .cross_connect_client import CrossConnectClient + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + app = fields.Selection( + selection_add=[("cross_connect", "Cross Connect Endpoint")], + ondelete={"cross_connect": "cascade"}, + ) + + cross_connect_client_ids = fields.One2many( + "cross.connect.client", + "endpoint_id", + string="Cross Connect Clients", + help="The clients that can access this endpoint.", + ) + cross_connect_allowed_group_ids = fields.Many2many( + "res.groups", + string="Cross Connect Allowed Groups", + help="The groups that can access the cross connect clients of this endpoint.", + ) + cross_connect_secret_key = fields.Char( + help="The secret key used for cross connection.", + required=True, + default=lambda self: self._generate_secret_key(), + ) + + @api.model + def _generate_secret_key(self): + # generate random ~64 chars secret key + return token_urlsafe(64) + + def _get_fastapi_routers(self) -> list[APIRouter]: + routers = super()._get_fastapi_routers() + + if self.app == "cross_connect": + routers += [cross_connect_router] + + return routers + + def _get_app_dependencies_overrides(self) -> dict[Callable, Callable]: + overrides = super()._get_app_dependencies_overrides() + + if self.app == "cross_connect": + overrides[authenticated_cross_connect_client] = ( + api_key_based_authenticated_cross_connect_client + ) + + return overrides + + def _get_routing_info(self): + if self.app == "cross_connect" and self.save_http_session: + # Force to not save the HTTP session for the login to work correctly + self.save_http_session = False + return super()._get_routing_info() + + @property + def _server_env_fields(self): + return {"cross_connect_secret_key": {}} + + +def api_key_based_authenticated_cross_connect_client( + api_key: Annotated[ + str, + Depends( + APIKeyHeader( + name="api-key", + description="Cross Connect Client API key.", + ) + ), + ], + fastapi_endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)], + env: Annotated[Environment, Depends(odoo_env)], +) -> CrossConnectClient: + cross_connect_client = ( + env["cross.connect.client"] + .sudo() + .search( + [("api_key", "=", api_key), ("endpoint_id", "=", fastapi_endpoint.id)], + limit=1, + ) + ) + if not cross_connect_client: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key" + ) + return cross_connect_client diff --git a/cross_connect_server/models/res_users.py b/cross_connect_server/models/res_users.py new file mode 100644 index 0000000000..d98d7ef9c5 --- /dev/null +++ b/cross_connect_server/models/res_users.py @@ -0,0 +1,19 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + cross_connect_client_id = fields.Many2one( + "cross.connect.client", + string="Cross Connect Client", + help="The cross connect client that created this user.", + ) + cross_connect_client_user_id = fields.Integer( + string="Cross Connect Client User ID", + help="The user ID on the cross connect client.", + ) diff --git a/cross_connect_server/pyproject.toml b/cross_connect_server/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/cross_connect_server/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/cross_connect_server/readme/CONTRIBUTORS.md b/cross_connect_server/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..328a37da87 --- /dev/null +++ b/cross_connect_server/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Florian Mounier diff --git a/cross_connect_server/readme/DESCRIPTION.md b/cross_connect_server/readme/DESCRIPTION.md new file mode 100644 index 0000000000..4a034abe18 --- /dev/null +++ b/cross_connect_server/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows other odoo instances, where the `cross_connect_client` module is +installed and configured, users to connect directly on this odoo instance. diff --git a/cross_connect_server/readme/USAGE.md b/cross_connect_server/readme/USAGE.md new file mode 100644 index 0000000000..6d6e80bd57 --- /dev/null +++ b/cross_connect_server/readme/USAGE.md @@ -0,0 +1,17 @@ +First of all after installing the module, you need to configure a fastapi endpoint. + +In order to do that, you need to go to the menu `FastAPI > FastAPI Endpoints` and create +a new endpoint for the client to connect to. + +Fill the fields with the endpoint's information : + +- App: `cross_connect` +- Cross Connect Allowed Groups: The groups that will be allowed to be selected for the + clients groups. + +Then for each client, you will have to add an entry in the `Cross Connect Clients` +table. + +An api key will be automatically generated for each client, this is the key that you +will have to provide to the client in order for them to connect to the server. You will +also have to choose the groups that this client will be able to give to its users. diff --git a/cross_connect_server/routers/__init__.py b/cross_connect_server/routers/__init__.py new file mode 100644 index 0000000000..114cbd2c8b --- /dev/null +++ b/cross_connect_server/routers/__init__.py @@ -0,0 +1 @@ +from .cross_connect import cross_connect_router diff --git a/cross_connect_server/routers/cross_connect.py b/cross_connect_server/routers/cross_connect.py new file mode 100644 index 0000000000..6d4be7abfd --- /dev/null +++ b/cross_connect_server/routers/cross_connect.py @@ -0,0 +1,86 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from typing import Annotated + +from fastapi import APIRouter, Depends, Request +from fastapi.responses import RedirectResponse + +from odoo import api +from odoo.exceptions import MissingError +from odoo.http import SESSION_LIFETIME, root + +from odoo.addons.fastapi.dependencies import odoo_env + +from ..dependencies import authenticated_cross_connect_client +from ..models.cross_connect_client import CrossConnectClient +from ..schemas import AccessRequest, AccessResponse, SyncResponse + +cross_connect_router = APIRouter(tags=["Cross Connect"]) + + +@cross_connect_router.get("/cross_connect/sync") +async def sync( + cross_connect_client: Annotated[ + CrossConnectClient, Depends(authenticated_cross_connect_client) + ], +) -> SyncResponse: + """Send back to client sync information.""" + return SyncResponse.from_groups(cross_connect_client.group_ids) + + +@cross_connect_router.post("/cross_connect/access") +async def access( + cross_connect_client: Annotated[ + CrossConnectClient, Depends(authenticated_cross_connect_client) + ], + access_request: AccessRequest, +) -> AccessResponse: + """Send back to client a token.""" + return AccessResponse.from_params( + client_id=cross_connect_client.id, + token=cross_connect_client.sudo()._request_access(access_request), + ) + + +@cross_connect_router.get("/cross_connect/login/{client_id}/{token}") +async def login( + client_id: int, + token: str, + env: Annotated[api.Environment, Depends(odoo_env)], + request: Request, +) -> RedirectResponse: + """Log user and redirect to odoo index.""" + cross_connect_client = env["cross.connect.client"].sudo().browse(client_id) + if not cross_connect_client: + raise MissingError(env._("Client not found")) + params = request.query_params + if token == "bypass": + return RedirectResponse( + url=cross_connect_client._get_final_redirect_url(bypass=True, **params) + ) + + user = cross_connect_client.sudo()._log_from_token(token) + user = user.with_user(user) + user._update_last_login() + env = env(user=user.id) + + # Create a odoo session + session = root.session_store.new() + session.db = env.cr.dbname + session.uid = user.id + session.login = user.login + session.context = dict(env["res.users"].context_get()) + session.session_token = user._compute_session_token(session.sid) + url = cross_connect_client._get_final_redirect_url(session=session, **params) + root.session_store.save(session) + # Redirect after login + response = RedirectResponse(url=url) + response.set_cookie( + "session_id", + session.sid, + httponly=True, + max_age=SESSION_LIFETIME, + ) + return response diff --git a/cross_connect_server/schemas.py b/cross_connect_server/schemas.py new file mode 100644 index 0000000000..b1295e3729 --- /dev/null +++ b/cross_connect_server/schemas.py @@ -0,0 +1,46 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from extendable_pydantic import StrictExtendableBaseModel + + +class CrossConnectGroup(StrictExtendableBaseModel): + id: int + name: str + comment: str | None = None + + @classmethod + def from_group(cls, group): + return cls.model_construct( + id=group.id, + name=group.full_name, + comment=group.comment or None, + ) + + +class SyncResponse(StrictExtendableBaseModel): + groups: list[CrossConnectGroup] + + @classmethod + def from_groups(cls, groups): + return cls.model_construct( + groups=[CrossConnectGroup.from_group(group) for group in groups] + ) + + +class AccessRequest(StrictExtendableBaseModel, extra="ignore"): + id: int + name: str + login: str + email: str + lang: str + groups: list[int] + + +class AccessResponse(StrictExtendableBaseModel): + client_id: int + token: str + + @classmethod + def from_params(cls, token, client_id): + return cls.model_construct(token=token, client_id=client_id) diff --git a/cross_connect_server/security/ir_model_access.xml b/cross_connect_server/security/ir_model_access.xml new file mode 100644 index 0000000000..cac56e1398 --- /dev/null +++ b/cross_connect_server/security/ir_model_access.xml @@ -0,0 +1,17 @@ + + + + + Cross Connect Client: Manager RW access + + + + + + + + diff --git a/cross_connect_server/security/res_groups.xml b/cross_connect_server/security/res_groups.xml new file mode 100644 index 0000000000..33defad930 --- /dev/null +++ b/cross_connect_server/security/res_groups.xml @@ -0,0 +1,15 @@ + + + + + Cross Connect Manager + + + diff --git a/cross_connect_server/static/description/icon.png b/cross_connect_server/static/description/icon.png new file mode 100644 index 0000000000..1dcc49c24f Binary files /dev/null and b/cross_connect_server/static/description/icon.png differ diff --git a/cross_connect_server/static/description/index.html b/cross_connect_server/static/description/index.html new file mode 100644 index 0000000000..9d297c1433 --- /dev/null +++ b/cross_connect_server/static/description/index.html @@ -0,0 +1,448 @@ + + + + + +Cross Connect Server + + + +
+

Cross Connect Server

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

This module allows other odoo instances, where the +cross_connect_client module is installed and configured, users to +connect directly on this odoo instance.

+

Table of contents

+ +
+

Usage

+

First of all after installing the module, you need to configure a +fastapi endpoint.

+

In order to do that, you need to go to the menu +FastAPI > FastAPI Endpoints and create a new endpoint for the client +to connect to.

+

Fill the fields with the endpoint’s information :

+
    +
  • App: cross_connect
  • +
  • Cross Connect Allowed Groups: The groups that will be allowed to be +selected for the clients groups.
  • +
+

Then for each client, you will have to add an entry in the +Cross Connect Clients table.

+

An api key will be automatically generated for each client, this is the +key that you will have to provide to the client in order for them to +connect to the server. You will also have to choose the groups that this +client will be able to give to its users.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

paradoxxxzero

+

This module is part of the OCA/server-auth project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/cross_connect_server/tests/__init__.py b/cross_connect_server/tests/__init__.py new file mode 100644 index 0000000000..f49cef96d5 --- /dev/null +++ b/cross_connect_server/tests/__init__.py @@ -0,0 +1 @@ +from . import test_cross_connect_server diff --git a/cross_connect_server/tests/test_cross_connect_server.py b/cross_connect_server/tests/test_cross_connect_server.py new file mode 100644 index 0000000000..371ffefcd0 --- /dev/null +++ b/cross_connect_server/tests/test_cross_connect_server.py @@ -0,0 +1,375 @@ +# Copyright 2024 Akretion (http://www.akretion.com). +# @author Florian Mounier +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.http import root +from odoo.tests.common import RecordCapturer, tagged + +from odoo.addons.extendable_fastapi.tests.common import FastAPITransactionCase + +from ..routers import cross_connect_router + + +@tagged("post_install", "-at_install") +class TestCrossConnectServer(FastAPITransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.endpoint = cls.env["fastapi.endpoint"].create( + { + "name": "Cross Connect Server Endpoint", + "root_path": "/api", + "app": "cross_connect", + } + ) + cls.available_groups = ( + cls.env.ref("base.group_user") + | cls.env.ref("fastapi.group_fastapi_user") + | cls.env.ref("fastapi.group_fastapi_manager") + ) + + cls.endpoint.cross_connect_allowed_group_ids = cls.available_groups + + cls.client = cls.env["cross.connect.client"].create( + { + "name": "Test Client", + "endpoint_id": cls.endpoint.id, + "api_key": "server-api-key", + "group_ids": [ + ( + 6, + 0, + ( + cls.available_groups + - cls.env.ref("fastapi.group_fastapi_manager") + ).ids, + ) + ], + } + ) + + cls.other_client = cls.env["cross.connect.client"].create( + { + "name": "Other Test Client", + "endpoint_id": cls.endpoint.id, + "api_key": "other-server-api-key", + "group_ids": [ + ( + 6, + 0, + (cls.available_groups - cls.env.ref("base.group_user")).ids, + ) + ], + } + ) + + cls.endpoint_user = cls.env["res.users"].create( + { + "name": "FastAPI Endpoint User", + "login": "fastapi_endpoint_user", + "groups_id": [ + (6, 0, [cls.env.ref("fastapi.group_fastapi_endpoint_runner").id]) + ], + } + ) + + cls.endpoint._handle_registry_sync(cls.endpoint.ids) + + cls.default_fastapi_running_user = cls.endpoint_user + cls.default_fastapi_router = cross_connect_router + cls.default_fastapi_app = cls.endpoint._get_app() + cls.default_fastapi_dependency_overrides = ( + cls.default_fastapi_app.dependency_overrides + ) + cls.default_fastapi_app.exception_handlers = {} + + def test_base(self): + self.assertTrue(self.endpoint.cross_connect_secret_key) + self.assertEqual(len(self.endpoint.cross_connect_client_ids), 2) + self.assertFalse(self.endpoint.save_http_session) + self.assertFalse(self.client.user_ids) + + def test_sync_ok(self): + with self._create_test_client() as test_client: + response = test_client.get( + "/cross_connect/sync", headers={"api-key": "server-api-key"} + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "groups": [ + { + "id": self.env.ref("base.group_user").id, + "name": "User types / Internal User", + "comment": None, + }, + { + "id": self.env.ref("fastapi.group_fastapi_user").id, + "name": "FastAPI / User", + "comment": None, + }, + ] + }, + ) + + def test_sync_other(self): + with self._create_test_client() as test_client: + response = test_client.get( + "/cross_connect/sync", headers={"api-key": "other-server-api-key"} + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "groups": [ + { + "id": self.env.ref("fastapi.group_fastapi_manager").id, + "name": "FastAPI / Administrator", + "comment": None, + }, + { + "id": self.env.ref("fastapi.group_fastapi_user").id, + "name": "FastAPI / User", + "comment": None, + }, + ] + }, + ) + + def test_sync_401(self): + with self._create_test_client(raise_server_exceptions=False) as test_client: + response = test_client.get( + "/cross_connect/sync", headers={"api-key": "wrong-api-key"} + ) + self.assertEqual(response.status_code, 401) + + def test_access_ok(self): + with RecordCapturer(self.env["res.users"], []) as rc: + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + + self.assertEqual(response.status_code, 200) + json = response.json() + self.assertEqual(json["client_id"], self.client.id) + self.assertTrue(json["token"]) + + self.assertEqual(len(rc.records), 1) + new_user = rc.records[0] + self.assertEqual(new_user.name, "Client User") + self.assertEqual(new_user.login, f"{self.client.id}_12_user@client.example.org") + self.assertEqual(new_user.email, "user@client.example.org") + self.assertEqual(new_user.lang, "en_US") + self.assertEqual(new_user.cross_connect_client_id.id, self.client.id) + self.assertEqual(new_user.cross_connect_client_user_id, 12) + self.assertIn( + self.env.ref("base.group_user"), + new_user.groups_id, + ) + self.assertNotIn(self.env.ref("fastapi.group_fastapi_user"), new_user.groups_id) + self.assertNotIn( + self.env.ref("fastapi.group_fastapi_manager"), new_user.groups_id + ) + + def test_access_401(self): + with RecordCapturer(self.env["res.users"], []) as rc: + with self._create_test_client(raise_server_exceptions=False) as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "wrong-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + + self.assertEqual(response.status_code, 401) + self.assertEqual(len(rc.records), 0) + + def test_access_wrong_groups(self): + with RecordCapturer(self.env["res.users"], []) as rc: + with self._create_test_client(raise_server_exceptions=False) as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "wrong-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("fastapi.group_fastapi_manager").id, + ], + }, + ) + + self.assertEqual(response.status_code, 401) + self.assertEqual(len(rc.records), 0) + + def test_access_existing(self): + with RecordCapturer(self.env["res.users"], []) as rc: + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + self.assertEqual(response.status_code, 200) + + with RecordCapturer(self.env["res.users"], []) as rc2: + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User2", + "login": "user2@client.example.org", + "email": "user2@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("fastapi.group_fastapi_user").id, + ], + }, + ) + + self.assertEqual(response.status_code, 200) + json = response.json() + self.assertEqual(json["client_id"], self.client.id) + self.assertTrue(json["token"]) + + self.assertEqual(len(rc.records), 1) + self.assertEqual(len(rc2.records), 0) + new_user = rc.records[0] + self.assertEqual(new_user.name, "Client User2") + self.assertEqual( + new_user.login, f"{self.client.id}_12_user2@client.example.org" + ) + self.assertEqual(new_user.email, "user2@client.example.org") + self.assertEqual(new_user.lang, "en_US") + self.assertIn(self.env.ref("fastapi.group_fastapi_user"), new_user.groups_id) + self.assertNotIn( + self.env.ref("fastapi.group_fastapi_manager"), new_user.groups_id + ) + + def test_login_ok(self): + with RecordCapturer(self.env["res.users"], []) as rc: + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + self.assertEqual(response.status_code, 200) + + new_user = rc.records[0] + + json = response.json() + + with self._create_test_client() as test_client: + response = test_client.get( + f"/cross_connect/login/{json['client_id']}/{json['token']}", + follow_redirects=False, + ) + + self.assertEqual(response.status_code, 307) + self.assertEqual(response.headers["location"], "/web") + self.assertIn("session_id", response.cookies) + self.assertEqual( + root.session_store.get(response.cookies["session_id"]).get("uid"), + new_user.id, + ) + + def test_login_wrong_client(self): + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + self.assertEqual(response.status_code, 200) + + json = response.json() + + with self._create_test_client(raise_server_exceptions=False) as test_client: + response = test_client.get( + f"/cross_connect/login/{self.other_client.id}/{json['token']}", + follow_redirects=False, + ) + + self.assertEqual(response.status_code, 403) + + def test_login_wrong_token(self): + with self._create_test_client() as test_client: + response = test_client.post( + "/cross_connect/access", + headers={"api-key": "server-api-key"}, + json={ + "id": 12, + "name": "Client User", + "login": "user@client.example.org", + "email": "user@client.example.org", + "lang": "en_US", + "groups": [ + self.env.ref("base.group_user").id, + ], + }, + ) + self.assertEqual(response.status_code, 200) + + json = response.json() + + with self._create_test_client(raise_server_exceptions=False) as test_client: + response = test_client.get( + f"/cross_connect/login/{json['client_id']}/wrong-token", + follow_redirects=False, + ) + + self.assertEqual(response.status_code, 403) diff --git a/cross_connect_server/views/fastapi_endpoint_views.xml b/cross_connect_server/views/fastapi_endpoint_views.xml new file mode 100644 index 0000000000..4330e7aacb --- /dev/null +++ b/cross_connect_server/views/fastapi_endpoint_views.xml @@ -0,0 +1,45 @@ + + + + + fastapi.endpoint + + + + app == 'cross_connect' + + + + + + + + + + + + + + + + + + + diff --git a/test-requirements.txt b/test-requirements.txt index 2cb24f43db..265b7f6b1e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,2 @@ responses +httpx