Skip to content

Commit

Permalink
Pam poc (#198)
Browse files Browse the repository at this point in the history
* PAM POC workflow
  • Loading branch information
bolyachevets authored Dec 20, 2024
1 parent 6481657 commit 9debcdc
Show file tree
Hide file tree
Showing 12 changed files with 909 additions and 0 deletions.
41 changes: 41 additions & 0 deletions gcp/pam/cloud-functions/api-gateway/generate-pam-api-gateway.sh
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
93 changes: 93 additions & 0 deletions gcp/pam/cloud-functions/api-gateway/open_api_spec.yml
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: []
103 changes: 103 additions & 0 deletions gcp/pam/cloud-functions/pam-grant-revoke/main.py
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
39 changes: 39 additions & 0 deletions gcp/pam/cloud-functions/pam-grant-revoke/requirements.txt
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
95 changes: 95 additions & 0 deletions gcp/pam/cloud-functions/pam-request-grant-approve/main.py
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
Loading

0 comments on commit 9debcdc

Please sign in to comment.