Skip to content

Commit 0b38fb1

Browse files
committed
[IMP] fastapi_captcha: Handle custom altcha url
1 parent 38c223d commit 0b38fb1

File tree

4 files changed

+169
-7
lines changed

4 files changed

+169
-7
lines changed

fastapi_captcha/captcha_middleware.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ async def dispatch(self, request, call_next):
3131
raise AccessError(
3232
_("Captcha token not found in headers"),
3333
)
34-
endpoint.validate_captcha(token)
34+
try:
35+
endpoint.validate_captcha(token)
36+
except AccessError as e:
37+
raise e
38+
except IOError as e:
39+
raise AccessError(
40+
_("Captcha validation failed: %s") % str(e),
41+
) from e
3542
response = await call_next(request)
3643
return response

fastapi_captcha/models/fastapi_endpoint.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class FastapiEndpoint(models.Model):
4848
"captcha service.",
4949
)
5050

51+
captcha_custom_verify_url = fields.Char(
52+
help="Custom URL to use for the captcha verification",
53+
)
54+
5155
@property
5256
def _server_env_fields(self):
5357
fields = getattr(super(), "_server_env_fields", None) or {}
@@ -180,16 +184,19 @@ def _validate_altcha(self, captcha_response, secret_key):
180184
"apiKey": secret_key,
181185
"payload": captcha_response,
182186
}
183-
response = requests.post(
184-
"https://eu.altcha.org/api/v1/challenge/verify",
185-
data=data,
186-
timeout=10,
187+
url = (
188+
self.captcha_custom_verify_url
189+
or "https://eu.altcha.org/api/v1/challenge/verify"
187190
)
191+
response = requests.post(url, data=data, timeout=10)
188192
result = response.json()
189193
success = result.get("verified", False)
190194
if not success:
191195
error = result.get("error", "?")
192-
raise AccessError(_("Altcha validation failed: %s") % error)
196+
raise AccessError(
197+
_("Altcha (%(url)s) validation failed: %(error)s")
198+
% {"url": url, "error": error}
199+
)
193200

194201
@api.model
195202
def _fastapi_app_fields(self):

fastapi_captcha/tests/test_fastapi_captcha.py

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import requests
77

8-
from odoo.exceptions import AccessError
8+
from odoo.exceptions import AccessError, UserError, ValidationError
99

1010
from odoo.addons.fastapi.tests.common import FastAPITransactionCase
1111

@@ -26,6 +26,22 @@ def setUpClass(cls):
2626
cls.endpoint.captcha_secret_key = "test_secret"
2727
cls.default_fastapi_app = cls.endpoint._get_app()
2828

29+
def test_no_secret_key(self):
30+
self.endpoint.captcha_secret_key = False
31+
with self._create_test_client() as test_client:
32+
with self.assertRaisesRegex(
33+
UserError,
34+
"No secret key found for this endpoint",
35+
):
36+
test_client.get("/demo/", headers={"X-Captcha-Token": "valid"})
37+
38+
def test_invalid_regex(self):
39+
with self.assertRaisesRegex(
40+
ValidationError,
41+
r"Invalid regex for captcha routes: /route/\( ",
42+
):
43+
self.endpoint.captcha_routes_regex = r"/route/("
44+
2945
def test_missing_header(self):
3046
with self._create_test_client() as test_client:
3147
with self.assertRaisesRegex(
@@ -95,6 +111,7 @@ def test_valid_header_recaptcha(self):
95111

96112
def test_invalid_header_hcaptcha(self):
97113
self.endpoint.captcha_type = "hcaptcha"
114+
self.endpoint.captcha_minimum_score = 0.8
98115
with patch(
99116
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
100117
return_value=requests.Response(),
@@ -120,6 +137,7 @@ def test_invalid_header_hcaptcha(self):
120137

121138
def test_valid_header_hcaptcha(self):
122139
self.endpoint.captcha_type = "hcaptcha"
140+
self.endpoint.captcha_minimum_score = 0.8
123141
with patch(
124142
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
125143
return_value=requests.Response(),
@@ -149,6 +167,132 @@ def test_valid_header_hcaptcha(self):
149167
},
150168
)
151169

170+
def test_valid_header_low_score_hcaptcha(self):
171+
self.endpoint.captcha_type = "hcaptcha"
172+
self.endpoint.captcha_minimum_score = 0.8
173+
with patch(
174+
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
175+
return_value=requests.Response(),
176+
) as mock_post:
177+
mock_post.return_value.status_code = 200
178+
mock_post.return_value.json = lambda: {
179+
"success": True,
180+
"score": 0.6,
181+
"score_reason": "low-confidence",
182+
}
183+
with self._create_test_client() as test_client:
184+
with self.assertRaisesRegex(
185+
AccessError,
186+
r"Hcaptcha validation failed: score 0.6 < 0.8 \(low-confidence\)",
187+
):
188+
test_client.get("/demo/", headers={"X-Captcha-Token": "valid"})
189+
190+
def test_invalid_header_altcha(self):
191+
self.endpoint.captcha_type = "altcha"
192+
with patch(
193+
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
194+
return_value=requests.Response(),
195+
) as mock_post:
196+
mock_post.return_value.status_code = 200
197+
mock_post.return_value.json = lambda: {
198+
"verified": False,
199+
"error": "invalid-input-response",
200+
}
201+
with self._create_test_client() as test_client:
202+
with self.assertRaisesRegex(
203+
AccessError,
204+
r"Altcha \(https://eu.altcha.org/api/v1/challenge/verify\) "
205+
"validation failed: invalid-input-response",
206+
):
207+
test_client.get("/demo/", headers={"X-Captcha-Token": "invalid"})
208+
209+
self.assertGreaterEqual(mock_post.call_count, 1)
210+
self.assertEqual(
211+
mock_post.call_args.args[0],
212+
"https://eu.altcha.org/api/v1/challenge/verify",
213+
)
214+
215+
with self.assertRaisesRegex(
216+
AccessError,
217+
r"Altcha \(https://eu.altcha.org/api/v1/challenge/verify\) "
218+
"validation failed: invalid-input-response",
219+
):
220+
test_client.get(
221+
"/demo/who_ami", headers={"X-Captcha-Token": "invalid"}
222+
)
223+
224+
def test_valid_header_altcha(self):
225+
self.endpoint.captcha_type = "altcha"
226+
with patch(
227+
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
228+
return_value=requests.Response(),
229+
) as mock_post:
230+
mock_post.return_value.status_code = 200
231+
mock_post.return_value.json = lambda: {
232+
"verified": True,
233+
}
234+
with self._create_test_client() as test_client:
235+
response = test_client.get(
236+
"/demo/", headers={"X-Captcha-Token": "valid"}
237+
)
238+
self.assertEqual(response.status_code, status.HTTP_200_OK)
239+
self.assertEqual(response.json(), {"Hello": "World"})
240+
241+
self.assertGreaterEqual(mock_post.call_count, 1)
242+
self.assertEqual(
243+
mock_post.call_args.args[0],
244+
"https://eu.altcha.org/api/v1/challenge/verify",
245+
)
246+
response = test_client.get(
247+
"/demo/who_ami", headers={"X-Captcha-Token": "valid"}
248+
)
249+
self.assertEqual(response.status_code, status.HTTP_200_OK)
250+
partner = self.default_fastapi_authenticated_partner
251+
self.assertDictEqual(
252+
response.json(),
253+
{
254+
"name": partner.name,
255+
"display_name": partner.display_name,
256+
},
257+
)
258+
259+
def test_valid_header_custom_url_altcha(self):
260+
self.endpoint.captcha_type = "altcha"
261+
self.endpoint.captcha_custom_verify_url = "https://custom.exemple.org/verify"
262+
263+
with patch(
264+
"odoo.addons.fastapi_captcha.models.fastapi_endpoint.requests.post",
265+
return_value=requests.Response(),
266+
) as mock_post:
267+
mock_post.return_value.status_code = 200
268+
mock_post.return_value.json = lambda: {
269+
"verified": True,
270+
}
271+
with self._create_test_client() as test_client:
272+
response = test_client.get(
273+
"/demo/", headers={"X-Captcha-Token": "valid"}
274+
)
275+
self.assertEqual(response.status_code, status.HTTP_200_OK)
276+
self.assertEqual(response.json(), {"Hello": "World"})
277+
278+
self.assertGreaterEqual(mock_post.call_count, 1)
279+
self.assertEqual(
280+
mock_post.call_args.args[0],
281+
"https://custom.exemple.org/verify",
282+
)
283+
response = test_client.get(
284+
"/demo/who_ami", headers={"X-Captcha-Token": "valid"}
285+
)
286+
self.assertEqual(response.status_code, status.HTTP_200_OK)
287+
partner = self.default_fastapi_authenticated_partner
288+
self.assertDictEqual(
289+
response.json(),
290+
{
291+
"name": partner.name,
292+
"display_name": partner.display_name,
293+
},
294+
)
295+
152296
def test_routes_matching_1(self):
153297
self.endpoint.captcha_routes_regex = "/demo/wh.*,/demo/ca.?"
154298
# Refresh app

fastapi_captcha/views/fastapi_endpoint_views.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
attrs="{'required': [('use_captcha', '=', True)]}"
2929
/>
3030
<field name="captcha_routes_regex" />
31+
<field
32+
name="captcha_custom_verify_url"
33+
attrs="{'invisible': [('captcha_type', '!=', 'altcha')]}"
34+
/>
3135
<field name="captcha_minimum_score" />
3236
</group>
3337
</group>

0 commit comments

Comments
 (0)