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 18f12ee
Show file tree
Hide file tree
Showing 24 changed files with 1,432 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 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 <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 <florian.mounier@akretion.com>
# 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 <florian.mounier@akretion.com>
# 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
3 changes: 3 additions & 0 deletions cross_connect_server/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import cross_connect_client
from . import fastapi_endpoint
from . import res_users
117 changes: 117 additions & 0 deletions cross_connect_server/models/cross_connect_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2024 Akretion (http://www.akretion.com).
# @author Florian Mounier <florian.mounier@akretion.com>
# 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.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)

Check warning on line 59 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#L59

Added line #L59 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 64 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#L64

Added line #L64 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 70 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#L70

Added line #L70 was not covered by tests

user = self.user_ids.filtered(
lambda u: u.cross_connect_client_user_id == access_request.id
)
vals = {
"login": access_request.login,
"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"].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,
"redirect_url": access_request.redirect_url or "/web",
},
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 115 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#L115

Added line #L115 was not covered by tests

return user, obj["redirect_url"]
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 <florian.mounier@akretion.com>
# 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
Loading

0 comments on commit 18f12ee

Please sign in to comment.