generated from bcgov/bcrs-template-ui
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
12 changed files
with
909 additions
and
0 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
gcp/pam/cloud-functions/api-gateway/generate-pam-api-gateway.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#!/bin/bash | ||
|
||
REGION="us-west4" | ||
|
||
declare -a projects=("mvnjri") | ||
declare -a environments=("prod") | ||
|
||
for ev in "${environments[@]}" | ||
do | ||
for ns in "${projects[@]}" | ||
do | ||
PROJECT_ID="${ns}-${ev}" | ||
echo "Processing project: ${PROJECT_ID}" | ||
|
||
if gcloud projects describe "${PROJECT_ID}" >/dev/null 2>&1; then | ||
echo "Project ${PROJECT_ID} found. Proceeding..." | ||
|
||
gcloud config set project "${PROJECT_ID}" | ||
|
||
gcloud services enable apigateway.googleapis.com --project="${PROJECT_ID}" | ||
gcloud services enable servicemanagement.googleapis.com --project="${PROJECT_ID}" | ||
gcloud services enable servicecontrol.googleapis.com --project="${PROJECT_ID}" | ||
|
||
gcloud api-gateway apis create create-pam-grant --project="${PROJECT_ID}" | ||
|
||
gcloud api-gateway api-configs create create-pam-grant \ | ||
--api=create-pam-grant \ | ||
--openapi-spec=open_api_spec.yml \ | ||
--project="${PROJECT_ID}" | ||
|
||
gcloud api-gateway gateways create create-pam-grant \ | ||
--api=create-pam-grant \ | ||
--api-config=create-pam-grant \ | ||
--location="${REGION}" \ | ||
--project="${PROJECT_ID}" | ||
|
||
else | ||
echo "Project ${PROJECT_ID} does not exist or cannot be accessed." | ||
fi | ||
done | ||
done |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
swagger: "2.0" | ||
info: | ||
title: PAM Grant Request API | ||
description: API for creating Privileged Access Manager (PAM) grant requests. | ||
version: 1.0.0 | ||
host: northamerica-northeast1-mvnjri-prod.cloudfunctions.net | ||
basePath: / | ||
schemes: | ||
- https | ||
paths: | ||
/pam-request-grant-create: | ||
post: | ||
operationId: createPamGrantRequest | ||
summary: Create a PAM Grant Request | ||
description: Processes a PAM grant request with specific entitlement, assignee, and duration. | ||
parameters: | ||
- in: body | ||
name: body | ||
description: Request payload containing assignee, entitlement, duration, and robot flag. | ||
required: true | ||
schema: | ||
type: object | ||
properties: | ||
assignee: | ||
type: string | ||
description: Email of the assignee. | ||
example: [email protected] | ||
entitlement: | ||
type: string | ||
description: Role entitlement for the project. | ||
example: roleitops | ||
duration: | ||
type: integer | ||
description: Duration of the grant in minutes. | ||
example: 60 | ||
robot: | ||
type: boolean | ||
description: Indicates if the request is from a service account. | ||
example: false | ||
responses: | ||
200: | ||
description: Successful processing of the PAM grant request. | ||
schema: | ||
type: object | ||
properties: | ||
status: | ||
type: string | ||
example: success | ||
message: | ||
type: string | ||
example: PAM grant request processed successfully | ||
400: | ||
description: Missing or invalid request payload. | ||
schema: | ||
type: object | ||
properties: | ||
status: | ||
type: string | ||
example: error | ||
message: | ||
type: string | ||
example: Missing required fields | ||
401: | ||
description: Unauthorized request. | ||
schema: | ||
type: object | ||
properties: | ||
status: | ||
type: string | ||
example: error | ||
message: | ||
type: string | ||
example: "Unauthorized: User is not part of the project" | ||
500: | ||
description: Internal server error. | ||
schema: | ||
type: object | ||
properties: | ||
status: | ||
type: string | ||
example: error | ||
message: | ||
type: string | ||
example: An error occurred while processing the request | ||
x-google-backend: | ||
address: https://northamerica-northeast1-mvnjri-prod.cloudfunctions.net/pam-request-grant-create | ||
securityDefinitions: | ||
api_key: | ||
type: apiKey | ||
name: key | ||
in: query | ||
security: | ||
- api_key: [] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import logging | ||
import os | ||
import base64 | ||
import json | ||
from googleapiclient.discovery import build | ||
from google.cloud import resourcemanager_v3, scheduler_v1 | ||
|
||
|
||
instance_connection_name = os.environ['DB_INSTANCE_CONNECTION_NAME'] | ||
project_number = os.environ['PROJECT_NUMBER'] | ||
|
||
|
||
def remove_iam_binding(project_id, role, email): | ||
client = resourcemanager_v3.ProjectsClient() | ||
project_name = f"projects/{project_id}" | ||
def modify_policy_remove_member(policy): | ||
"""Callback to remove a member from a specific role.""" | ||
for binding in policy.bindings: | ||
if role in binding.role and f"user:{email}" in binding.members and binding.condition: | ||
binding.members.remove(f"user:{email}") | ||
if not binding.members: | ||
policy.bindings.remove(binding) | ||
|
||
return policy | ||
|
||
try: | ||
policy_request = { | ||
"resource": project_name, | ||
"options": {"requested_policy_version": 3}, | ||
} | ||
|
||
policy = client.get_iam_policy(request=policy_request) | ||
updated_policy = modify_policy_remove_member(policy) | ||
client.set_iam_policy( | ||
request={"resource": project_name, "policy": updated_policy} | ||
) | ||
logging.info(f"IAM policy updated for project {project_id}") | ||
except Exception as e: | ||
logging.error(f"Error removing IAM binding: {str(e)}") | ||
raise | ||
|
||
|
||
def remove_scheduler_job(full_job_name): | ||
client = scheduler_v1.CloudSchedulerClient() | ||
|
||
try: | ||
client.delete_job(name=full_job_name) | ||
logging.info(f"Scheduler job {full_job_name} deleted successfully!") | ||
except Exception as e: | ||
logging.error(f"Error deleting scheduler job {full_job_name}: {str(e)}") | ||
raise | ||
|
||
|
||
def remove_iam_user(project_id, instance_name, iam_user_email): | ||
service = build("sqladmin", "v1beta4") | ||
request = service.users().delete( | ||
project=project_number, | ||
instance=instance_name.split(":")[-1], | ||
name=iam_user_email | ||
) | ||
response = request.execute() | ||
logging.info(f"IAM user {iam_user_email} removed successfully!") | ||
return response | ||
|
||
def pam_event_handler(event, context): | ||
try: | ||
logging.info(f"Received event: {event}") | ||
pubsub_message = base64.b64decode(event['data']).decode('utf-8') | ||
logging.info(f"Decoded message: {pubsub_message}") | ||
|
||
request_json = json.loads(pubsub_message) | ||
|
||
email = request_json.get('user', {}) | ||
|
||
if not email: | ||
logging.warning("Email not found in the event") | ||
return "Email not found in the Pub/Sub message payload", 400 | ||
|
||
remove_iam_user(project_number, instance_connection_name, email) | ||
|
||
grant = request_json.get('grant', {}) | ||
|
||
if not grant: | ||
logging.warning("Role grant not found in the event") | ||
return "Role not found in the Pub/Sub message payload", 400 | ||
|
||
robot = request_json.get('robot', {}) | ||
|
||
if robot: | ||
remove_iam_binding(project_number, grant, email) | ||
|
||
job_name = request_json.get('job_name', {}) | ||
|
||
remove_scheduler_job(job_name) | ||
|
||
return f"Successfully processed the event for {email}", 200 | ||
|
||
except KeyError as e: | ||
logging.error(f"Missing payload key: {str(e)}") | ||
return f"Missing key in the payload: {str(e)}", 400 | ||
except Exception as e: | ||
logging.error(f"Failed to process the event: {str(e)}") | ||
return f"Failed to process the event: {str(e)}", 500 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
cachetools==5.5.0 | ||
certifi==2024.12.14 | ||
charset-normalizer==3.4.0 | ||
click==8.1.7 | ||
cloudevents==1.11.0 | ||
deprecation==2.1.0 | ||
flask==2.2.5 | ||
functions-framework==3.8.1 | ||
google-api-core==2.24.0 | ||
google-api-python-client==2.155.0 | ||
google-auth-httplib2==0.2.0 | ||
google-auth==2.37.0 | ||
google-cloud-resource-manager==1.14.0 | ||
google-cloud-scheduler==2.15.0 | ||
googleapis-common-protos==1.66.0 | ||
grpc-google-iam-v1==0.13.1 | ||
grpcio-status==1.68.1 | ||
grpcio==1.68.1 | ||
gunicorn==23.0.0 | ||
httplib2==0.22.0 | ||
idna==3.10 | ||
importlib-metadata==6.7.0 | ||
itsdangerous==2.1.2 | ||
jinja2==3.1.4 | ||
markupsafe==2.1.5 | ||
packaging==24.0 | ||
proto-plus==1.25.0 | ||
protobuf==5.29.1 | ||
pyasn1-modules==0.4.1 | ||
pyasn1==0.6.1 | ||
pyparsing==3.2.0 | ||
requests==2.32.3 | ||
rsa==4.9 | ||
typing-extensions==4.7.1 | ||
uritemplate==4.1.1 | ||
urllib3==2.2.3 | ||
watchdog==3.0.0 | ||
werkzeug==2.2.3 | ||
zipp==3.15.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import base64 | ||
import json | ||
import logging | ||
import requests | ||
import os | ||
from google.cloud import secretmanager | ||
|
||
project_number = os.environ['PROJECT_NUMBER'] | ||
pam_api_secret_id = os.environ['PAM_API_KEY_SECRET_ID'] | ||
pam_api_secret_name = f"projects/{project_number}/secrets/{pam_api_secret_id}/versions/latest" | ||
client = secretmanager.SecretManagerServiceClient() | ||
key_response = client.access_secret_version(name=pam_api_secret_name) | ||
api_key = key_response.payload.data.decode("UTF-8") | ||
pam_url_secret_id = os.environ['PAM_API_URL_SECRET_ID'] | ||
pam_url_secret_name = f"projects/{project_number}/secrets/{pam_url_secret_id}/versions/latest" | ||
client = secretmanager.SecretManagerServiceClient() | ||
url_response = client.access_secret_version(name=pam_url_secret_name) | ||
api_url = url_response.payload.data.decode("UTF-8") | ||
|
||
def pam_event_handler(event, context): | ||
try: | ||
logging.info(f"Received event: {event}") | ||
pubsub_message = base64.b64decode(event['data']).decode('utf-8') | ||
logging.info(f"Decoded message: {pubsub_message}") | ||
|
||
request_json = json.loads(pubsub_message) | ||
|
||
email = ( | ||
request_json.get("protoPayload", {}) | ||
.get("metadata", {}) | ||
.get("updatedGrant", {}) | ||
.get("requester", {}) | ||
) | ||
|
||
if not email: | ||
logging.error("Email not found in timeline's approved event") | ||
return "Email not found in the Pub/Sub message payload", 400 | ||
|
||
role_bindings = ( | ||
request_json.get("protoPayload", {}) | ||
.get("metadata", {}) | ||
.get("updatedGrant", {}) | ||
.get("privilegedAccess", {}) | ||
.get("gcpIamAccess", {}) | ||
.get("roleBindings", []) | ||
) | ||
|
||
role = None | ||
if role_bindings: | ||
role = role_bindings[0].get("role") | ||
|
||
if not role: | ||
logging.error("Role not found in roleBindings") | ||
return "Role not found in the Pub/Sub message payload", 400 | ||
|
||
role_name = role.split('/')[-1] | ||
|
||
requested_duration = ( | ||
request_json.get("protoPayload", {}) | ||
.get("metadata", {}) | ||
.get("updatedGrant", {}) | ||
.get("requestedDuration", None) | ||
) | ||
|
||
if not requested_duration: | ||
logging.error("Duration not found in roleBindings") | ||
return "Duration not found in the Pub/Sub message payload", 400 | ||
|
||
minutes = int(''.join(filter(str.isdigit, requested_duration))) // 60 | ||
|
||
payload = { | ||
"assignee": email, | ||
"entitlement": role_name, | ||
"duration": minutes, | ||
"robot": False | ||
} | ||
|
||
logging.warning(f"Constructed payload: {payload}") | ||
|
||
headers = { | ||
"Content-Type": "application/json", | ||
"X-API-Key": api_key | ||
} | ||
response = requests.post(api_url, json=payload, headers=headers) | ||
|
||
logging.warning(f"Response from target Cloud Function: {response.status_code}, {response.text}") | ||
|
||
if response.status_code == 200: | ||
return "Payload successfully sent to target Cloud Function", 200 | ||
else: | ||
return f"Failed to send payload: {response.status_code}, {response.text}", 500 | ||
|
||
except Exception as e: | ||
logging.error(f"Error processing Pub/Sub event: {e}") | ||
return f"Error: {str(e)}", 500 |
Oops, something went wrong.