Skip to content

Commit

Permalink
Add default project name to created service account full token (#293)
Browse files Browse the repository at this point in the history
* add default project name to created service account NEURO_PASS_CONFIG_TOKEN

* add default org name to created service account NEURO_PASS_CONFIG_TOKEN
  • Loading branch information
YevheniiSemendiak authored Apr 27, 2023
1 parent dd153f1 commit 7e061fc
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 22 deletions.
2 changes: 2 additions & 0 deletions platform_service_accounts_api/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def validate_name(name: str) -> None:
class ServiceAccountCreateSchema(Schema):
name = fields.String(required=False, load_default=None, validate=validate_name)
default_cluster = fields.String(required=True)
default_project = fields.String(required=True)
default_org = fields.String(required=False)


class ServiceAccountSchema(ServiceAccountCreateSchema):
Expand Down
24 changes: 13 additions & 11 deletions platform_service_accounts_api/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ class AccountCreateData:
name: Optional[str]
default_cluster: str
owner: str
default_project: str
default_org: Optional[str] = None


@dataclass(frozen=True)
Expand All @@ -47,16 +49,16 @@ def __init__(
def _make_token_uri(self, account_id: str) -> str:
return f"token://service_account/{account_id}"

def _encode_token(self, auth_token: str, default_cluster: str) -> str:
return base64.b64encode(
json.dumps(
{
"token": auth_token,
"cluster": default_cluster,
"url": str(self._api_base_url),
}
).encode()
).decode()
def _encode_token(self, auth_token: str, account: ServiceAccount) -> str:
token = {
"token": auth_token,
"cluster": account.default_cluster,
"url": str(self._api_base_url),
"project_name": account.default_project,
}
if account.default_org:
token["org_name"] = account.default_org
return base64.b64encode(json.dumps(token).encode()).decode()

async def create(self, data: AccountCreateData) -> ServiceAccountWithToken:
if data.name:
Expand All @@ -74,7 +76,7 @@ async def create(self, data: AccountCreateData) -> ServiceAccountWithToken:
try:
await self._auth_client.add_user(User(name=role))
auth_token = await self._auth_client.get_user_token(role)
token = self._encode_token(auth_token, account.default_cluster)
token = self._encode_token(auth_token, account)
except Exception:
await self._storage.delete(account.id)
raise
Expand Down
2 changes: 2 additions & 0 deletions platform_service_accounts_api/storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ServiceAccountData:
role: str
owner: str
created_at: datetime.datetime
default_project: str
default_org: Optional[str]


@dataclass(frozen=True)
Expand Down
109 changes: 98 additions & 11 deletions tests/integration/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ async def test_account_create(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -246,6 +250,7 @@ async def test_account_create(
token_data = json.loads(base64.b64decode(token.encode()).decode())
assert token_data["cluster"] == "default"
assert token_data["url"] == "https://dev.neu.ro/api/v1"
assert token_data["project_name"] == "some-project"

auth_token = token_data["token"]

Expand All @@ -262,7 +267,7 @@ async def test_account_create_no_name(

async with client.post(
url=service_accounts_api.accounts_url,
json={"default_cluster": "default"},
json={"default_cluster": "default", "default_project": "some-project"},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -273,6 +278,7 @@ async def test_account_create_no_name(
token_data = json.loads(base64.b64decode(token.encode()).decode())
assert token_data["cluster"] == "default"
assert token_data["url"] == "https://dev.neu.ro/api/v1"
assert token_data["project_name"] == "some-project"

auth_token = token_data["token"]

Expand Down Expand Up @@ -304,7 +310,11 @@ async def test_account_create_invalid_name(
) -> None:
async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPBadRequest.status_code, await resp.text()
Expand All @@ -327,6 +337,48 @@ async def test_account_create_no_cluster(
payload = await resp.json()
assert "Missing data for required field" in payload["error"]

async def test_account_create_default_org(
self,
service_accounts_api: ServiceAccountsApiEndpoints,
regular_user: _User,
client: aiohttp.ClientSession,
auth_client: AuthClient,
) -> None:
role_name = f"{regular_user.name}/service-accounts/sa-name"
async with client.post(
url=service_accounts_api.accounts_url,
json={
"name": "sa-name",
"default_cluster": "default",
"default_project": "some-project",
"default_org": "some-org",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
payload = await resp.json()
assert payload["name"] == "sa-name"
assert payload["owner"] == regular_user.name
assert payload["default_cluster"] == "default"
assert payload["default_project"] == "some-project"
assert payload["default_org"] == "some-org"
assert datetime.fromisoformat(payload["created_at"])
assert not payload["role_deleted"]
assert "id" in payload
token = payload["token"]

token_data = json.loads(base64.b64decode(token.encode()).decode())
assert token_data["cluster"] == "default"
assert token_data["url"] == "https://dev.neu.ro/api/v1"
assert token_data["org_name"] == "some-org"
assert token_data["cluster"] == "default"
assert token_data["project_name"] == "some-project"

auth_token = token_data["token"]

fetched_role = await auth_client.get_user(role_name, token=auth_token)
assert fetched_role.name == role_name

async def test_account_get(
self,
service_accounts_api: ServiceAccountsApiEndpoints,
Expand All @@ -339,7 +391,11 @@ async def test_account_get(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -357,6 +413,7 @@ async def test_account_get(
assert payload["name"] == sa_name
assert payload["owner"] == regular_user.name
assert payload["default_cluster"] == "default"
assert payload["default_project"] == "some-project"
assert payload["role"] == role_name
assert datetime.fromisoformat(payload["created_at"])
assert not payload["role_deleted"]
Expand All @@ -373,7 +430,11 @@ async def test_account_get_by_name(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -391,6 +452,7 @@ async def test_account_get_by_name(
assert payload["name"] == sa_name
assert payload["owner"] == regular_user.name
assert payload["default_cluster"] == "default"
assert payload["default_project"] == "some-project"
assert payload["role"] == role_name
assert datetime.fromisoformat(payload["created_at"])
assert not payload["role_deleted"]
Expand Down Expand Up @@ -421,7 +483,11 @@ async def test_accounts_list_one(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -441,6 +507,7 @@ async def test_accounts_list_one(
assert payload["name"] == sa_name
assert payload["owner"] == regular_user.name
assert payload["default_cluster"] == "default"
assert payload["default_project"] == "some-project"
assert payload["role"] == role_name
assert datetime.fromisoformat(payload["created_at"])
assert not payload["role_deleted"]
Expand All @@ -454,7 +521,11 @@ async def test_accounts_list_many(
) -> None:
async with client.post(
url=service_accounts_api.accounts_url,
json={"name": "test1", "default_cluster": "default"},
json={
"name": "test1",
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -463,7 +534,11 @@ async def test_accounts_list_many(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": "test2", "default_cluster": "default"},
json={
"name": "test2",
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand All @@ -490,7 +565,11 @@ async def test_account_delete(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand Down Expand Up @@ -522,7 +601,11 @@ async def test_account_delete_role_deleted(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand Down Expand Up @@ -552,7 +635,11 @@ async def test_account_delete_role_recreated(

async with client.post(
url=service_accounts_api.accounts_url,
json={"name": sa_name, "default_cluster": "default"},
json={
"name": sa_name,
"default_cluster": "default",
"default_project": "some-project",
},
headers=regular_user.headers,
) as resp:
assert resp.status == HTTPCreated.status_code, await resp.text()
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/test_in_memory_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ async def gen_data(self, **kwargs: Any) -> ServiceAccountData:
owner=secrets.token_hex(8),
role=secrets.token_hex(8),
default_cluster=secrets.token_hex(8),
default_project=secrets.token_hex(8),
default_org=None,
created_at=datetime.now(timezone.utc),
)
# Updating this way so constructor call is typechecked properly
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class TestService:
name="test",
owner="testowner",
default_cluster="default",
default_project="default-project",
default_org=None,
)

def compare_data(
Expand Down Expand Up @@ -99,6 +101,7 @@ async def test_create(
assert token_data["token"] == f"token-{expected_role}"
assert token_data["cluster"] == self.CREATE_DATA.default_cluster
assert token_data["url"] == "https://dev.neu.ro/api/v1"
assert token_data["project_name"] == self.CREATE_DATA.default_project
assert mock_auth_client.created_users[0].name == expected_role

async def test_create_no_name(
Expand Down

0 comments on commit 7e061fc

Please sign in to comment.