Skip to content

Commit 97862a0

Browse files
authored
fix(submission): fix anonymous submission failures from KoboCollect DEV-1347 (#6499)
### 📣 Summary Ensure `KoboCollect` can submit anonymously by returning 204 for `HEAD /<username>/submission` before applying the [empty-request authentication check](https://github.com/kobotoolbox/kpi/blob/b4b82b809c205dfb0a23eba7633e57f50ac35c6f/kobo/apps/openrosa/apps/api/viewsets/xform_submission_api.py#L253-L257), while keeping Digest authentication behavior intact. ### 📖 Description After adding a guard to return `401` for unauthenticated empty-body POST requests (required for Digest auth handshake), KoboCollect began failing anonymous submissions. KoboCollect performs an initial HEAD request to `/<username>/submission` to probe server availability. Because the guard ran before the HEAD handler, this probe was incorrectly treated as unauthenticated, returning 401. KoboCollect interprets a 401 on HEAD as "credentials required" and therefore never sends the actual XML payload, showing the login screen instead. Enketo continued to work because it tolerates a 401 on the first probe, but KoboCollect does not.
1 parent 7fd2421 commit 97862a0

File tree

2 files changed

+39
-6
lines changed

2 files changed

+39
-6
lines changed

kobo/apps/openrosa/apps/api/tests/viewsets/test_xform_submission_api.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,39 @@ def test_digest_auth_allows_submission_on_username_endpoint(self):
955955
response, 'Successful submission', status_code=status.HTTP_201_CREATED
956956
)
957957

958+
def test_head_and_post_behavior_for_username_endpoint(self):
959+
"""
960+
Test that HEAD requests to `/<username>/submission` return 204 and that
961+
anonymous POSTs work when the xform does not require auth.
962+
963+
KoboCollect performs an initial HEAD probe to confirm the endpoint is
964+
reachable. This HEAD request must return 204. After that, an anonymous
965+
POST containing the XML payload should succeed with a 201 response.
966+
"""
967+
username = self.user.username
968+
self.xform.require_auth = False
969+
self.xform.save(update_fields=['require_auth'])
970+
971+
# HEAD request to `/<username>/submission` must return 204
972+
head_req = self.factory.head(f'/{username}/submission')
973+
head_resp = self.view(head_req, username=username)
974+
self.assertEqual(head_resp.status_code, status.HTTP_204_NO_CONTENT)
975+
976+
# Anonymous POST with actual xml payload must succeed
977+
s = self.surveys[0]
978+
submission_path = os.path.join(
979+
self.main_directory, 'fixtures',
980+
'transportation', 'instances', s, s + '.xml'
981+
)
982+
with open(submission_path, 'rb') as sf:
983+
data = {'xml_submission_file': sf}
984+
request = self.factory.post(f'/{username}/submission', data)
985+
request.user = AnonymousUser()
986+
response = self.view(request, username=username)
987+
self.assertContains(
988+
response, 'Successful submission', status_code=201
989+
)
990+
958991

959992
class ConcurrentSubmissionTestCase(RequestMixin, LiveServerTestCase):
960993
"""

kobo/apps/openrosa/apps/api/viewsets/xform_submission_api.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,19 +250,19 @@ def create(self, request, *args, **kwargs):
250250
user = get_database_user(request.user)
251251
username = user.username
252252

253-
# Return 401 if no authentication provided and there are no files,
254-
# for digest authentication to work properly
255-
has_auth = bool(get_authorization_header(request))
256-
if not has_auth and not (bool(request.FILES) or bool(request.data)):
257-
raise NotAuthenticated
258-
259253
if request.method.upper() == 'HEAD':
260254
return Response(
261255
status=status.HTTP_204_NO_CONTENT,
262256
headers=self.get_openrosa_headers(request),
263257
template_name=self.template_name,
264258
)
265259

260+
# Return 401 if no authentication provided and there are no files,
261+
# for digest authentication to work properly
262+
has_auth = bool(get_authorization_header(request))
263+
if not has_auth and not (bool(request.FILES) or bool(request.data)):
264+
raise NotAuthenticated
265+
266266
is_json_request = is_json(request)
267267

268268
create_instance_func = (

0 commit comments

Comments
 (0)