Skip to content

Commit dba2b88

Browse files
authored
Enable sudo users to overwrite client ownership (#448)
* Prevent JS development files being sent to docker * Make error handling more robust * Extract auth interface * Treat user as a required argument * Move scope validation into connexion * Enable sudo team members to override ownership
1 parent 4468eda commit dba2b88

File tree

18 files changed

+303
-247
lines changed

18 files changed

+303
-247
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ opwen_email_client/webapp/static/fonts/
1212
opwen_email_client/webapp/static/js/*.min.js
1313
!opwen_email_server/
1414
!opwen_statuspage/
15+
opwen_statuspage/build/
16+
opwen_statuspage/node_modules/
1517
!tests/
1618
!makefile
1719
!requirements*.txt

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ before_script: |
1818
export DOCKER_TAG="$TRAVIS_TAG"
1919
2020
if [[ "$TEST_MODE" = "live" ]]; then
21-
export REGISTRATION_CREDENTIALS="$GITHUB_AUTH_USERNAME:$GITHUB_AUTH_TOKEN"
21+
export REGISTRATION_CREDENTIALS="$GITHUB_AUTH_TOKEN"
2222
export LOKOLE_QUEUE_BROKER_SCHEME="azureservicebus"
2323
export LOKOLE_RESOURCE_SUFFIX="$SUFFIX"
2424
export APPINSIGHTS_INSTRUMENTATIONKEY="$SUFFIX"

docker/integtest/1-register-client.sh

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ wait_for_client() {
1515
local i
1616

1717
for i in $(seq 1 "${max_retries}"); do
18-
if curl -fs -u "${REGISTRATION_CREDENTIALS}" "http://nginx:8888/api/email/register/developer${client}.lokole.ca" | tee "${out_dir}/register${client}.json"; then
18+
if authenticated_request "http://nginx:8888/api/email/register/developer${client}.lokole.ca" | tee "${out_dir}/register${client}.json"; then
1919
log "Client ${client} is registered"
2020
return
2121
fi
@@ -28,48 +28,48 @@ wait_for_client() {
2828

2929
# workflow 3: register a new client called "developer"
3030
# normally this endpoint would be called during a new lokole device setup
31-
curl -fs \
31+
authenticated_request \
32+
"http://nginx:8888/api/email/register/" \
3233
-H "Content-Type: application/json" \
33-
-u "${REGISTRATION_CREDENTIALS}" \
34-
-d '{"domain":"developer1.lokole.ca"}' \
35-
"http://nginx:8888/api/email/register/"
34+
-d '{"domain":"developer1.lokole.ca"}'
3635

3736
wait_for_client 1
3837

3938
# registering a client with bad credentials should fail
40-
if curl -fs \
39+
if REGISTRATION_CREDENTIALS="baduser:badpassword" authenticated_request \
40+
"http://nginx:8888/api/email/register/" \
4141
-H "Content-Type: application/json" \
42-
-u "baduser:badpassword" \
4342
-d '{"domain":"hacker.lokole.ca"}' \
43+
; then echo "Was able to register a client with bad basic auth credentials" >&2; exit 4; fi
44+
45+
if REGISTRATION_CREDENTIALS="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" authenticated_request \
4446
"http://nginx:8888/api/email/register/" \
45-
; then echo "Was able to register a client with bad credentials" >&2; exit 4; fi
47+
-H "Content-Type: application/json" \
48+
-d '{"domain":"hacker.lokole.ca"}' \
49+
; then echo "Was able to register a client with bad bearer credentials" >&2; exit 4; fi
4650

4751
# also register another client to simulate multi-client emails
48-
curl -fs \
52+
authenticated_request \
53+
"http://nginx:8888/api/email/register/" \
4954
-H "Content-Type: application/json" \
50-
-u "${REGISTRATION_CREDENTIALS}" \
51-
-d '{"domain":"developer2.lokole.ca"}' \
52-
"http://nginx:8888/api/email/register/"
55+
-d '{"domain":"developer2.lokole.ca"}'
5356

5457
wait_for_client 2
5558

5659
# after creating a client, creating the same one again should fail but we should be able to delete it
57-
curl -fs \
60+
authenticated_request \
61+
"http://nginx:8888/api/email/register/" \
5862
-H "Content-Type: application/json" \
59-
-u "${REGISTRATION_CREDENTIALS}" \
60-
-d '{"domain":"developer3.lokole.ca"}' \
61-
"http://nginx:8888/api/email/register/"
63+
-d '{"domain":"developer3.lokole.ca"}'
6264

6365
wait_for_client 3
6466

65-
if curl -fs \
67+
if authenticated_request \
68+
"http://nginx:8888/api/email/register/" \
6669
-H "Content-Type: application/json" \
67-
-u "${REGISTRATION_CREDENTIALS}" \
6870
-d '{"domain":"developer3.lokole.ca"}' \
69-
"http://nginx:8888/api/email/register/" \
7071
; then echo "Was able to register a duplicate client" >&2; exit 5; fi
7172

72-
curl -fs \
73-
-u "${REGISTRATION_CREDENTIALS}" \
74-
-X DELETE \
75-
"http://nginx:8888/api/email/register/developer3.lokole.ca"
73+
authenticated_request \
74+
"http://nginx:8888/api/email/register/developer3.lokole.ca" \
75+
-X DELETE

docker/integtest/5-assert-on-results.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if [[ "${num_exceptions}" -ne "${num_exceptions_expected}" ]]; then
1919
exit 2
2020
fi
2121

22-
num_users="$(curl -fs 'http://nginx:8888/api/email/metrics/users/developer1.lokole.ca' -u "${REGISTRATION_CREDENTIALS}" | jq -r '.users')"
22+
num_users="$(authenticated_request 'http://nginx:8888/api/email/metrics/users/developer1.lokole.ca' | jq -r '.users')"
2323
num_users_expected=1
2424

2525
if [[ "${num_users}" -ne "${num_users_expected}" ]]; then

docker/integtest/utils.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ log() {
99
echo "$@" >&2
1010
}
1111

12+
authenticated_request() {
13+
local endpoint="$1"; shift
14+
15+
if [[ "${REGISTRATION_CREDENTIALS}" =~ ^[^:]+:.*$ ]]; then
16+
curl -fs "${endpoint}" -u "${REGISTRATION_CREDENTIALS}" "$@"
17+
else
18+
curl -fs "${endpoint}" -H "Authorization: Bearer ${REGISTRATION_CREDENTIALS}" "$@"
19+
fi
20+
}
21+
1222
az_connection_string() {
1323
if [[ -z "${AZURITE_HOST}" ]]; then
1424
echo "AccountName=${AZURITE_ACCOUNT};AccountKey=${AZURITE_KEY};"

install.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#!/usr/bin/env python3
22
"""Script to set up a Debian Linux based system as a Lokole client."""
33
from argparse import ArgumentParser
4-
from base64 import b64encode
54
from json import dumps
65
from json import loads
76
from logging import StreamHandler
@@ -421,13 +420,11 @@ def is_enabled(self):
421420

422421
class ClientSetup(Setup):
423422
def _run(self):
424-
request_auth = b64encode(self.args.registration_credentials.encode('ascii')).decode('ascii')
425-
426423
create_request_payload = dumps({'domain': self.client_domain}).encode('utf-8')
427424
create_request = Request(self.client_url_create)
428425
create_request.add_header('Content-Type', 'application/json; charset=utf-8')
429426
create_request.add_header('Content-Length', str(len(create_request_payload)))
430-
create_request.add_header('Authorization', 'Basic {}'.format(request_auth))
427+
create_request.add_header('Authorization', 'Bearer {}'.format(self.args.registration_credentials))
431428

432429
try:
433430
with urlopen(create_request, create_request_payload): # nosec
@@ -440,7 +437,7 @@ def _run(self):
440437

441438
while True:
442439
get_request = Request(self.client_url_details)
443-
get_request.add_header('Authorization', 'Basic {}'.format(request_auth))
440+
get_request.add_header('Authorization', 'Bearer {}'.format(self.args.registration_credentials))
444441
try:
445442
with urlopen(get_request) as response: # nosec
446443
response_body = response.read().decode('utf-8')
@@ -476,6 +473,9 @@ def client_url_details(self):
476473

477474
@property
478475
def is_enabled(self):
476+
if ':' in self.args.registration_credentials:
477+
self.abort('Registration credential should be set to Github access token, not username and password')
478+
479479
return self.args.sim_type != 'LocalOnly'
480480

481481

@@ -826,8 +826,7 @@ def cli():
826826
'Example: "34 * * * *" for once per hour at the 34th minute.'
827827
))
828828
parser.add_argument('registration_credentials', nargs='?', help=(
829-
'Username and password (separated by a colon ":") for '
830-
'registering with the Lokole server.'
829+
'Github access token for registering with the Lokole server.'
831830
))
832831
parser.add_argument('--app_root', default=getenv('OPWEN_APP_ROOT', ''), help=(
833832
'The URL prefix at which the app will be accessible.'

opwen_email_server/actions.py

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
from opwen_email_server.constants import events
1313
from opwen_email_server.constants import mailbox
1414
from opwen_email_server.constants import sync
15-
from opwen_email_server.services.auth import AzureAuth
16-
from opwen_email_server.services.auth import NoAuth
15+
from opwen_email_server.services.auth import Auth
1716
from opwen_email_server.services.sendgrid import SendSendgridEmail
1817
from opwen_email_server.services.storage import AzureObjectsStorage
1918
from opwen_email_server.services.storage import AzureObjectStorage
@@ -214,9 +213,7 @@ def _decode_attachments(cls, email: dict) -> dict:
214213

215214

216215
class ReceiveInboundEmail(_Action):
217-
def __init__(self, auth: Union[AzureAuth, NoAuth], raw_email_storage: AzureTextStorage, next_task: Callable[[str],
218-
None]):
219-
216+
def __init__(self, auth: Auth, raw_email_storage: AzureTextStorage, next_task: Callable[[str], None]):
220217
self._auth = auth
221218
self._raw_email_storage = raw_email_storage
222219
self._next_task = next_task
@@ -289,7 +286,7 @@ def _action(self, resource_id): # type: ignore
289286

290287

291288
class DownloadClientEmails(_Action):
292-
def __init__(self, auth: AzureAuth, client_storage: AzureObjectsStorage, email_storage: AzureObjectStorage,
289+
def __init__(self, auth: Auth, client_storage: AzureObjectsStorage, email_storage: AzureObjectStorage,
293290
pending_storage: AzureTextStorage):
294291

295292
self._auth = auth
@@ -347,8 +344,7 @@ def _mark_emails_as_delivered(self, domain: str, email_ids: Iterable[str]) -> No
347344

348345

349346
class UploadClientEmails(_Action):
350-
def __init__(self, auth: AzureAuth, next_task: Callable[[str], None]):
351-
347+
def __init__(self, auth: Auth, next_task: Callable[[str], None]):
352348
self._auth = auth
353349
self._next_task = next_task
354350

@@ -367,9 +363,8 @@ def _action(self, client_id, upload_info): # type: ignore
367363

368364

369365
class RegisterClient(_Action):
370-
def __init__(self, auth: AzureAuth, client_storage: AzureObjectsStorage, setup_mailbox: Callable[[str, str], None],
366+
def __init__(self, auth: Auth, client_storage: AzureObjectsStorage, setup_mailbox: Callable[[str, str], None],
371367
setup_mx_records: Callable[[str], None], client_id_source: Callable[[], str]):
372-
373368
self._auth = auth
374369
self._client_storage = client_storage
375370
self._setup_mailbox = setup_mailbox
@@ -389,25 +384,25 @@ def _action(self, domain, owner): # type: ignore
389384

390385

391386
class CreateClient(_Action):
392-
def __init__(self, auth: AzureAuth, task: Callable[[str, str], None]):
387+
def __init__(self, auth: Auth, task: Callable[[str, str], None]):
393388
self._auth = auth
394389
self._task = task
395390

396-
def _action(self, client, **auth_args): # type: ignore
391+
def _action(self, client, user, **auth_args): # type: ignore
397392
domain = client['domain']
398393
if not is_lowercase(domain):
399394
return 'domain must be lowercase', 400
400395
if self._auth.client_id_for(domain) is not None:
401396
return 'client already exists', 409
402397

403-
self._task(domain, auth_args.get('user'))
398+
self._task(domain, user)
404399

405400
self.log_event(events.CLIENT_CREATED, {'domain': domain}) # noqa: E501 # yapf: disable
406401
return 'accepted', 201
407402

408403

409404
class ListClients(_Action):
410-
def __init__(self, auth: AzureAuth):
405+
def __init__(self, auth: Auth):
411406
self._auth = auth
412407

413408
def _action(self, **auth_args): # type: ignore
@@ -420,19 +415,19 @@ def _action(self, **auth_args): # type: ignore
420415

421416

422417
class GetClient(_Action):
423-
def __init__(self, auth: AzureAuth, client_storage: AzureObjectsStorage):
418+
def __init__(self, auth: Auth, client_storage: AzureObjectsStorage):
424419
self._auth = auth
425420
self._client_storage = client_storage
426421

427-
def _action(self, domain, **auth_args): # type: ignore
422+
def _action(self, domain, user, **auth_args): # type: ignore
428423
if not is_lowercase(domain):
429424
return 'domain must be lowercase', 400
430425

431426
client_id = self._auth.client_id_for(domain)
432427
if client_id is None:
433428
return 'client does not exist', 404
434429

435-
if not self._auth.is_owner(domain, auth_args.get('user')):
430+
if not self._auth.is_owner(domain, user):
436431
return 'client does not belong to the user', 403
437432

438433
access_info = self._client_storage.access_info()
@@ -447,26 +442,25 @@ def _action(self, domain, **auth_args): # type: ignore
447442

448443

449444
class DeleteClient(_Action):
450-
def __init__(self, auth: AzureAuth, delete_mailbox: Callable[[str, str], None],
451-
delete_mx_records: Callable[[str], None], mailbox_storage: AzureTextStorage,
452-
pending_storage: AzureTextStorage, user_storage: AzureObjectStorage):
453-
445+
def __init__(self, auth: Auth, delete_mailbox: Callable[[str, str], None], delete_mx_records: Callable[[str], None],
446+
mailbox_storage: AzureTextStorage, pending_storage: AzureTextStorage,
447+
user_storage: AzureObjectStorage):
454448
self._auth = auth
455449
self._delete_mailbox = delete_mailbox
456450
self._delete_mx_records = delete_mx_records
457451
self._mailbox_storage = mailbox_storage
458452
self._pending_storage = pending_storage
459453
self._user_storage = user_storage
460454

461-
def _action(self, domain, **auth_args): # type: ignore
455+
def _action(self, domain, user, **auth_args): # type: ignore
462456
if not is_lowercase(domain):
463457
return 'domain must be lowercase', 400
464458

465459
client_id = self._auth.client_id_for(domain)
466460
if client_id is None:
467461
return 'client does not exist', 404
468462

469-
if not self._auth.is_owner(domain, auth_args.get('user')):
463+
if not self._auth.is_owner(domain, user):
470464
return 'client does not belong to the user', 403
471465

472466
self._delete_mailbox(client_id, domain)
@@ -486,12 +480,12 @@ def _delete_index(cls, storage: Union[AzureTextStorage, AzureObjectStorage], dom
486480

487481

488482
class CalculateNumberOfUsersMetric(_Action):
489-
def __init__(self, auth: AzureAuth, user_storage: AzureObjectStorage):
483+
def __init__(self, auth: Auth, user_storage: AzureObjectStorage):
490484
self._auth = auth
491485
self._user_storage = user_storage
492486

493-
def _action(self, domain, **auth_args): # type: ignore
494-
if not self._auth.is_owner(domain, auth_args.get('user')):
487+
def _action(self, domain, user, **auth_args): # type: ignore
488+
if not self._auth.is_owner(domain, user):
495489
return 'client does not belong to the user', 403
496490

497491
users = sum(1 for _ in self._user_storage.iter(f'{domain}/'))
@@ -502,12 +496,12 @@ def _action(self, domain, **auth_args): # type: ignore
502496

503497

504498
class CalculatePendingEmailsMetric(_Action):
505-
def __init__(self, auth: AzureAuth, pending_storage: AzureTextStorage):
499+
def __init__(self, auth: Auth, pending_storage: AzureTextStorage):
506500
self._auth = auth
507501
self._pending_storage = pending_storage
508502

509-
def _action(self, domain, **auth_args): # type: ignore
510-
if not self._auth.is_owner(domain, auth_args.get('user')):
503+
def _action(self, domain, user, **auth_args): # type: ignore
504+
if not self._auth.is_owner(domain, user):
511505
return 'client does not belong to the user', 403
512506

513507
pending_emails = sum(1 for _ in self._pending_storage.iter(f'{domain}/'))

opwen_email_server/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
REGISTRATION_USERNAME = env('LOKOLE_REGISTRATION_USERNAME', '')
5555
REGISTRATION_PASSWORD = env('LOKOLE_REGISTRATION_PASSWORD', '')
5656
REGISTRATION_GITHUB_ORGANIZATION = env('LOKOLE_REGISTRATION_GITHUB_ORGANIZATION', 'ascoderu')
57-
REGISTRATION_GITHUB_TEAM = env('LOKOLE_REGISTRATION_GITHUB_ORGANIZATION', 'lokole-registration')
57+
REGISTRATION_SUDO_TEAM = env('LOKOLE_REGISTRATION_SUDO_TEAM', 'lokole-sudo')
5858

5959
MAX_WIDTH_IMAGES = env.int('LOKOLE_MAX_WIDTH_EMAIL_IMAGES', 200)
6060
MAX_HEIGHT_IMAGES = env.int('LOKOLE_MAX_HEIGHT_EMAIL_IMAGES', 200)

opwen_email_server/constants/events.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
UNREGISTERED_CLIENT = 'unregistered_client' # type: Final
99
UNKNOWN_USER = 'unknown_user' # type: Final
1010
BAD_PASSWORD = 'bad_password' # type: Final # nosec
11-
MISSING_SCOPES = 'missing_scopes' # type: Final
1211
UNKNOWN_COMPRESSION_FORMAT = 'unknown_compression_format' # type: Final
1312
EMAILS_DELIVERED_TO_CLIENT = 'emails_delivered_to_client' # type: Final
1413
EMAILS_FORMATTED_FOR_CLIENT = 'emails_formatted_for_client' # type: Final

opwen_email_server/integration/azure.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from opwen_email_server import config
2+
from opwen_email_server.services.auth import Auth
23
from opwen_email_server.services.auth import AzureAuth
34
from opwen_email_server.services.auth import NoAuth
45
from opwen_email_server.services.storage import AzureFileStorage
@@ -19,15 +20,18 @@ def get_guid_source() -> NewGuid:
1920

2021

2122
@singleton
22-
def get_auth() -> AzureAuth:
23-
return AzureAuth(storage=AzureObjectStorage(
24-
account=config.TABLES_ACCOUNT,
25-
key=config.TABLES_KEY,
26-
host=config.TABLES_HOST,
27-
secure=config.TABLES_SECURE,
28-
container=config.CONTAINER_AUTH,
29-
provider=config.STORAGE_PROVIDER,
30-
))
23+
def get_auth() -> Auth:
24+
return AzureAuth(
25+
storage=AzureObjectStorage(
26+
account=config.TABLES_ACCOUNT,
27+
key=config.TABLES_KEY,
28+
host=config.TABLES_HOST,
29+
secure=config.TABLES_SECURE,
30+
container=config.CONTAINER_AUTH,
31+
provider=config.STORAGE_PROVIDER,
32+
),
33+
sudo_scope=config.REGISTRATION_SUDO_TEAM,
34+
)
3135

3236

3337
@singleton

0 commit comments

Comments
 (0)