Skip to content

Commit

Permalink
Merge pull request RedHatInsights#999 from MikelAlejoBR/RHCLOUD-30299…
Browse files Browse the repository at this point in the history
…-return-service-accounts-in-group

RHCLOUD-30299 | feature: return if service accounts are in group
  • Loading branch information
petracihalova authored Feb 21, 2024
2 parents c62304f + 9d42181 commit 43f341c
Show file tree
Hide file tree
Showing 5 changed files with 697 additions and 3 deletions.
39 changes: 39 additions & 0 deletions docs/source/specs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,18 @@
]
}
},
{
"in": "query",
"name": "service_account_client_ids",
"required": false,
"description": "By specifying a list of client IDs with this query parameter, RBAC will return an object with the specified client ID and it's matching boolean value to flag whether the client ID is present in the group or not. This query parameter cannot be used along with any other query parameter.",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"in": "query",
"name": "service_account_description",
Expand Down Expand Up @@ -1304,6 +1316,9 @@
},
{
"$ref": "#/components/schemas/ServiceAccountPagination"
},
{
"$ref": "#/components/schemas/ServiceAccountInGroupResponse"
}
]
}
Expand Down Expand Up @@ -3369,6 +3384,30 @@
}
]
},
"ServiceAccountInGroupResponse": {
"properties": {
"meta": {
"$ref": "#/components/schemas/PaginationMeta"
},
"links": {
"description": "The links object for this particular response will be empty, since there is no pagination available for the query parameter",
"type": "object",
"example": {}
},
"data": {
"description": "Object which indicates whether the given service account UUIDs in the query parameter are present in the specified group or not",
"type": "object",
"additionalProperties": {
"description": "The response is a map of the form \"UUID\": (true|false)",
"type": "boolean"
},
"example": {
"dd946f24-cfda-11ee-acb6-7b2702ff4dc8": true,
"3e728bb0-b167-013c-c455-6aa2427b506c": false
}
}
}
},
"Group": {
"required": [
"name"
Expand Down
79 changes: 77 additions & 2 deletions rbac/management/group/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

"""View for group management."""
import logging
from typing import Iterable
from typing import Iterable, Optional
from uuid import UUID

import requests
from django.conf import settings
Expand Down Expand Up @@ -70,6 +71,7 @@
PRINCIPAL_USERNAME_KEY = "principal_username"
VALID_ROLE_ORDER_FIELDS = list(RoleViewSet.ordering_fields)
ROLE_DISCRIMINATOR_KEY = "role_discriminator"
SERVICE_ACCOUNT_CLIENT_IDS_KEY = "service_account_client_ids"
SERVICE_ACCOUNT_DESCRIPTION_KEY = "service_account_description"
SERVICE_ACCOUNT_NAME_KEY = "service_account_name"
SERVICE_ACCOUNT_USERNAME_FORMAT = "service-account-{clientID}"
Expand Down Expand Up @@ -500,7 +502,7 @@ def remove_principals(self, group, principals, account=None, org_id=None):
return group

@action(detail=True, methods=["get", "post", "delete"])
def principals(self, request: Request, uuid=None):
def principals(self, request: Request, uuid: Optional[UUID] = None):
"""Get, add or remove principals from a group."""
"""
@api {get} /api/v1/groups/:uuid/principals/ Get principals for a group
Expand Down Expand Up @@ -653,6 +655,79 @@ def principals(self, request: Request, uuid=None):
# ... and return it.
response = Response(status=status.HTTP_200_OK, data=output.data)
elif request.method == "GET":
# Check if the request comes with a bunch of service account client IDs that we need to check. Since this
# query parameter is incompatible with any other query parameter, we make the checks first. That way if any
# other query parameter was specified, we simply return early.
if SERVICE_ACCOUNT_CLIENT_IDS_KEY in request.query_params:
if len(request.query_params) > 1:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": f"The '{SERVICE_ACCOUNT_CLIENT_IDS_KEY}' parameter is incompatible with"
" any other query parameter. Please, use it alone",
"source": "groups",
"status": str(status.HTTP_400_BAD_REQUEST),
}
]
},
)

# Check that the specified query parameter is not empty.
service_account_client_ids_raw = request.query_params.get(SERVICE_ACCOUNT_CLIENT_IDS_KEY).strip()
if not service_account_client_ids_raw:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": "Not a single client ID was specified for the client IDs filter",
"source": "groups",
"status": str(status.HTTP_400_BAD_REQUEST),
}
]
},
)

# Turn the received and comma separated client IDs into a manageable set.
received_client_ids: set[str] = set(service_account_client_ids_raw.split(","))

# Validate that the provided strings are actually UUIDs.
for rci in received_client_ids:
try:
UUID(rci)
except ValueError:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data={
"errors": [
{
"detail": f"The specified client ID '{rci}' is not a valid UUID",
"source": "groups",
"status": str(status.HTTP_400_BAD_REQUEST),
}
]
},
)

# Generate the report of which of the tenant's service accounts are in a group, and which
# ones are available to be added to the given group.
it_service = ITService()
result: dict = it_service.generate_service_accounts_report_in_group(
group=group, client_ids=received_client_ids
)

# Prettify the output payload and return it.
return Response(
status=status.HTTP_200_OK,
data={
"meta": {"count": len(result)},
"links": {},
"data": result,
},
)

# Get the "order_by" query parameter.
all_valid_fields = VALID_PRINCIPAL_ORDER_FIELDS + ["-" + field for field in VALID_PRINCIPAL_ORDER_FIELDS]
sort_order = None
Expand Down
17 changes: 17 additions & 0 deletions rbac/management/principal/it_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

# Constants or global variables.
LOGGER = logging.getLogger(__name__)
SERVICE_ACCOUNT_CLIENT_IDS_KEY = "service_account_client_ids"
TYPE_SERVICE_ACCOUNT = "service-account"

# IT path to fetch the service accounts.
Expand Down Expand Up @@ -401,6 +402,22 @@ def extract_client_id_service_account_username(username: str) -> uuid.UUID:
}
)

def generate_service_accounts_report_in_group(self, group: Group, client_ids: set[str]) -> dict[str, bool]:
"""Check if the given service accounts are in the specified group."""
# Fetch the service accounts from the group.
group_service_account_principals = (
group.principals.values_list("service_account_id", flat=True)
.filter(type=TYPE_SERVICE_ACCOUNT)
.filter(service_account_id__in=client_ids)
)

# Mark the specified client IDs as "present or missing" from the result set.
result: dict[str, bool] = {}
for incoming_client_id in client_ids:
result[incoming_client_id] = incoming_client_id in group_service_account_principals

return result

def _transform_incoming_payload(self, service_account_from_it_service: dict) -> dict:
"""Transform the incoming service account from IT into a dict which fits our response structure."""
service_account: dict = {}
Expand Down
Loading

0 comments on commit 43f341c

Please sign in to comment.