Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit 2fcb499

Browse files
bmc-msftdemoray
andauthored
Merge pull request from GHSA-q5vh-6whw-x745
* verify aad tenants, primarily needed in multi-tenant deployments * add logging and fix trailing slash for issuer * handle call_if* not supporting additional argument callbacks * add logging * include new datatype in webhook docs * fix pytypes unit tests Co-authored-by: Brian Caswell <[email protected]>
1 parent ba3a6ea commit 2fcb499

File tree

12 files changed

+193
-31
lines changed

12 files changed

+193
-31
lines changed

docs/webhook_events.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,10 @@ Each event will be submitted via HTTP POST to the user provided URL.
641641
"admins": [
642642
"00000000-0000-0000-0000-000000000000"
643643
],
644-
"allow_pool_management": true
644+
"allow_pool_management": true,
645+
"allowed_aad_tenants": [
646+
"00000000-0000-0000-0000-000000000000"
647+
]
645648
}
646649
}
647650
```
@@ -665,8 +668,19 @@ Each event will be submitted via HTTP POST to the user provided URL.
665668
"default": true,
666669
"title": "Allow Pool Management",
667670
"type": "boolean"
671+
},
672+
"allowed_aad_tenants": {
673+
"items": {
674+
"format": "uuid",
675+
"type": "string"
676+
},
677+
"title": "Allowed Aad Tenants",
678+
"type": "array"
668679
}
669680
},
681+
"required": [
682+
"allowed_aad_tenants"
683+
],
670684
"title": "InstanceConfig",
671685
"type": "object"
672686
}
@@ -5599,8 +5613,19 @@ Each event will be submitted via HTTP POST to the user provided URL.
55995613
"default": true,
56005614
"title": "Allow Pool Management",
56015615
"type": "boolean"
5616+
},
5617+
"allowed_aad_tenants": {
5618+
"items": {
5619+
"format": "uuid",
5620+
"type": "string"
5621+
},
5622+
"title": "Allowed Aad Tenants",
5623+
"type": "array"
56025624
}
56035625
},
5626+
"required": [
5627+
"allowed_aad_tenants"
5628+
],
56045629
"title": "InstanceConfig",
56055630
"type": "object"
56065631
},

src/api-service/__app__/info/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
get_instance_id,
1515
get_subscription,
1616
)
17+
from ..onefuzzlib.endpoint_authorization import call_if_user
1718
from ..onefuzzlib.request import ok
1819
from ..onefuzzlib.versions import versions
1920

2021

21-
def main(req: func.HttpRequest) -> func.HttpResponse:
22+
def get(req: func.HttpRequest) -> func.HttpResponse:
2223
response = ok(
2324
Info(
2425
resource_group=get_base_resource_group(),
@@ -32,3 +33,11 @@ def main(req: func.HttpRequest) -> func.HttpResponse:
3233
)
3334

3435
return response
36+
37+
38+
def main(req: func.HttpRequest) -> func.HttpResponse:
39+
methods = {"GET": get}
40+
method = methods[req.method]
41+
result = call_if_user(req, method)
42+
43+
return result

src/api-service/__app__/negotiate/__init__.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import azure.functions as func
77

8+
from ..onefuzzlib.endpoint_authorization import call_if_user
9+
810
# This endpoint handles the signalr negotation
911
# As we do not differentiate from clients at this time, we pass the Functions runtime
1012
# provided connection straight to the client
@@ -14,8 +16,19 @@
1416

1517

1618
def main(req: func.HttpRequest, connectionInfoJson: str) -> func.HttpResponse:
17-
return func.HttpResponse(
18-
connectionInfoJson,
19-
status_code=200,
20-
headers={"Content-type": "application/json"},
21-
)
19+
# NOTE: this is a sub-method because the call_if* do not support callbacks with
20+
# additional arguments at this time. Once call_if* supports additional arguments,
21+
# this should be made a generic function
22+
def post(req: func.HttpRequest) -> func.HttpResponse:
23+
return func.HttpResponse(
24+
connectionInfoJson,
25+
status_code=200,
26+
headers={"Content-type": "application/json"},
27+
)
28+
29+
methods = {"POST": post}
30+
method = methods[req.method]
31+
32+
result = call_if_user(req, method)
33+
34+
return result

src/api-service/__app__/onefuzzlib/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def key_fields(cls) -> Tuple[str, Optional[str]]:
2525
def fetch(cls) -> "InstanceConfig":
2626
entry = cls.get(get_instance_name())
2727
if entry is None:
28-
entry = cls()
28+
entry = cls(allowed_aad_tenants=[])
2929
entry.save()
3030
return entry
3131

src/api-service/__app__/onefuzzlib/user_credentials.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
# Copyright (c) Microsoft Corporation.
44
# Licensed under the MIT License.
55

6-
from typing import Optional
6+
import logging
7+
from typing import List, Optional
78
from uuid import UUID
89

910
import azure.functions as func
1011
import jwt
12+
from memoization import cached
1113
from onefuzztypes.enums import ErrorCode
1214
from onefuzztypes.models import Error, Result, UserInfo
1315

16+
from .config import InstanceConfig
17+
1418

1519
def get_bearer_token(request: func.HttpRequest) -> Optional[str]:
1620
auth: str = request.headers.get("Authorization", None)
@@ -39,6 +43,13 @@ def get_auth_token(request: func.HttpRequest) -> Optional[str]:
3943
return str(token_header)
4044

4145

46+
@cached(ttl=60)
47+
def get_allowed_tenants() -> List[str]:
48+
config = InstanceConfig.fetch()
49+
entries = [f"https://sts.windows.net/{x}/" for x in config.allowed_aad_tenants]
50+
return entries
51+
52+
4253
def parse_jwt_token(request: func.HttpRequest) -> Result[UserInfo]:
4354
"""Obtains the Access Token from the Authorization Header"""
4455
token_str = get_auth_token(request)
@@ -48,9 +59,20 @@ def parse_jwt_token(request: func.HttpRequest) -> Result[UserInfo]:
4859
errors=["unable to find authorization token"],
4960
)
5061

51-
# This token has already been verified by the azure authentication layer
62+
# The JWT token has already been verified by the azure authentication layer,
63+
# but we need to verify the tenant is as we expect.
5264
token = jwt.decode(token_str, options={"verify_signature": False})
5365

66+
if "iss" not in token:
67+
return Error(
68+
code=ErrorCode.INVALID_REQUEST, errors=["missing issuer from token"]
69+
)
70+
71+
tenants = get_allowed_tenants()
72+
if token["iss"] not in tenants:
73+
logging.error("issuer not from allowed tenant: %s - %s", token["iss"], tenants)
74+
return Error(code=ErrorCode.INVALID_REQUEST, errors=["unauthorized AAD issuer"])
75+
5476
application_id = UUID(token["appid"]) if "appid" in token else None
5577
object_id = UUID(token["oid"]) if "oid" in token else None
5678
upn = token.get("upn")

src/api-service/tests/test_auth_check.py

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import os
77
import unittest
8-
from uuid import uuid4
8+
from uuid import UUID, uuid4
99

1010
from onefuzztypes.models import UserInfo
1111

@@ -25,29 +25,41 @@ def test_modify_config(self) -> None:
2525
user2 = uuid4()
2626

2727
# no admins set
28-
self.assertTrue(can_modify_config_impl(InstanceConfig(), UserInfo()))
28+
self.assertTrue(
29+
can_modify_config_impl(
30+
InstanceConfig(allowed_aad_tenants=[UUID(int=0)]), UserInfo()
31+
)
32+
)
2933

3034
# with oid, but no admin
3135
self.assertTrue(
32-
can_modify_config_impl(InstanceConfig(), UserInfo(object_id=user1))
36+
can_modify_config_impl(
37+
InstanceConfig(allowed_aad_tenants=[UUID(int=0)]),
38+
UserInfo(object_id=user1),
39+
)
3340
)
3441

3542
# is admin
3643
self.assertTrue(
3744
can_modify_config_impl(
38-
InstanceConfig(admins=[user1]), UserInfo(object_id=user1)
45+
InstanceConfig(allowed_aad_tenants=[UUID(int=0)], admins=[user1]),
46+
UserInfo(object_id=user1),
3947
)
4048
)
4149

4250
# no user oid set
4351
self.assertFalse(
44-
can_modify_config_impl(InstanceConfig(admins=[user1]), UserInfo())
52+
can_modify_config_impl(
53+
InstanceConfig(allowed_aad_tenants=[UUID(int=0)], admins=[user1]),
54+
UserInfo(),
55+
)
4556
)
4657

4758
# not an admin
4859
self.assertFalse(
4960
can_modify_config_impl(
50-
InstanceConfig(admins=[user1]), UserInfo(object_id=user2)
61+
InstanceConfig(allowed_aad_tenants=[UUID(int=0)], admins=[user1]),
62+
UserInfo(object_id=user2),
5163
)
5264
)
5365

@@ -58,36 +70,55 @@ def test_manage_pools(self) -> None:
5870
# by default, any can modify
5971
self.assertIsNone(
6072
check_can_manage_pools_impl(
61-
InstanceConfig(allow_pool_management=True), UserInfo()
73+
InstanceConfig(
74+
allowed_aad_tenants=[UUID(int=0)], allow_pool_management=True
75+
),
76+
UserInfo(),
6277
)
6378
)
6479

6580
# with oid, but no admin
6681
self.assertIsNone(
6782
check_can_manage_pools_impl(
68-
InstanceConfig(allow_pool_management=True), UserInfo(object_id=user1)
83+
InstanceConfig(
84+
allowed_aad_tenants=[UUID(int=0)], allow_pool_management=True
85+
),
86+
UserInfo(object_id=user1),
6987
)
7088
)
7189

7290
# is admin
7391
self.assertIsNone(
7492
check_can_manage_pools_impl(
75-
InstanceConfig(allow_pool_management=False, admins=[user1]),
93+
InstanceConfig(
94+
allowed_aad_tenants=[UUID(int=0)],
95+
allow_pool_management=False,
96+
admins=[user1],
97+
),
7698
UserInfo(object_id=user1),
7799
)
78100
)
79101

80102
# no user oid set
81103
self.assertIsNotNone(
82104
check_can_manage_pools_impl(
83-
InstanceConfig(allow_pool_management=False, admins=[user1]), UserInfo()
105+
InstanceConfig(
106+
allowed_aad_tenants=[UUID(int=0)],
107+
allow_pool_management=False,
108+
admins=[user1],
109+
),
110+
UserInfo(),
84111
)
85112
)
86113

87114
# not an admin
88115
self.assertIsNotNone(
89116
check_can_manage_pools_impl(
90-
InstanceConfig(allow_pool_management=False, admins=[user1]),
117+
InstanceConfig(
118+
allowed_aad_tenants=[UUID(int=0)],
119+
allow_pool_management=False,
120+
admins=[user1],
121+
),
91122
UserInfo(object_id=user2),
92123
)
93124
)

src/deployment/azuredeploy.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,10 @@
821821
"scaleset-identity": {
822822
"type": "string",
823823
"value": "[variables('scaleset_identity')]"
824+
},
825+
"tenant_id": {
826+
"type": "string",
827+
"value": "[subscription().tenantId]"
824828
}
825829
}
826830
}

src/deployment/deploy.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
set_app_audience,
7373
update_pool_registration,
7474
)
75-
from set_admins import update_admins
75+
from set_admins import update_admins, update_allowed_aad_tenants
7676

7777
# Found by manually assigning the User.Read permission to application
7878
# registration in the admin portal. The values are in the manifest under
@@ -130,7 +130,8 @@ def __init__(
130130
multi_tenant_domain: str,
131131
upgrade: bool,
132132
subscription_id: Optional[str],
133-
admins: List[UUID]
133+
admins: List[UUID],
134+
allowed_aad_tenants: List[UUID],
134135
):
135136
self.subscription_id = subscription_id
136137
self.resource_group = resource_group
@@ -161,6 +162,7 @@ def __init__(
161162
self.export_appinsights = export_appinsights
162163
self.log_service_principal = log_service_principal
163164
self.admins = admins
165+
self.allowed_aad_tenants = allowed_aad_tenants
164166

165167
machine = platform.machine()
166168
system = platform.system()
@@ -560,13 +562,20 @@ def apply_migrations(self) -> None:
560562
table_service = TableService(account_name=name, account_key=key)
561563
migrate(table_service, self.migrations)
562564

563-
def set_admins(self) -> None:
565+
def set_instance_config(self) -> None:
564566
name = self.results["deploy"]["func-name"]["value"]
565567
key = self.results["deploy"]["func-key"]["value"]
568+
tenant = UUID(self.results["deploy"]["tenant_id"]["value"])
566569
table_service = TableService(account_name=name, account_key=key)
570+
567571
if self.admins:
568572
update_admins(table_service, self.application_name, self.admins)
569573

574+
tenants = self.allowed_aad_tenants
575+
if tenant not in tenants:
576+
tenants.append(tenant)
577+
update_allowed_aad_tenants(table_service, self.application_name, tenants)
578+
570579
def create_queues(self) -> None:
571580
logger.info("creating eventgrid destination queue")
572581

@@ -926,7 +935,7 @@ def main() -> None:
926935

927936
full_deployment_states = rbac_only_states + [
928937
("apply_migrations", Client.apply_migrations),
929-
("set_admins", Client.set_admins),
938+
("set_instance_config", Client.set_instance_config),
930939
("queues", Client.create_queues),
931940
("eventgrid", Client.create_eventgrid),
932941
("tools", Client.upload_tools),
@@ -1038,6 +1047,12 @@ def main() -> None:
10381047
nargs="*",
10391048
help="set the list of administrators (by OID in AAD)",
10401049
)
1050+
parser.add_argument(
1051+
"--allowed_aad_tenants",
1052+
type=UUID,
1053+
nargs="*",
1054+
help="Set additional AAD tenants beyond the tenant the app is deployed in",
1055+
)
10411056

10421057
args = parser.parse_args()
10431058

@@ -1066,6 +1081,7 @@ def main() -> None:
10661081
upgrade=args.upgrade,
10671082
subscription_id=args.subscription_id,
10681083
admins=args.set_admins,
1084+
allowed_aad_tenants=args.allowed_aad_tenants or [],
10691085
)
10701086
if args.verbose:
10711087
level = logging.DEBUG

0 commit comments

Comments
 (0)