Skip to content

Commit

Permalink
[ADD] cross_connect_server
Browse files Browse the repository at this point in the history
  • Loading branch information
paradoxxxzero committed Dec 12, 2024
1 parent 035093d commit 7c9a99d
Show file tree
Hide file tree
Showing 23 changed files with 1,396 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ repos:
- --header
- "# generated from manifests external_dependencies"
- repo: https://github.com/PyCQA/flake8
rev: 3.8.3
rev: 4.0.1
hooks:
- id: flake8
name: flake8
Expand Down
110 changes: 110 additions & 0 deletions cross_connect_server/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
====================
Cross Connect Server
====================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e7f2983ebb91caf2611da85b500923b3a91de86fbb4577c967a2a30e0ce7e739
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/16.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-16-0/server-auth-16-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=16.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 <https://github.com/OCA/server-auth/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 <https://github.com/OCA/server-auth/issues/new?body=module:%20cross_connect_server%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Akretion

Contributors
------------

- Florian Mounier [email protected]

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 <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-paradoxxxzero|

This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/16.0/cross_connect_server>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions cross_connect_server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
25 changes: 25 additions & 0 deletions cross_connect_server/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Cross Connect Server",
"version": "16.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"],
},
}
9 changes: 9 additions & 0 deletions cross_connect_server/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# 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

Check warning on line 9 in cross_connect_server/dependencies.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/dependencies.py#L9

Added line #L9 was not covered by tests
2 changes: 2 additions & 0 deletions cross_connect_server/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import cross_connect_client
from . import fastapi_endpoint
113 changes: 113 additions & 0 deletions cross_connect_server/models/cross_connect_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
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",
)

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.Many2many(
"res.users",
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)

Check warning on line 58 in cross_connect_server/models/cross_connect_client.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/models/cross_connect_client.py#L58

Added line #L58 was not covered by tests

@api.depends("user_ids")
def _compute_user_count(self):
for record in self:
record.user_count = len(record.user_ids)

Check warning on line 63 in cross_connect_server/models/cross_connect_client.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/models/cross_connect_client.py#L63

Added line #L63 was not covered by tests

def _request_access(self, access_request):
# check groups
groups = self.env["res.groups"].browse(access_request.groups)
if groups - self.group_ids or not groups.exists():
raise AccessDenied(_("You are not allowed to access this endpoint."))

Check warning on line 69 in cross_connect_server/models/cross_connect_client.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/models/cross_connect_client.py#L69

Added line #L69 was not covered by tests

# TODO: check remote id change?
user = self.user_ids.filtered(lambda u: u.login == access_request.login)
vals = {
"login": access_request.login,
"name": access_request.name,
"lang": access_request.lang,
"groups_id": [(6, 0, groups.ids)],
}
# Create user if not exists
if not user:
user = self.env["res.users"].create(vals)
self.user_ids |= user
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(_("Invalid Token")) from e

user = self.env["res.users"].browse(obj["id"])

if not user:
raise AccessDenied(_("Invalid Token"))

Check warning on line 111 in cross_connect_server/models/cross_connect_client.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/models/cross_connect_client.py#L111

Added line #L111 was not covered by tests

return user
104 changes: 104 additions & 0 deletions cross_connect_server/models/fastapi_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from secrets import token_urlsafe
from typing import Annotated, Callable, Dict, List

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":
# 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": {}}

Check warning on line 76 in cross_connect_server/models/fastapi_endpoint.py

View check run for this annotation

Codecov / codecov/patch

cross_connect_server/models/fastapi_endpoint.py#L76

Added line #L76 was not covered by tests


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
1 change: 1 addition & 0 deletions cross_connect_server/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Florian Mounier <[email protected]>
2 changes: 2 additions & 0 deletions cross_connect_server/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 7c9a99d

Please sign in to comment.