mirror of
https://github.com/microsoft/onefuzz.git
synced 2025-06-16 20:08:09 +00:00
Adding Admin Checks to Node Operations. (#1779)
* Adding Admin Checks to Node Operations. * Importing function. * Changing naming convention. * Fixing webhook events. * Adding changes to scaleset init.
This commit is contained in:
committed by
GitHub
parent
0b1c7aea9c
commit
44059f20ca
@ -681,7 +681,6 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"admins": [
|
"admins": [
|
||||||
"00000000-0000-0000-0000-000000000000"
|
"00000000-0000-0000-0000-000000000000"
|
||||||
],
|
],
|
||||||
"allow_pool_management": true,
|
|
||||||
"allowed_aad_tenants": [
|
"allowed_aad_tenants": [
|
||||||
"00000000-0000-0000-0000-000000000000"
|
"00000000-0000-0000-0000-000000000000"
|
||||||
],
|
],
|
||||||
@ -693,7 +692,8 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"allowed_ips": [],
|
"allowed_ips": [],
|
||||||
"allowed_service_tags": []
|
"allowed_service_tags": []
|
||||||
},
|
},
|
||||||
"proxy_vm_sku": "Standard_B2s"
|
"proxy_vm_sku": "Standard_B2s",
|
||||||
|
"require_admin_privileges": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -809,11 +809,6 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Admins",
|
"title": "Admins",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"allow_pool_management": {
|
|
||||||
"default": true,
|
|
||||||
"title": "Allow Pool Management",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"allowed_aad_tenants": {
|
"allowed_aad_tenants": {
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
@ -854,6 +849,11 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Proxy Vm Sku",
|
"title": "Proxy Vm Sku",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"require_admin_privileges": {
|
||||||
|
"default": true,
|
||||||
|
"title": "Require Admin Privileges",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"vm_tags": {
|
"vm_tags": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -6006,11 +6006,6 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Admins",
|
"title": "Admins",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"allow_pool_management": {
|
|
||||||
"default": true,
|
|
||||||
"title": "Allow Pool Management",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"allowed_aad_tenants": {
|
"allowed_aad_tenants": {
|
||||||
"items": {
|
"items": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
@ -6051,6 +6046,11 @@ If webhook is set to have Event Grid message format then the payload will look a
|
|||||||
"title": "Proxy Vm Sku",
|
"title": "Proxy Vm Sku",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"require_admin_privileges": {
|
||||||
|
"default": true,
|
||||||
|
"title": "Require Admin Privileges",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"vm_tags": {
|
"vm_tags": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -9,7 +9,7 @@ from onefuzztypes.models import Error
|
|||||||
from onefuzztypes.requests import NodeGet, NodeSearch, NodeUpdate
|
from onefuzztypes.requests import NodeGet, NodeSearch, NodeUpdate
|
||||||
from onefuzztypes.responses import BoolResult
|
from onefuzztypes.responses import BoolResult
|
||||||
|
|
||||||
from ..onefuzzlib.endpoint_authorization import call_if_user
|
from ..onefuzzlib.endpoint_authorization import call_if_user, check_require_admins
|
||||||
from ..onefuzzlib.request import not_ok, ok, parse_request
|
from ..onefuzzlib.request import not_ok, ok, parse_request
|
||||||
from ..onefuzzlib.workers.nodes import Node, NodeMessage, NodeTasks
|
from ..onefuzzlib.workers.nodes import Node, NodeMessage, NodeTasks
|
||||||
|
|
||||||
@ -50,6 +50,10 @@ def post(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="NodeUpdate")
|
return not_ok(request, context="NodeUpdate")
|
||||||
|
|
||||||
|
answer = check_require_admins(req)
|
||||||
|
if isinstance(answer, Error):
|
||||||
|
return not_ok(answer, context="NodeUpdate")
|
||||||
|
|
||||||
node = Node.get_by_machine_id(request.machine_id)
|
node = Node.get_by_machine_id(request.machine_id)
|
||||||
if not node:
|
if not node:
|
||||||
return not_ok(
|
return not_ok(
|
||||||
@ -68,6 +72,10 @@ def delete(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="NodeDelete")
|
return not_ok(request, context="NodeDelete")
|
||||||
|
|
||||||
|
answer = check_require_admins(req)
|
||||||
|
if isinstance(answer, Error):
|
||||||
|
return not_ok(answer, context="NodeDelete")
|
||||||
|
|
||||||
node = Node.get_by_machine_id(request.machine_id)
|
node = Node.get_by_machine_id(request.machine_id)
|
||||||
if not node:
|
if not node:
|
||||||
return not_ok(
|
return not_ok(
|
||||||
@ -88,6 +96,10 @@ def patch(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="NodeReimage")
|
return not_ok(request, context="NodeReimage")
|
||||||
|
|
||||||
|
answer = check_require_admins(req)
|
||||||
|
if isinstance(answer, Error):
|
||||||
|
return not_ok(answer, context="NodeReimage")
|
||||||
|
|
||||||
node = Node.get_by_machine_id(request.machine_id)
|
node = Node.get_by_machine_id(request.machine_id)
|
||||||
if not node:
|
if not node:
|
||||||
return not_ok(
|
return not_ok(
|
||||||
|
@ -110,10 +110,10 @@ def can_modify_config(req: func.HttpRequest, config: InstanceConfig) -> bool:
|
|||||||
return can_modify_config_impl(config, user_info)
|
return can_modify_config_impl(config, user_info)
|
||||||
|
|
||||||
|
|
||||||
def check_can_manage_pools_impl(
|
def check_require_admins_impl(
|
||||||
config: InstanceConfig, user_info: UserInfo
|
config: InstanceConfig, user_info: UserInfo
|
||||||
) -> Optional[Error]:
|
) -> Optional[Error]:
|
||||||
if config.allow_pool_management:
|
if config.require_admin_privileges:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if config.admins is None:
|
if config.admins is None:
|
||||||
@ -125,25 +125,25 @@ def check_can_manage_pools_impl(
|
|||||||
return Error(code=ErrorCode.UNAUTHORIZED, errors=["not authorized to manage pools"])
|
return Error(code=ErrorCode.UNAUTHORIZED, errors=["not authorized to manage pools"])
|
||||||
|
|
||||||
|
|
||||||
def check_can_manage_pools(req: func.HttpRequest) -> Optional[Error]:
|
def check_require_admins(req: func.HttpRequest) -> Optional[Error]:
|
||||||
user_info = parse_jwt_token(req)
|
user_info = parse_jwt_token(req)
|
||||||
if isinstance(user_info, Error):
|
if isinstance(user_info, Error):
|
||||||
return user_info
|
return user_info
|
||||||
|
|
||||||
# When there are no admins in the `admins` list, all users are considered
|
# When there are no admins in the `admins` list, all users are considered
|
||||||
# admins. However, `allow_pool_management` is still useful to protect from
|
# admins. However, `require_admin_privileges` is still useful to protect from
|
||||||
# mistakes.
|
# mistakes.
|
||||||
#
|
#
|
||||||
# To make changes while still protecting against accidental changes to
|
# To make changes while still protecting against accidental changes to
|
||||||
# pools, do the following:
|
# pools, do the following:
|
||||||
#
|
#
|
||||||
# 1. set `allow_pool_management` to `True`
|
# 1. set `require_admin_privileges` to `True`
|
||||||
# 2. make the change
|
# 2. make the change
|
||||||
# 3. set `allow_pool_management` to `False`
|
# 3. set `require_admin_privileges` to `False`
|
||||||
|
|
||||||
config = InstanceConfig.fetch()
|
config = InstanceConfig.fetch()
|
||||||
|
|
||||||
return check_can_manage_pools_impl(config, user_info)
|
return check_require_admins_impl(config, user_info)
|
||||||
|
|
||||||
|
|
||||||
def is_user(token_data: UserInfo) -> bool:
|
def is_user(token_data: UserInfo) -> bool:
|
||||||
|
@ -21,7 +21,7 @@ from ..onefuzzlib.azure.creds import (
|
|||||||
from ..onefuzzlib.azure.queue import get_queue_sas
|
from ..onefuzzlib.azure.queue import get_queue_sas
|
||||||
from ..onefuzzlib.azure.storage import StorageType
|
from ..onefuzzlib.azure.storage import StorageType
|
||||||
from ..onefuzzlib.azure.vmss import list_available_skus
|
from ..onefuzzlib.azure.vmss import list_available_skus
|
||||||
from ..onefuzzlib.endpoint_authorization import call_if_user, check_can_manage_pools
|
from ..onefuzzlib.endpoint_authorization import call_if_user, check_require_admins
|
||||||
from ..onefuzzlib.request import not_ok, ok, parse_request
|
from ..onefuzzlib.request import not_ok, ok, parse_request
|
||||||
from ..onefuzzlib.workers.pools import Pool
|
from ..onefuzzlib.workers.pools import Pool
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ def post(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="PoolCreate")
|
return not_ok(request, context="PoolCreate")
|
||||||
|
|
||||||
answer = check_can_manage_pools(req)
|
answer = check_require_admins(req)
|
||||||
if isinstance(answer, Error):
|
if isinstance(answer, Error):
|
||||||
return not_ok(answer, context="PoolCreate")
|
return not_ok(answer, context="PoolCreate")
|
||||||
|
|
||||||
@ -133,7 +133,7 @@ def delete(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="PoolDelete")
|
return not_ok(request, context="PoolDelete")
|
||||||
|
|
||||||
answer = check_can_manage_pools(req)
|
answer = check_require_admins(req)
|
||||||
if isinstance(answer, Error):
|
if isinstance(answer, Error):
|
||||||
return not_ok(answer, context="PoolDelete")
|
return not_ok(answer, context="PoolDelete")
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from onefuzztypes.responses import BoolResult
|
|||||||
from ..onefuzzlib.azure.creds import get_base_region, get_regions
|
from ..onefuzzlib.azure.creds import get_base_region, get_regions
|
||||||
from ..onefuzzlib.azure.vmss import list_available_skus
|
from ..onefuzzlib.azure.vmss import list_available_skus
|
||||||
from ..onefuzzlib.config import InstanceConfig
|
from ..onefuzzlib.config import InstanceConfig
|
||||||
from ..onefuzzlib.endpoint_authorization import call_if_user, check_can_manage_pools
|
from ..onefuzzlib.endpoint_authorization import call_if_user, check_require_admins
|
||||||
from ..onefuzzlib.request import not_ok, ok, parse_request
|
from ..onefuzzlib.request import not_ok, ok, parse_request
|
||||||
from ..onefuzzlib.workers.pools import Pool
|
from ..onefuzzlib.workers.pools import Pool
|
||||||
from ..onefuzzlib.workers.scalesets import AutoScale, Scaleset
|
from ..onefuzzlib.workers.scalesets import AutoScale, Scaleset
|
||||||
@ -49,7 +49,7 @@ def post(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="ScalesetCreate")
|
return not_ok(request, context="ScalesetCreate")
|
||||||
|
|
||||||
answer = check_can_manage_pools(req)
|
answer = check_require_admins(req)
|
||||||
if isinstance(answer, Error):
|
if isinstance(answer, Error):
|
||||||
return not_ok(answer, context="ScalesetCreate")
|
return not_ok(answer, context="ScalesetCreate")
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ def delete(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="ScalesetDelete")
|
return not_ok(request, context="ScalesetDelete")
|
||||||
|
|
||||||
answer = check_can_manage_pools(req)
|
answer = check_require_admins(req)
|
||||||
if isinstance(answer, Error):
|
if isinstance(answer, Error):
|
||||||
return not_ok(answer, context="ScalesetDelete")
|
return not_ok(answer, context="ScalesetDelete")
|
||||||
|
|
||||||
@ -144,7 +144,7 @@ def patch(req: func.HttpRequest) -> func.HttpResponse:
|
|||||||
if isinstance(request, Error):
|
if isinstance(request, Error):
|
||||||
return not_ok(request, context="ScalesetUpdate")
|
return not_ok(request, context="ScalesetUpdate")
|
||||||
|
|
||||||
answer = check_can_manage_pools(req)
|
answer = check_require_admins(req)
|
||||||
if isinstance(answer, Error):
|
if isinstance(answer, Error):
|
||||||
return not_ok(answer, context="ScalesetUpdate")
|
return not_ok(answer, context="ScalesetUpdate")
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ from onefuzztypes.models import UserInfo
|
|||||||
from __app__.onefuzzlib.config import InstanceConfig
|
from __app__.onefuzzlib.config import InstanceConfig
|
||||||
from __app__.onefuzzlib.endpoint_authorization import (
|
from __app__.onefuzzlib.endpoint_authorization import (
|
||||||
can_modify_config_impl,
|
can_modify_config_impl,
|
||||||
check_can_manage_pools_impl,
|
check_require_admins_impl,
|
||||||
)
|
)
|
||||||
|
|
||||||
if "ONEFUZZ_INSTANCE_NAME" not in os.environ:
|
if "ONEFUZZ_INSTANCE_NAME" not in os.environ:
|
||||||
@ -69,9 +69,9 @@ class TestAdmin(unittest.TestCase):
|
|||||||
|
|
||||||
# by default, any can modify
|
# by default, any can modify
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
check_can_manage_pools_impl(
|
check_require_admins_impl(
|
||||||
InstanceConfig(
|
InstanceConfig(
|
||||||
allowed_aad_tenants=[UUID(int=0)], allow_pool_management=True
|
allowed_aad_tenants=[UUID(int=0)], require_admin_privileges=True
|
||||||
),
|
),
|
||||||
UserInfo(),
|
UserInfo(),
|
||||||
)
|
)
|
||||||
@ -79,9 +79,9 @@ class TestAdmin(unittest.TestCase):
|
|||||||
|
|
||||||
# with oid, but no admin
|
# with oid, but no admin
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
check_can_manage_pools_impl(
|
check_require_admins_impl(
|
||||||
InstanceConfig(
|
InstanceConfig(
|
||||||
allowed_aad_tenants=[UUID(int=0)], allow_pool_management=True
|
allowed_aad_tenants=[UUID(int=0)], require_admin_privileges=True
|
||||||
),
|
),
|
||||||
UserInfo(object_id=user1),
|
UserInfo(object_id=user1),
|
||||||
)
|
)
|
||||||
@ -89,10 +89,10 @@ class TestAdmin(unittest.TestCase):
|
|||||||
|
|
||||||
# is admin
|
# is admin
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
check_can_manage_pools_impl(
|
check_require_admins_impl(
|
||||||
InstanceConfig(
|
InstanceConfig(
|
||||||
allowed_aad_tenants=[UUID(int=0)],
|
allowed_aad_tenants=[UUID(int=0)],
|
||||||
allow_pool_management=False,
|
require_admin_privileges=False,
|
||||||
admins=[user1],
|
admins=[user1],
|
||||||
),
|
),
|
||||||
UserInfo(object_id=user1),
|
UserInfo(object_id=user1),
|
||||||
@ -101,10 +101,10 @@ class TestAdmin(unittest.TestCase):
|
|||||||
|
|
||||||
# no user oid set
|
# no user oid set
|
||||||
self.assertIsNotNone(
|
self.assertIsNotNone(
|
||||||
check_can_manage_pools_impl(
|
check_require_admins_impl(
|
||||||
InstanceConfig(
|
InstanceConfig(
|
||||||
allowed_aad_tenants=[UUID(int=0)],
|
allowed_aad_tenants=[UUID(int=0)],
|
||||||
allow_pool_management=False,
|
require_admin_privileges=False,
|
||||||
admins=[user1],
|
admins=[user1],
|
||||||
),
|
),
|
||||||
UserInfo(),
|
UserInfo(),
|
||||||
@ -113,10 +113,10 @@ class TestAdmin(unittest.TestCase):
|
|||||||
|
|
||||||
# not an admin
|
# not an admin
|
||||||
self.assertIsNotNone(
|
self.assertIsNotNone(
|
||||||
check_can_manage_pools_impl(
|
check_require_admins_impl(
|
||||||
InstanceConfig(
|
InstanceConfig(
|
||||||
allowed_aad_tenants=[UUID(int=0)],
|
allowed_aad_tenants=[UUID(int=0)],
|
||||||
allow_pool_management=False,
|
require_admin_privileges=False,
|
||||||
admins=[user1],
|
admins=[user1],
|
||||||
),
|
),
|
||||||
UserInfo(object_id=user2),
|
UserInfo(object_id=user2),
|
||||||
|
@ -872,7 +872,7 @@ class InstanceConfig(BaseModel):
|
|||||||
admins: Optional[List[UUID]] = None
|
admins: Optional[List[UUID]] = None
|
||||||
|
|
||||||
# if set, only admins can manage pools or scalesets
|
# if set, only admins can manage pools or scalesets
|
||||||
allow_pool_management: bool = Field(default=True)
|
require_admin_privileges: bool = Field(default=True)
|
||||||
|
|
||||||
allowed_aad_tenants: List[UUID]
|
allowed_aad_tenants: List[UUID]
|
||||||
network_config: NetworkConfig = Field(default_factory=NetworkConfig)
|
network_config: NetworkConfig = Field(default_factory=NetworkConfig)
|
||||||
|
Reference in New Issue
Block a user