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.
- odoo/addons/fastapi_captcha/README.rst +105 -0
- odoo/addons/fastapi_captcha/__init__.py +1 -0
- odoo/addons/fastapi_captcha/__manifest__.py +19 -0
- odoo/addons/fastapi_captcha/captcha_middleware.py +42 -0
- odoo/addons/fastapi_captcha/i18n/fastapi_captcha.pot +153 -0
- odoo/addons/fastapi_captcha/models/__init__.py +1 -0
- odoo/addons/fastapi_captcha/models/fastapi_endpoint.py +224 -0
- odoo/addons/fastapi_captcha/readme/CONTRIBUTORS.md +1 -0
- odoo/addons/fastapi_captcha/readme/DESCRIPTION.md +8 -0
- odoo/addons/fastapi_captcha/readme/USAGE.md +5 -0
- odoo/addons/fastapi_captcha/static/description/icon.png +0 -0
- odoo/addons/fastapi_captcha/static/description/index.html +447 -0
- odoo/addons/fastapi_captcha/tests/__init__.py +1 -0
- odoo/addons/fastapi_captcha/tests/test_fastapi_captcha.py +333 -0
- odoo/addons/fastapi_captcha/views/fastapi_endpoint_views.xml +29 -0
- odoo_addon_fastapi_captcha-18.0.1.0.0.2.dist-info/METADATA +122 -0
- odoo_addon_fastapi_captcha-18.0.1.0.0.2.dist-info/RECORD +19 -0
- odoo_addon_fastapi_captcha-18.0.1.0.0.2.dist-info/WHEEL +5 -0
- odoo_addon_fastapi_captcha-18.0.1.0.0.2.dist-info/top_level.txt +1 -0
|
@@ -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/)
|
|
Binary file
|