Skip to content

Commit

Permalink
Integration with Keycloak (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
rconway authored Feb 16, 2024
1 parent a99ace4 commit 6283cf5
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Build and publish Docker image
on:
push:
branches: [ master ]
branches: [ master, integration ]
tags:
- "*"
pull_request:
Expand Down
21 changes: 14 additions & 7 deletions workspace_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,20 @@
S3_ENDPOINT = os.environ["S3_ENDPOINT"]
S3_REGION = os.environ["S3_REGION"]
BUCKET_ENDPOINT_URL = os.environ["BUCKET_ENDPOINT_URL"]
PEP_BASE_URL = os.environ.get("PEPBaseUrl", "http://workspace-api-pep:5576")
AUTO_PROTECTION_ENABLED = "True" == os.environ.get("AUTO_PROTECTION_ENABLED", "True")
# TODO: whitelistings = list of strings (applied to helm chart)

# Gluu integration
GLUU_INTEGRATION_ENABLED = os.environ.get("GLUU_INTEGRATION_ENABLED", "false").lower() == "true"
PEP_BASE_URL = os.environ.get("PEP_BASE_URL", "http://workspace-api-pep:5576")
UMA_CLIENT_SECRET_NAME = os.environ["UMA_CLIENT_SECRET_NAME"]
UMA_CLIENT_SECRET_NAMESPACE = os.environ["UMA_CLIENT_SECRET_NAMESPACE"]

# Keycloak integration
KEYCLOAK_INTEGRATION_ENABLED = os.environ.get("KEYCLOAK_INTEGRATION_ENABLED", "false").lower() == "true"
KEYCLOAK_URL = os.environ.get("KEYCLOAK_URL", "http://identity-keycloak.um.svc.cluster.local:8080")
KEYCLOAK_REALM = os.environ.get("KEYCLOAK_REALM", "master")
IDENTITY_API_URL = os.environ.get("IDENTITY_API_URL", "http://identity-api.um.svc.cluster.local:8080")
WORKSPACE_API_CLIENT_ID = os.environ.get("WORKSPACE_API_CLIENT_ID", "workspace-api")
DEFAULT_IAM_CLIENT_SECRET = os.environ.get("DEFAULT_IAM_CLIENT_SECRET", "changeme")

# registration endpoint variables
REDIS_SERVICE_NAME = os.environ.get("REDIS_SERVICE_NAME", "vs-redis-master")
Expand Down Expand Up @@ -57,10 +68,6 @@

REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379"))

# Guard specific values
UMA_CLIENT_SECRET_NAME = os.environ["UMA_CLIENT_SECRET_NAME"]
UMA_CLIENT_SECRET_NAMESPACE = os.environ["UMA_CLIENT_SECRET_NAMESPACE"]

HARBOR_URL = os.environ["HARBOR_URL"]
HARBOR_ADMIN_USERNAME = os.environ["HARBOR_ADMIN_USERNAME"]
HARBOR_ADMIN_PASSWORD = os.environ["HARBOR_ADMIN_PASSWORD"]
Expand Down
188 changes: 166 additions & 22 deletions workspace_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ async def create_workspace(

workspace_name = workspace_name_from_preferred_name(data.preferred_name)
bucket_endpoint_url = config.BUCKET_ENDPOINT_URL
pep_base_url = config.PEP_BASE_URL
auto_protection_enabled = config.AUTO_PROTECTION_ENABLED

if namespace_exists(workspace_name):
raise HTTPException(
Expand Down Expand Up @@ -116,16 +114,27 @@ async def create_workspace(
elif 400 <= response.status_code <= 511:
raise HTTPException(status_code=response.status_code)

create_uma_client_credentials_secret(workspace_name=workspace_name)

create_harbor_user(workspace_name=workspace_name)

if auto_protection_enabled:
register_workspace_api_protection(
if config.GLUU_INTEGRATION_ENABLED:
create_uma_client_credentials_secret(workspace_name=workspace_name)
register_workspace_api_gluu_protection(
authorization=authorization,
creation_data=data,
workspace_name=workspace_name,
base_url=config.PEP_BASE_URL,
)

if config.KEYCLOAK_INTEGRATION_ENABLED:
register_workspace_api_keycloak_protection(
authorization=authorization,
creation_data=data,
workspace_name=workspace_name,
base_url=pep_base_url,
keycloak_url=config.KEYCLOAK_URL,
realm=config.KEYCLOAK_REALM,
identity_api_url=config.IDENTITY_API_URL,
workspace_api_client_id = config.WORKSPACE_API_CLIENT_ID,
new_client_secret = config.DEFAULT_IAM_CLIENT_SECRET,
)

background_tasks.add_task(
Expand All @@ -137,7 +146,7 @@ async def create_workspace(
return {"name": workspace_name}


def register_workspace_api_protection(
def register_workspace_api_gluu_protection(
authorization: Union[str, None], creation_data: WorkspaceCreate,
workspace_name: str, base_url: str
) -> None:
Expand All @@ -158,6 +167,138 @@ def register_workspace_api_protection(
pep_response.raise_for_status()


def register_workspace_api_keycloak_protection(
authorization: Union[str, None], creation_data: WorkspaceCreate,
workspace_name: str, keycloak_url: str, realm: str, identity_api_url: str,
workspace_api_client_id: str, new_client_secret: str
) -> None:
pass
# Steps...
# 1. Protect workspace-api/workspaces/{workspace_name} for {creation_data.preferred_name}
# 2. Create new client '{workspace_name}' with /* protected for user 'data.default_owner'

logger.info(f"Auth header is '{authorization}'")

#--------------------------------------------------------------------------
# Protect the URI of the new workspace in the Workspace API
#--------------------------------------------------------------------------
logger.info(f"Protect Workspace API URI '/workspaces/{workspace_name}' for user '{creation_data.default_owner}'")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": authorization
}
body = [
{
"name": workspace_name,
"uris": [ f"/workspaces/{workspace_name}/*" ],
"scopes": [ "view" ],
"permissions": {
"user": [ creation_data.default_owner ]
}
}
]
response = requests.post(f"{identity_api_url}/{workspace_api_client_id}/resources", headers=headers, json=body)
if response.status_code == 200 or response.status_code == 409:
logger.info(f" [Protected Workspace API] Completed with response: {response.status_code}")
else:
logger.error(f" [Protected Workspace API] Failed with response: {response.status_code}")
response.raise_for_status()

#--------------------------------------------------------------------------
# Create a new Keycloak client to protect the new workspace services
#--------------------------------------------------------------------------
logger.info(f"Create a new Keycloak client for new workspace '{workspace_name}' with protected access for user '{creation_data.default_owner}'")

#--------------------------------------------------------------------------
# Step 1 - Create the client with permissions
#--------------------------------------------------------------------------
logger.info("[step 1] Create the client with permissions...")
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": authorization
}
body = {
"clientId": workspace_name,
"secret": new_client_secret,
"name": f"Workspace {workspace_name} Gatekeeper",
"resources": [
{
"name": creation_data.default_owner,
"uris": [ "/*" ],
"scopes": [ "view" ],
"permissions": {
"user": [ creation_data.default_owner ]
}
}
],
"description": f"Client to be used by Workspace {workspace_name} Gatekeeper"
}
response = requests.post(f"{identity_api_url}/clients", headers=headers, json=body)
if response.status_code == 200 or response.status_code == 409:
logger.info(f" [Create Client] Completed with response: {response.status_code}\n{response.text}")
created_client_details = response.json()
if "client" in created_client_details:
new_client_uuid = created_client_details["client"]
logger.info(f" [Create Client] New client created with UUID: {new_client_uuid}")
else:
logger.error(f" [Create Client] Failed with response: {response.status_code}")
response.raise_for_status()

#--------------------------------------------------------------------------
# Step 2 - Update the Default Resource with the 'view' scope
#--------------------------------------------------------------------------
logger.info("[step 2] Update the Default Resource with the 'view' scope...")
# Get the UUID of the new client
if not new_client_uuid:
response = requests.get(f"{keycloak_url}/admin/realms/{realm}/clients", headers=headers)
if response.ok:
client_list = response.json()
for client in client_list:
if "clientId" in client:
if client["clientId"] == workspace_name:
new_client_uuid = client["id"]
break

if new_client_uuid:
# Get the UUID of the Default Resource
response = requests.get(f"{keycloak_url}/admin/realms/{realm}/clients/{new_client_uuid}/authz/resource-server/resource", headers=headers)
if response.ok:
resource_list = response.json()
for resource in resource_list:
if "name" in resource:
if resource["name"] == "Default Resource":
default_resource_uuid = resource["_id"]
break
else:
logger.error(f" [Update Default Resource] Get Default Resource UUID failed with response: {response.status_code}\n{response.text}")
response.raise_for_status()

if default_resource_uuid:
# Get the details of the Default Resource, and add the 'view' scope
response = requests.get(f"{keycloak_url}/admin/realms/{realm}/clients/{new_client_uuid}/authz/resource-server/resource/{default_resource_uuid}", headers=headers)
if response.ok:
logger.info(f" [Update Default Resource] Get Default Resource details completed with response: {response.status_code}\n{response.text}")
default_resource_details = response.json()
if "scopes" not in default_resource_details:
default_resource_details["scopes"] = []
if "view" not in default_resource_details["scopes"]:
default_resource_details["scopes"].append("view")
# Update the Default Resource
response = requests.put(f"{keycloak_url}/admin/realms/{realm}/clients/{new_client_uuid}/authz/resource-server/resource/{default_resource_uuid}", headers=headers, json=default_resource_details)
if response.ok:
logger.info(f" [Update Default Resource] Update completed with response: {response.status_code}")
else:
logger.error(f" [Update Default Resource] Update failed with response: {response.status_code}\n{response.text}")
response.raise_for_status()
else:
logger.error(f" [Update Default Resource] Get Default Resource details failed with response: {response.status_code}\n{response.text}")
response.raise_for_status()
else:
logger.error(f" [Update Default Resource] Get Default Resource UUID failed parsing '_id' (UUID) from response: {response.status_code}\n{response.text}")


def create_bucket_secret(workspace_name: str, credentials: Dict[str, Any]) -> None:

logger.info(f"Creating secret for namespace {workspace_name}")
Expand Down Expand Up @@ -240,20 +381,23 @@ def create_harbor_user(workspace_name: str) -> None:


def create_uma_client_credentials_secret(workspace_name: str):
logger.info("Creating uma client credentials secret")
original_secret = k8s_client.CoreV1Api().read_namespaced_secret(
name=config.UMA_CLIENT_SECRET_NAME,
namespace=config.UMA_CLIENT_SECRET_NAMESPACE,
)
k8s_client.CoreV1Api().create_namespaced_secret(
namespace=workspace_name,
body=k8s_client.V1Secret(
metadata=k8s_client.V1ObjectMeta(
name=config.UMA_CLIENT_SECRET_NAME,
if config.UMA_CLIENT_SECRET_NAME and config.UMA_CLIENT_SECRET_NAMESPACE:
logger.info("Creating uma client credentials secret")
original_secret = k8s_client.CoreV1Api().read_namespaced_secret(
name=config.UMA_CLIENT_SECRET_NAME,
namespace=config.UMA_CLIENT_SECRET_NAMESPACE,
)
k8s_client.CoreV1Api().create_namespaced_secret(
namespace=workspace_name,
body=k8s_client.V1Secret(
metadata=k8s_client.V1ObjectMeta(
name=config.UMA_CLIENT_SECRET_NAME,
),
data=original_secret.data,
),
data=original_secret.data,
),
)
)
else:
logger.warning("Not creating uma client credentials secret - due to missing input values")


def wait_for_namespace_secret(workspace_name) -> V1Secret:
Expand Down Expand Up @@ -291,7 +435,7 @@ def install_workspace_phase2(workspace_name, default_owner=None, patch=False) ->
)
for item in response["items"]:
try:
if item["spec"]["chart"]["spec"]["chart"] == "resource-guard":
if item["spec"]["chart"]["spec"]["chart"] == "identity-gatekeeper":
default_owner = item["spec"]["values"]["global"]["default_owner"]
break

Expand Down

0 comments on commit 6283cf5

Please sign in to comment.