odoo-addon-fastapi-captcha 18.0.1.0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,105 @@
1
+ .. image:: https://odoo-community.org/readme-banner-image
2
+ :target: https://odoo-community.org/get-involved?utm_source=readme
3
+ :alt: Odoo Community Association
4
+
5
+ ===============
6
+ Fastapi Captcha
7
+ ===============
8
+
9
+ ..
10
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11
+ !! This file is generated by oca-gen-addon-readme !!
12
+ !! changes will be overwritten. !!
13
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14
+ !! source digest: sha256:9f521219b54944652afeaf18f64a122f01e1b6646171bc5212e48cb40d0906a0
15
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16
+
17
+ .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
18
+ :target: https://odoo-community.org/page/development-status
19
+ :alt: Beta
20
+ .. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
21
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
22
+ :alt: License: AGPL-3
23
+ .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Frest--framework-lightgray.png?logo=github
24
+ :target: https://github.com/OCA/rest-framework/tree/18.0/fastapi_captcha
25
+ :alt: OCA/rest-framework
26
+ .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27
+ :target: https://translation.odoo-community.org/projects/rest-framework-18-0/rest-framework-18-0-fastapi_captcha
28
+ :alt: Translate me on Weblate
29
+ .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/rest-framework&target_branch=18.0
31
+ :alt: Try me on Runboat
32
+
33
+ |badge1| |badge2| |badge3| |badge4| |badge5|
34
+
35
+ This module provides a simple way to protect several fastapi endpoints
36
+ routes with a captcha.
37
+
38
+ It currently supports the following captcha providers:
39
+
40
+ - `Google reCAPTCHA <https://www.google.com/recaptcha>`__
41
+ - `hCaptcha <https://www.hcaptcha.com/>`__
42
+ - `Altcha <https://altcha.org/>`__
43
+
44
+ **Table of contents**
45
+
46
+ .. contents::
47
+ :local:
48
+
49
+ Usage
50
+ =====
51
+
52
+ Check the ``Use Captcha`` checkbox in your FastAPI endpoint to enable
53
+ captcha validation, then enter your captcha provider, secret key and an
54
+ array of route url regex.
55
+
56
+ Every matching route will now require a valid captcha token in the
57
+ X-Captcha-Token header.
58
+
59
+ Bug Tracker
60
+ ===========
61
+
62
+ Bugs are tracked on `GitHub Issues <https://github.com/OCA/rest-framework/issues>`_.
63
+ In case of trouble, please check there if your issue has already been reported.
64
+ If you spotted it first, help us to smash it by providing a detailed and welcomed
65
+ `feedback <https://github.com/OCA/rest-framework/issues/new?body=module:%20fastapi_captcha%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
66
+
67
+ Do not contact contributors directly about support or help with technical issues.
68
+
69
+ Credits
70
+ =======
71
+
72
+ Authors
73
+ -------
74
+
75
+ * Akretion
76
+
77
+ Contributors
78
+ ------------
79
+
80
+ - Florian Mounier florian.mounier@akretion.com
81
+
82
+ Maintainers
83
+ -----------
84
+
85
+ This module is maintained by the OCA.
86
+
87
+ .. image:: https://odoo-community.org/logo.png
88
+ :alt: Odoo Community Association
89
+ :target: https://odoo-community.org
90
+
91
+ OCA, or the Odoo Community Association, is a nonprofit organization whose
92
+ mission is to support the collaborative development of Odoo features and
93
+ promote its widespread use.
94
+
95
+ .. |maintainer-paradoxxxzero| image:: https://github.com/paradoxxxzero.png?size=40px
96
+ :target: https://github.com/paradoxxxzero
97
+ :alt: paradoxxxzero
98
+
99
+ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
100
+
101
+ |maintainer-paradoxxxzero|
102
+
103
+ This module is part of the `OCA/rest-framework <https://github.com/OCA/rest-framework/tree/18.0/fastapi_captcha>`_ project on GitHub.
104
+
105
+ You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
@@ -0,0 +1 @@
1
+ from . import models
@@ -0,0 +1,19 @@
1
+ # Copyright 2025 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": "Fastapi Captcha",
7
+ "version": "18.0.1.0.0",
8
+ "author": "Akretion, Odoo Community Association (OCA)",
9
+ "summary": "Add a captcha to your FastAPI routes",
10
+ "category": "Tools",
11
+ "depends": ["fastapi"],
12
+ "website": "https://github.com/OCA/rest-framework",
13
+ "data": [
14
+ "views/fastapi_endpoint_views.xml",
15
+ ],
16
+ "maintainers": ["paradoxxxzero"],
17
+ "installable": True,
18
+ "license": "AGPL-3",
19
+ }
@@ -0,0 +1,42 @@
1
+ # Copyright 2025 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 starlette.middleware.base import BaseHTTPMiddleware
6
+
7
+ from odoo.exceptions import AccessError
8
+
9
+ from odoo.addons.fastapi.context import odoo_env_ctx
10
+
11
+
12
+ class CaptchaMiddleware(BaseHTTPMiddleware):
13
+ def __init__(self, app, endpoint_id, root_path, routes_regex=None):
14
+ super().__init__(app)
15
+ self.endpoint_id = endpoint_id
16
+ self.root_path = root_path
17
+ self.routes_regex = routes_regex
18
+
19
+ async def dispatch(self, request, call_next):
20
+ url = request.url.path.replace(self.root_path, "", 1)
21
+ if self.routes_regex and not any(
22
+ rex.fullmatch(url) for rex in self.routes_regex
23
+ ):
24
+ return await call_next(request)
25
+
26
+ env = odoo_env_ctx.get()
27
+ endpoint = env["fastapi.endpoint"].sudo().browse(self.endpoint_id)
28
+ token = request.headers.get("X-Captcha-Token")
29
+ if not token:
30
+ raise AccessError(
31
+ env._("Captcha token not found in headers"),
32
+ )
33
+ try:
34
+ endpoint.validate_captcha(token)
35
+ except AccessError as e:
36
+ raise e
37
+ except OSError as e:
38
+ raise AccessError(
39
+ env._("Captcha validation failed: %s") % str(e),
40
+ ) from e
41
+ response = await call_next(request)
42
+ return response
@@ -0,0 +1,153 @@
1
+ # Translation of Odoo Server.
2
+ # This file contains the translation of the following modules:
3
+ # * fastapi_captcha
4
+ #
5
+ msgid ""
6
+ msgstr ""
7
+ "Project-Id-Version: Odoo Server 18.0\n"
8
+ "Report-Msgid-Bugs-To: \n"
9
+ "Last-Translator: \n"
10
+ "Language-Team: \n"
11
+ "MIME-Version: 1.0\n"
12
+ "Content-Type: text/plain; charset=UTF-8\n"
13
+ "Content-Transfer-Encoding: \n"
14
+ "Plural-Forms: \n"
15
+
16
+ #. module: fastapi_captcha
17
+ #: model:ir.model.fields.selection,name:fastapi_captcha.selection__fastapi_endpoint__captcha_type__altcha
18
+ msgid "Altcha"
19
+ msgstr ""
20
+
21
+ #. module: fastapi_captcha
22
+ #. odoo-python
23
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
24
+ msgid "Altcha (%(url)s) validation failed: %(error)s"
25
+ msgstr ""
26
+
27
+ #. module: fastapi_captcha
28
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__captcha_custom_verify_url
29
+ msgid "Captcha Custom Verify Url"
30
+ msgstr ""
31
+
32
+ #. module: fastapi_captcha
33
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__captcha_minimum_score
34
+ msgid "Captcha Minimum Score"
35
+ msgstr ""
36
+
37
+ #. module: fastapi_captcha
38
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__captcha_routes_regex
39
+ msgid "Captcha Routes Regex"
40
+ msgstr ""
41
+
42
+ #. module: fastapi_captcha
43
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__captcha_secret_key
44
+ msgid "Captcha Secret Key"
45
+ msgstr ""
46
+
47
+ #. module: fastapi_captcha
48
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__captcha_type
49
+ msgid "Captcha Type"
50
+ msgstr ""
51
+
52
+ #. module: fastapi_captcha
53
+ #. odoo-python
54
+ #: code:addons/fastapi_captcha/captcha_middleware.py:0
55
+ msgid "Captcha token not found in headers"
56
+ msgstr ""
57
+
58
+ #. module: fastapi_captcha
59
+ #. odoo-python
60
+ #: code:addons/fastapi_captcha/captcha_middleware.py:0
61
+ msgid "Captcha validation failed: %s"
62
+ msgstr ""
63
+
64
+ #. module: fastapi_captcha
65
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__captcha_custom_verify_url
66
+ msgid "Custom URL to use for the captcha verification"
67
+ msgstr ""
68
+
69
+ #. module: fastapi_captcha
70
+ #: model:ir.model,name:fastapi_captcha.model_fastapi_endpoint
71
+ msgid "FastAPI Endpoint"
72
+ msgstr ""
73
+
74
+ #. module: fastapi_captcha
75
+ #: model:ir.model.fields.selection,name:fastapi_captcha.selection__fastapi_endpoint__captcha_type__hcaptcha
76
+ msgid "Hcaptcha"
77
+ msgstr ""
78
+
79
+ #. module: fastapi_captcha
80
+ #. odoo-python
81
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
82
+ msgid "Hcaptcha validation failed: %s"
83
+ msgstr ""
84
+
85
+ #. module: fastapi_captcha
86
+ #. odoo-python
87
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
88
+ msgid ""
89
+ "Hcaptcha validation failed: score %(score)s < %(min_score)s (%(reason)s)"
90
+ msgstr ""
91
+
92
+ #. module: fastapi_captcha
93
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__use_captcha
94
+ msgid "If checked, this endpoint will be protected by a captcha"
95
+ msgstr ""
96
+
97
+ #. module: fastapi_captcha
98
+ #. odoo-python
99
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
100
+ msgid "Invalid regex for captcha routes: %(regex)s (error: %(error)s)"
101
+ msgstr ""
102
+
103
+ #. module: fastapi_captcha
104
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__captcha_minimum_score
105
+ msgid ""
106
+ "Minimum score to accept the captcha if a score is provided by the captcha "
107
+ "service."
108
+ msgstr ""
109
+
110
+ #. module: fastapi_captcha
111
+ #. odoo-python
112
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
113
+ msgid "No secret key found for this endpoint"
114
+ msgstr ""
115
+
116
+ #. module: fastapi_captcha
117
+ #: model:ir.model.fields.selection,name:fastapi_captcha.selection__fastapi_endpoint__captcha_type__recaptcha
118
+ msgid "Recaptcha"
119
+ msgstr ""
120
+
121
+ #. module: fastapi_captcha
122
+ #. odoo-python
123
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
124
+ msgid "Recaptcha validation failed: %s"
125
+ msgstr ""
126
+
127
+ #. module: fastapi_captcha
128
+ #. odoo-python
129
+ #: code:addons/fastapi_captcha/models/fastapi_endpoint.py:0
130
+ msgid "Recaptcha validation failed: score %(score)s < %(min_score)s"
131
+ msgstr ""
132
+
133
+ #. module: fastapi_captcha
134
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__captcha_routes_regex
135
+ msgid ""
136
+ "Regexes to match against routes url that should be protected by this "
137
+ "captcha, comma separated. If empty, all routes will be protected"
138
+ msgstr ""
139
+
140
+ #. module: fastapi_captcha
141
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__captcha_secret_key
142
+ msgid "Secret key to use for the captcha validation"
143
+ msgstr ""
144
+
145
+ #. module: fastapi_captcha
146
+ #: model:ir.model.fields,help:fastapi_captcha.field_fastapi_endpoint__captcha_type
147
+ msgid "Type of captcha to use for this endpoint"
148
+ msgstr ""
149
+
150
+ #. module: fastapi_captcha
151
+ #: model:ir.model.fields,field_description:fastapi_captcha.field_fastapi_endpoint__use_captcha
152
+ msgid "Use Captcha"
153
+ msgstr ""
@@ -0,0 +1 @@
1
+ from . import fastapi_endpoint
@@ -0,0 +1,224 @@
1
+ # Copyright 2025 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
+ import re
6
+ from typing import Annotated
7
+
8
+ import requests
9
+ from starlette.middleware import Middleware
10
+
11
+ from odoo import api, fields, models
12
+ from odoo.exceptions import AccessError, UserError, ValidationError
13
+
14
+ from fastapi import Depends, Header
15
+
16
+ from ..captcha_middleware import CaptchaMiddleware
17
+
18
+
19
+ class FastapiEndpoint(models.Model):
20
+ _inherit = "fastapi.endpoint"
21
+
22
+ use_captcha = fields.Boolean(
23
+ help="If checked, this endpoint will be protected by a captcha",
24
+ )
25
+
26
+ captcha_type = fields.Selection(
27
+ [
28
+ ("recaptcha", "Recaptcha"),
29
+ ("hcaptcha", "Hcaptcha"),
30
+ ("altcha", "Altcha"),
31
+ ],
32
+ help="Type of captcha to use for this endpoint",
33
+ )
34
+
35
+ captcha_secret_key = fields.Char(
36
+ help="Secret key to use for the captcha validation",
37
+ groups="base.group_system",
38
+ )
39
+
40
+ captcha_routes_regex = fields.Char(
41
+ help="Regexes to match against routes url that should be protected "
42
+ "by this captcha, comma separated. If empty, all routes will be protected",
43
+ )
44
+
45
+ captcha_minimum_score = fields.Float(
46
+ default=0.5,
47
+ help="Minimum score to accept the captcha if a score is provided by the "
48
+ "captcha service.",
49
+ )
50
+
51
+ captcha_custom_verify_url = fields.Char(
52
+ help="Custom URL to use for the captcha verification",
53
+ )
54
+
55
+ @property
56
+ def _server_env_fields(self):
57
+ fields = getattr(super(), "_server_env_fields", None) or {}
58
+ fields["captcha_secret_key"] = {}
59
+ return fields
60
+
61
+ @api.constrains("captcha_routes_regex")
62
+ def _check_captcha_routes_regex(self):
63
+ """Check that the captcha routes regex is valid"""
64
+ for record in self:
65
+ if record.captcha_routes_regex:
66
+ for rex in record.captcha_routes_regex.split(","):
67
+ rex = rex.strip()
68
+ if not rex:
69
+ continue
70
+ # Check that the regex is valid
71
+ try:
72
+ re.compile(rex)
73
+ except re.error as e:
74
+ raise ValidationError(
75
+ self.env._(
76
+ "Invalid regex for captcha routes: %(regex)s "
77
+ "(error: %(error)s)"
78
+ )
79
+ % {
80
+ "regex": rex,
81
+ "error": str(e),
82
+ }
83
+ ) from e
84
+
85
+ def _get_fastapi_app_middlewares(self):
86
+ # Add the captcha middleware to the list of middlewares if enabled
87
+ middlewares = super()._get_fastapi_app_middlewares()
88
+ if self.use_captcha:
89
+ middlewares.append(
90
+ Middleware(
91
+ CaptchaMiddleware,
92
+ endpoint_id=self.id,
93
+ root_path=self.root_path,
94
+ routes_regex=[
95
+ re.compile(rex) for rex in self.captcha_routes_regex.split(",")
96
+ ]
97
+ if self.captcha_routes_regex
98
+ else None,
99
+ )
100
+ )
101
+ return middlewares
102
+
103
+ def _get_fastapi_app_dependencies(self):
104
+ # Add the captcha header to the list of dependencies
105
+ dependencies = super()._get_fastapi_app_dependencies()
106
+ if self.use_captcha:
107
+ dependencies.append(Depends(captcha_token))
108
+
109
+ return dependencies
110
+
111
+ def validate_captcha(self, captcha_response):
112
+ """Validate the captcha response."""
113
+ secret_key = self.captcha_secret_key
114
+ if not secret_key:
115
+ raise UserError(self.env._("No secret key found for this endpoint"))
116
+
117
+ if self.captcha_type == "recaptcha":
118
+ return self._validate_recaptcha(captcha_response, secret_key)
119
+ elif self.captcha_type == "hcaptcha":
120
+ return self._validate_hcaptcha(captcha_response, secret_key)
121
+ elif self.captcha_type == "altcha":
122
+ return self._validate_altcha(captcha_response, secret_key)
123
+
124
+ def _validate_recaptcha(self, captcha_response, secret_key):
125
+ """Validate the recaptcha response"""
126
+ data = {
127
+ "secret": secret_key,
128
+ "response": captcha_response,
129
+ }
130
+ response = requests.post(
131
+ "https://www.google.com/recaptcha/api/siteverify",
132
+ data=data,
133
+ timeout=10,
134
+ )
135
+ result = response.json()
136
+ success = result.get("success", False)
137
+ if not success:
138
+ error_codes = result.get("error-codes", ["?"])
139
+ raise AccessError(
140
+ self.env._("Recaptcha validation failed: %s") % ", ".join(error_codes)
141
+ )
142
+ score = result.get("score", 1)
143
+ if score < self.captcha_minimum_score:
144
+ raise AccessError(
145
+ self.env._(
146
+ "Recaptcha validation failed: score %(score)s < %(min_score)s"
147
+ )
148
+ % {
149
+ "score": score,
150
+ "min_score": self.captcha_minimum_score,
151
+ }
152
+ )
153
+
154
+ def _validate_hcaptcha(self, captcha_response, secret_key):
155
+ """Validate the hcaptcha response"""
156
+
157
+ data = {
158
+ "secret": secret_key,
159
+ "response": captcha_response,
160
+ }
161
+ response = requests.post(
162
+ "https://api.hcaptcha.com/siteverify", data=data, timeout=10
163
+ )
164
+ result = response.json()
165
+ success = result.get("success", False)
166
+ if not success:
167
+ error_codes = result.get("error-codes", ["?"])
168
+ raise AccessError(
169
+ self.env._("Hcaptcha validation failed: %s") % ", ".join(error_codes)
170
+ )
171
+ score = result.get("score", 1)
172
+ if score < self.captcha_minimum_score:
173
+ raise AccessError(
174
+ self.env._(
175
+ "Hcaptcha validation failed: score %(score)s < %(min_score)s "
176
+ "(%(reason)s)"
177
+ )
178
+ % {
179
+ "score": score,
180
+ "min_score": self.captcha_minimum_score,
181
+ "reason": result.get("score_reason", ""),
182
+ }
183
+ )
184
+
185
+ def _validate_altcha(self, captcha_response, secret_key):
186
+ """Validate the altcha response"""
187
+ data = {
188
+ "apiKey": secret_key,
189
+ "payload": captcha_response,
190
+ }
191
+ url = (
192
+ self.captcha_custom_verify_url
193
+ or "https://eu.altcha.org/api/v1/challenge/verify"
194
+ )
195
+ response = requests.post(url, data=data, timeout=10)
196
+ result = response.json()
197
+ success = result.get("verified", False)
198
+ if not success:
199
+ error = result.get("error", "?")
200
+ raise AccessError(
201
+ self.env._("Altcha (%(url)s) validation failed: %(error)s")
202
+ % {"url": url, "error": error}
203
+ )
204
+
205
+ @api.model
206
+ def _fastapi_app_fields(self):
207
+ # We need to reload fastapi app when we change these captcha fields
208
+ fields = super()._fastapi_app_fields()
209
+ return [
210
+ "use_captcha",
211
+ "captcha_routes_regex",
212
+ ] + fields
213
+
214
+
215
+ def captcha_token(
216
+ captcha_token: Annotated[
217
+ str | None,
218
+ Header(
219
+ alias="X-Captcha-Token",
220
+ description="The X-Captcha-Token header is used to specify the captcha ",
221
+ ),
222
+ ] = None,
223
+ ) -> str:
224
+ return captcha_token
@@ -0,0 +1 @@
1
+ - Florian Mounier <florian.mounier@akretion.com>
@@ -0,0 +1,8 @@
1
+ This module provides a simple way to protect several fastapi endpoints routes with a
2
+ captcha.
3
+
4
+ It currently supports the following captcha providers:
5
+
6
+ - [Google reCAPTCHA](https://www.google.com/recaptcha)
7
+ - [hCaptcha](https://www.hcaptcha.com/)
8
+ - [Altcha](https://altcha.org/)
@@ -0,0 +1,5 @@
1
+ Check the `Use Captcha` checkbox in your FastAPI endpoint to enable captcha validation,
2
+ then enter your captcha provider, secret key and an array of route url regex.
3
+
4
+ Every matching route will now require a valid captcha token in the X-Captcha-Token
5
+ header.