Skip to content

Commit e2ff3f5

Browse files
committed
[ADD] cross_connect_server
1 parent 00b48ac commit e2ff3f5

23 files changed

Lines changed: 1443 additions & 0 deletions

cross_connect_server/README.rst

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
====================
2+
Cross Connect Server
3+
====================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:e7f2983ebb91caf2611da85b500923b3a91de86fbb4577c967a2a30e0ce7e739
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-auth/tree/16.0/cross_connect_server
21+
:alt: OCA/server-auth
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-auth-16-0/server-auth-16-0-cross_connect_server
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module allows other odoo instances, where the
32+
``cross_connect_client`` module is installed and configured, users to
33+
connect directly on this odoo instance.
34+
35+
**Table of contents**
36+
37+
.. contents::
38+
:local:
39+
40+
Usage
41+
=====
42+
43+
First of all after installing the module, you need to configure a
44+
fastapi endpoint.
45+
46+
In order to do that, you need to go to the menu
47+
``FastAPI > FastAPI Endpoints`` and create a new endpoint for the client
48+
to connect to.
49+
50+
Fill the fields with the endpoint's information :
51+
52+
- App: ``cross_connect``
53+
- Cross Connect Allowed Groups: The groups that will be allowed to be
54+
selected for the clients groups.
55+
56+
Then for each client, you will have to add an entry in the
57+
``Cross Connect Clients`` table.
58+
59+
An api key will be automatically generated for each client, this is the
60+
key that you will have to provide to the client in order for them to
61+
connect to the server. You will also have to choose the groups that this
62+
client will be able to give to its users.
63+
64+
Bug Tracker
65+
===========
66+
67+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
68+
In case of trouble, please check there if your issue has already been reported.
69+
If you spotted it first, help us to smash it by providing a detailed and welcomed
70+
`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**>`_.
71+
72+
Do not contact contributors directly about support or help with technical issues.
73+
74+
Credits
75+
=======
76+
77+
Authors
78+
-------
79+
80+
* Akretion
81+
82+
Contributors
83+
------------
84+
85+
- Florian Mounier florian.mounier@akretion.com
86+
87+
Maintainers
88+
-----------
89+
90+
This module is maintained by the OCA.
91+
92+
.. image:: https://odoo-community.org/logo.png
93+
:alt: Odoo Community Association
94+
:target: https://odoo-community.org
95+
96+
OCA, or the Odoo Community Association, is a nonprofit organization whose
97+
mission is to support the collaborative development of Odoo features and
98+
promote its widespread use.
99+
100+
.. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px
101+
:target: https://github.com/paradoxxxzero
102+
:alt: paradoxxxzero
103+
104+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
105+
106+
|maintainer-paradoxxxzero|
107+
108+
This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/16.0/cross_connect_server>`_ project on GitHub.
109+
110+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

cross_connect_server/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Cross Connect Server",
7+
"version": "16.0.1.0.0",
8+
"author": "Akretion, Odoo Community Association (OCA)",
9+
"summary": "Cross Connect Server allows Cross Connect Client to connect to it.",
10+
"category": "Tools",
11+
"depends": ["extendable_fastapi", "server_environment"],
12+
"website": "https://github.com/OCA/server-auth",
13+
"data": [
14+
"security/res_groups.xml",
15+
"security/ir_model_access.xml",
16+
"views/fastapi_endpoint_views.xml",
17+
],
18+
"maintainers": ["paradoxxxzero"],
19+
"demo": [],
20+
"installable": True,
21+
"license": "AGPL-3",
22+
"external_dependencies": {
23+
"python": ["pyjwt"],
24+
},
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from .models.cross_connect_client import CrossConnectClient
6+
7+
8+
def authenticated_cross_connect_client() -> CrossConnectClient:
9+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import cross_connect_client
2+
from . import fastapi_endpoint
3+
from . import res_users
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from datetime import datetime, timedelta, timezone
5+
from secrets import token_urlsafe
6+
7+
import jwt
8+
9+
from odoo import _, api, fields, models
10+
from odoo.exceptions import AccessDenied
11+
12+
13+
class CrossConnectClient(models.Model):
14+
_name = "cross.connect.client"
15+
_description = "Cross Connect Client"
16+
_inherit = "server.env.mixin"
17+
18+
name = fields.Char(required=True)
19+
20+
endpoint_id = fields.Many2one(
21+
"fastapi.endpoint",
22+
required=True,
23+
string="Endpoint",
24+
)
25+
26+
api_key = fields.Char(
27+
required=True,
28+
string="API Key",
29+
help="The API key to give to configure on the client.",
30+
default=lambda self: self._generate_api_key(),
31+
)
32+
33+
allowed_group_ids = fields.Many2many(
34+
related="endpoint_id.cross_connect_allowed_group_ids",
35+
)
36+
37+
group_ids = fields.Many2many(
38+
"res.groups",
39+
string="Groups",
40+
help="The groups that this client belongs to.",
41+
domain="[('id', 'in', allowed_group_ids)]",
42+
)
43+
44+
user_ids = fields.One2many(
45+
"res.users",
46+
"cross_connect_client_id",
47+
string="Users",
48+
help="The users created by this cross connection.",
49+
)
50+
user_count = fields.Integer(
51+
compute="_compute_user_count",
52+
string="Cross Connected User Count",
53+
help="The number of users created by this cross connection.",
54+
)
55+
56+
@api.model
57+
def _generate_api_key(self):
58+
# generate random ~64 chars secret key
59+
return token_urlsafe(64)
60+
61+
@api.depends("user_ids")
62+
def _compute_user_count(self):
63+
for record in self:
64+
record.user_count = len(record.user_ids)
65+
66+
def _request_access(self, access_request):
67+
# check groups
68+
groups = self.env["res.groups"].browse(access_request.groups)
69+
if groups - self.group_ids or not groups.exists():
70+
raise AccessDenied(_("You are not allowed to access this endpoint."))
71+
72+
user = self.user_ids.filtered(
73+
lambda u: u.cross_connect_client_user_id == access_request.id
74+
)
75+
vals = {
76+
"login": access_request.login,
77+
"email": access_request.email,
78+
"name": access_request.name,
79+
"lang": access_request.lang,
80+
"groups_id": [(6, 0, groups.ids)],
81+
"cross_connect_client_id": self.id,
82+
"cross_connect_client_user_id": access_request.id,
83+
}
84+
# Create user if not exists
85+
if not user:
86+
user = self.env["res.users"].create(vals)
87+
else:
88+
user.write(vals)
89+
90+
return jwt.encode(
91+
{
92+
"exp": datetime.now(tz=timezone.utc) + timedelta(minutes=2),
93+
"aud": str(self.id),
94+
"id": user.id,
95+
"redirect_url": access_request.redirect_url or "/web",
96+
},
97+
self.endpoint_id.cross_connect_secret_key,
98+
algorithm="HS256",
99+
)
100+
101+
def _log_from_token(self, token):
102+
try:
103+
obj = jwt.decode(
104+
token,
105+
self.endpoint_id.cross_connect_secret_key,
106+
audience=str(self.id),
107+
options={"require": ["exp", "aud", "id"]},
108+
algorithms=["HS256"],
109+
)
110+
except jwt.PyJWTError as e:
111+
raise AccessDenied(_("Invalid Token")) from e
112+
113+
user = self.env["res.users"].browse(obj["id"])
114+
115+
if not user:
116+
raise AccessDenied(_("Invalid Token"))
117+
118+
return user, obj["redirect_url"]
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from secrets import token_urlsafe
5+
from typing import Annotated, Callable, Dict, List
6+
7+
from fastapi import APIRouter, Depends, HTTPException, status
8+
from fastapi.security import APIKeyHeader
9+
10+
from odoo import api, fields, models
11+
from odoo.api import Environment
12+
13+
from odoo.addons.fastapi.dependencies import fastapi_endpoint, odoo_env
14+
15+
from ..dependencies import authenticated_cross_connect_client
16+
from ..routers import cross_connect_router
17+
from .cross_connect_client import CrossConnectClient
18+
19+
20+
class FastapiEndpoint(models.Model):
21+
_inherit = "fastapi.endpoint"
22+
23+
app = fields.Selection(
24+
selection_add=[("cross_connect", "Cross Connect Endpoint")],
25+
ondelete={"cross_connect": "cascade"},
26+
)
27+
28+
cross_connect_client_ids = fields.One2many(
29+
"cross.connect.client",
30+
"endpoint_id",
31+
string="Cross Connect Clients",
32+
help="The clients that can access this endpoint.",
33+
)
34+
cross_connect_allowed_group_ids = fields.Many2many(
35+
"res.groups",
36+
string="Cross Connect Allowed Groups",
37+
help="The groups that can access the cross connect clients of this endpoint.",
38+
)
39+
cross_connect_secret_key = fields.Char(
40+
help="The secret key used for cross connection.",
41+
required=True,
42+
default=lambda self: self._generate_secret_key(),
43+
)
44+
45+
@api.model
46+
def _generate_secret_key(self):
47+
# generate random ~64 chars secret key
48+
return token_urlsafe(64)
49+
50+
def _get_fastapi_routers(self) -> List[APIRouter]:
51+
routers = super()._get_fastapi_routers()
52+
53+
if self.app == "cross_connect":
54+
routers += [cross_connect_router]
55+
56+
return routers
57+
58+
def _get_app_dependencies_overrides(self) -> Dict[Callable, Callable]:
59+
overrides = super()._get_app_dependencies_overrides()
60+
61+
if self.app == "cross_connect":
62+
overrides[
63+
authenticated_cross_connect_client
64+
] = api_key_based_authenticated_cross_connect_client
65+
66+
return overrides
67+
68+
def _get_routing_info(self):
69+
if self.app == "cross_connect":
70+
# Force to not save the HTTP session for the login to work correctly
71+
self.save_http_session = False
72+
return super()._get_routing_info()
73+
74+
@property
75+
def _server_env_fields(self):
76+
return {"cross_connect_secret_key": {}}
77+
78+
79+
def api_key_based_authenticated_cross_connect_client(
80+
api_key: Annotated[
81+
str,
82+
Depends(
83+
APIKeyHeader(
84+
name="api-key",
85+
description="Cross Connect Client API key.",
86+
)
87+
),
88+
],
89+
fastapi_endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
90+
env: Annotated[Environment, Depends(odoo_env)],
91+
) -> CrossConnectClient:
92+
cross_connect_client = (
93+
env["cross.connect.client"]
94+
.sudo()
95+
.search(
96+
[("api_key", "=", api_key), ("endpoint_id", "=", fastapi_endpoint.id)],
97+
limit=1,
98+
)
99+
)
100+
if not cross_connect_client:
101+
raise HTTPException(
102+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect API Key"
103+
)
104+
return cross_connect_client
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import fields, models
6+
7+
8+
class ResUsers(models.Model):
9+
_inherit = "res.users"
10+
11+
cross_connect_client_id = fields.Many2one(
12+
"cross.connect.client",
13+
string="Cross Connect Client",
14+
help="The cross connect client that created this user.",
15+
)
16+
cross_connect_client_user_id = fields.Integer(
17+
string="Cross Connect Client User ID",
18+
help="The user ID on the cross connect client.",
19+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Florian Mounier <florian.mounier@akretion.com>

0 commit comments

Comments
 (0)