Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration with Keycloak #24

Merged
merged 4 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading