This repository has been archived by the owner on Dec 30, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 12
/
ingestion.py
195 lines (176 loc) · 8.07 KB
/
ingestion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Copyright (C) 2020 Presidenza del Consiglio dei Ministri.
# Please refer to the AUTHORS file for more information.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import secrets
from http import HTTPStatus
from typing import List
from marshmallow import fields
from marshmallow.validate import Regexp
from sanic import Blueprint
from sanic.request import Request
from sanic.response import HTTPResponse
from sanic_openapi import doc
from immuni_common.core.exceptions import SchemaValidationException, UnauthorizedOtpException
from immuni_common.helpers.cache import cache
from immuni_common.helpers.sanic import validate
from immuni_common.helpers.swagger import doc_exception
from immuni_common.models.dataclasses import ExposureDetectionSummary
from immuni_common.models.enums import Location
from immuni_common.models.marshmallow.fields import IntegerBoolField, Province
from immuni_common.models.marshmallow.schemas import (
ExposureDetectionSummarySchema,
TemporaryExposureKeySchema,
)
from immuni_common.models.marshmallow.validators import TekListValidator
from immuni_common.models.mongoengine.temporary_exposure_key import TemporaryExposureKey
from immuni_common.models.swagger import HeaderImmuniContentTypeJson
from immuni_exposure_ingestion.core import config
from immuni_exposure_ingestion.helpers.exposure_data import store_exposure_detection_summaries
from immuni_exposure_ingestion.helpers.otp import validate_otp_token
from immuni_exposure_ingestion.helpers.upload import (
slow_down_request,
validate_token_format,
wait_configured_time,
)
from immuni_exposure_ingestion.models.swagger import (
CheckOtp,
HeaderImmuniAuthorizationOtpSha,
HeaderImmuniClientClock,
HeaderImmuniDummyData,
)
from immuni_exposure_ingestion.models.swagger import Upload as UploadDoc
from immuni_exposure_ingestion.models.upload import Upload
_LOGGER = logging.getLogger(__name__)
bp = Blueprint("ingestion", url_prefix="ingestion")
@bp.route("/upload", version=1, methods=["POST"])
@doc.summary("Upload TEKs (caller: Mobile Client).")
@doc.description(
"Once it has validated the OTP, the Mobile Client uploads its TEKs for the past 14 days, "
"together with the user’s Province of Domicile. "
"If any Epidemiological Infos from the previous 14 days are available, the Mobile Client "
"uploads those too. "
"The timestamp that accompanies each TEK is referred to the Mobile Client’s system time. "
"For this reason, the Mobile Client informs the Exposure Ingestion Service about its system "
"time so that a skew can be corrected. "
"Using the dedicated request header, the Mobile Client can indicate to the server that the "
"call it is making is a dummy one. "
"The server will ignore the content of such calls."
)
@doc.consumes(
UploadDoc, location="body", required=True, content_type="application/json; charset=utf-8"
)
@doc.consumes(HeaderImmuniDummyData(), location="header", required=True)
@doc.consumes(HeaderImmuniClientClock(), location="header", required=True)
@doc.consumes(HeaderImmuniContentTypeJson(), location="header", required=True)
@doc.consumes(HeaderImmuniAuthorizationOtpSha(), location="header", required=True)
@doc_exception(SchemaValidationException)
@doc.response(
HTTPStatus.NO_CONTENT.value, None, description="Upload completed successfully.",
)
@validate(
location=Location.HEADERS,
client_clock=fields.Integer(required=True, data_key=HeaderImmuniClientClock.DATA_KEY),
is_dummy=IntegerBoolField(
required=True, allow_strings=True, data_key=HeaderImmuniDummyData.DATA_KEY,
),
)
@validate(
location=Location.JSON,
province=Province(),
teks=fields.Nested(
TemporaryExposureKeySchema, required=True, many=True, validate=TekListValidator()
),
exposure_detection_summaries=fields.Nested(
ExposureDetectionSummarySchema, required=True, many=True,
),
padding=fields.String(validate=Regexp(r"^[a-zA-Z0-9]*$")),
)
@validate_token_format
@cache(no_store=True)
async def upload( # pylint: disable=too-many-arguments
request: Request,
province: str,
teks: List[TemporaryExposureKey],
exposure_detection_summaries: List[ExposureDetectionSummary],
client_clock: int,
padding: str,
is_dummy: bool,
) -> HTTPResponse:
"""
Allow Mobile Clients to upload their Temporary Exposure Keys.
:param request: the HTTP request object.
:param province: the user's Province of Domicile.
:param teks: the list of TEKs.
:param exposure_detection_summaries: the Epidemiological Info of the last 14 days, if any.
:param client_clock: the clock on client's side, validated, but ignored.
:param padding: the dummy data sent to protect against analysis of the traffic size.
:param is_dummy: whether the uploaded data is dummy or not.
:return: 204 on successful upload, 400 on SchemaValidationException.
"""
if is_dummy:
await wait_configured_time() # Simulate the time of a real request
return HTTPResponse(status=HTTPStatus.NO_CONTENT)
upload_model = Upload(keys=teks)
otp = await validate_otp_token(request.token, delete=True)
upload_model.symptoms_started_on = otp.symptoms_started_on
upload_model.save()
_LOGGER.info("Created new upload.", extra=dict(n_teks=len(teks)))
await store_exposure_detection_summaries(exposure_detection_summaries, province=province)
return HTTPResponse(status=HTTPStatus.NO_CONTENT)
@bp.route("/check-otp", version=1, methods=["POST"])
@doc.summary("Check OTP (caller: Mobile Client).")
@doc.description(
"The Mobile Client validates the OTP prior to uploading data. "
"The request is authenticated with the OTP to be validated. "
"Using the dedicated request header, the Mobile Client can indicate to the server that the "
"call it is making is a dummy one. "
"The server will ignore the content of such calls."
)
@doc.consumes(
CheckOtp, location="body", required=True, content_type="application/json; charset=utf-8"
)
@doc.consumes(HeaderImmuniDummyData(), location="header", required=True)
@doc.consumes(HeaderImmuniContentTypeJson(), location="header", required=True)
@doc.consumes(HeaderImmuniAuthorizationOtpSha(), location="header", required=True)
@doc_exception(SchemaValidationException)
@doc_exception(UnauthorizedOtpException)
@doc.response(
HTTPStatus.NO_CONTENT.value, None, description="Operation completed successfully.",
)
@validate(
location=Location.HEADERS,
is_dummy=IntegerBoolField(
required=True, allow_strings=True, data_key=HeaderImmuniDummyData.DATA_KEY,
),
)
@validate(
location=Location.JSON,
padding=fields.String(required=True, validate=Regexp(r"^[a-zA-Z0-9]*$")),
)
@validate_token_format
@slow_down_request
@cache(no_store=True)
async def check_otp(request: Request, is_dummy: bool, padding: str) -> HTTPResponse:
"""
Check the OTP validity, aka successfully enabled by the OTP Service.
:param request: the HTTP request object.
:param is_dummy: whether the uploaded data is dummy or not.
:param padding: the dummy data sent to protect against analysis of the traffic size.
:return: 204 if the OTP is valid, 400 on SchemaValidationException, 401 on unauthorised OTP.
"""
if is_dummy:
if secrets.randbelow(100) < config.DUMMY_DATA_TOKEN_ERROR_CHANCE_PERCENT:
raise UnauthorizedOtpException()
return HTTPResponse(status=HTTPStatus.NO_CONTENT)
await validate_otp_token(request.token)
return HTTPResponse(status=HTTPStatus.NO_CONTENT)