Skip to content
This repository has been archived by the owner on Aug 2, 2023. It is now read-only.

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prevent domain admin from mutating domain (#148)
Browse files Browse the repository at this point in the history
* Domain mutation should only be allowed to global admins.
adrysn committed May 13, 2019
1 parent 60e4bee commit 3e5e02e
Showing 2 changed files with 99 additions and 5 deletions.
27 changes: 22 additions & 5 deletions src/ai/backend/manager/models/domain.py
Original file line number Diff line number Diff line change
@@ -95,7 +95,19 @@ class ModifyDomainInput(graphene.InputObjectType):
total_resource_slots = graphene.JSONString(required=False)


class CreateDomain(graphene.Mutation):
class DomainMutationMixin:

@staticmethod
def check_perm(info):
from .user import UserRole
user = info.context['user']
if user['role'] == UserRole.ADMIN and user['domain_name'] is None:
return True
else:
return False


class CreateDomain(DomainMutationMixin, graphene.Mutation):

class Arguments:
name = graphene.String(required=True)
@@ -107,6 +119,7 @@ class Arguments:

@classmethod
async def mutate(cls, root, info, name, props):
assert cls.check_perm(info), 'no permission'
known_slot_types = await info.context['config_server'].get_resource_slots()
async with info.context['dbpool'].acquire() as conn, conn.begin():
assert _rx_slug.search(name) is not None, 'invalid name format. slug format required.'
@@ -135,7 +148,7 @@ async def mutate(cls, root, info, name, props):
return cls(ok=False, msg=f'unexpected error: {e}', domain=None)


class ModifyDomain(graphene.Mutation):
class ModifyDomain(DomainMutationMixin, graphene.Mutation):

class Arguments:
name = graphene.String(required=True)
@@ -147,6 +160,7 @@ class Arguments:

@classmethod
async def mutate(cls, root, info, name, props):
assert cls.check_perm(info), 'no permission'
known_slot_types = await info.context['config_server'].get_resource_slots()
async with info.context['dbpool'].acquire() as conn, conn.begin():
data = {}
@@ -164,8 +178,10 @@ def clean_resource_slot(v):
set_if_set('description')
set_if_set('is_active')
set_if_set('total_resource_slots', clean_resource_slot)
assert _rx_slug.search(data['name']) is not None, \
'invalid name format. slug format required.'

if 'name' in data:
assert _rx_slug.search(data['name']) is not None, \
'invalid name format. slug format required.'

query = (domains.update().values(data).where(domains.c.name == name))
try:
@@ -185,7 +201,7 @@ def clean_resource_slot(v):
return cls(ok=False, msg=f'unexpected error: {e}', domain=None)


class DeleteDomain(graphene.Mutation):
class DeleteDomain(DomainMutationMixin, graphene.Mutation):

class Arguments:
name = graphene.String(required=True)
@@ -195,6 +211,7 @@ class Arguments:

@classmethod
async def mutate(cls, root, info, name):
assert cls.check_perm(info), 'no permission'
async with info.context['dbpool'].acquire() as conn, conn.begin():
try:
query = domains.delete().where(domains.c.name == name)
77 changes: 77 additions & 0 deletions tests/gateway/test_domain.py
Original file line number Diff line number Diff line change
@@ -196,6 +196,83 @@ async def test_mutate_domain(self, create_app_and_client, get_headers):

assert ret.status == 200

async def test_domain_user_cannot_mutate_domain(self, create_app_and_client, get_headers,
default_domain_keypair):
app, client = await create_app_and_client(modules=['auth', 'admin', 'manager'])

# Create a domain.
domain_name = 'domain-user-cannot-mutate-domain'
query = textwrap.dedent('''\
mutation($name: String!, $input: DomainInput!) {
create_domain(name: $name, props: $input) {
ok msg domain { name description is_active total_resource_slots }
}
}''')
variables = {
'name': domain_name,
'input': {
'description': 'desc',
'is_active': True,
'total_resource_slots': '{}',
}
}
payload = json.dumps({'query': query, 'variables': variables}).encode()
headers = get_headers('POST', self.url, payload, keypair=default_domain_keypair)
ret = await client.post(self.url, data=payload, headers=headers)
assert ret.status == 400

# Create a domain for testing by global admin.
query = textwrap.dedent('''\
mutation($name: String!, $input: DomainInput!) {
create_domain(name: $name, props: $input) {
ok msg domain { name description is_active total_resource_slots }
}
}''')
variables = {
'name': domain_name,
'input': {
'description': 'desc',
'is_active': True,
'total_resource_slots': '{}',
}
}
payload = json.dumps({'query': query, 'variables': variables}).encode()
headers = get_headers('POST', self.url, payload)
ret = await client.post(self.url, data=payload, headers=headers)
assert ret.status == 200

# Update the domain.
query = textwrap.dedent('''\
mutation($name: String!, $input: ModifyDomainInput!) {
modify_domain(name: $name, props: $input) {
ok msg domain { name description is_active total_resource_slots }
}
}''')
variables = {
'name': domain_name,
'input': {
'name': domain_name + '-by-domain-admin',
'description': 'New domain-mod',
'is_active': False,
'total_resource_slots': '{"cpu": "1", "mem": "1"}',
}
}
payload = json.dumps({'query': query, 'variables': variables}).encode()
headers = get_headers('POST', self.url, payload, keypair=default_domain_keypair)
ret = await client.post(self.url, data=payload, headers=headers)
assert ret.status == 400

# Delete the domain.
query = textwrap.dedent('''\
mutation($name: String!) {
delete_domain(name: $name) { ok msg }
}''')
variables = {'name': domain_name}
payload = json.dumps({'query': query, 'variables': variables}).encode()
headers = get_headers('POST', self.url, payload, keypair=default_domain_keypair)
ret = await client.post(self.url, data=payload, headers=headers)
assert ret.status == 400

async def test_name_should_be_slugged(self, create_app_and_client, get_headers):
app, client = await create_app_and_client(modules=['auth', 'admin', 'manager'])

0 comments on commit 3e5e02e

Please sign in to comment.