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

RDS IAM #1

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
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
38 changes: 38 additions & 0 deletions .github/workflows/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This is a comment.
# Each line is a file pattern followed by one or more owners.

# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
# @global-owner1 and @global-owner2 will be requested for
# review when someone opens a pull request.
#* @SPHTech/devops

# Order is important; the last matching pattern takes the most
# precedence. When someone opens a pull request that only
# modifies JS files, only @js-owner and not the global
# owner(s) will be requested for a review.
#*.js @js-owner

# You can also use email addresses if you prefer. They'll be
# used to look up users just like we do for commit author
# emails.
#*.go [email protected]

# In this example, @doctocat owns any files in the build/logs
# directory at the root of the repository and any of its
# subdirectories.
#/build/logs/ @doctocat

# The `docs/*` pattern will match files like
# `docs/getting-started.md` but not further nested files like
# `docs/build-app/troubleshooting.md`.
#docs/* [email protected]

# In this example, @octocat owns any file in an apps directory
# anywhere in your repository.
#apps/ @octocat

# In this example, @doctocat owns any file in the `/docs`
# directory in the root of your repository.
#/docs/ @doctocat
* @sphtech-platform/platform-engineering
20 changes: 14 additions & 6 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ resource "aws_lambda_function" "default" {
PROVISION_DB_NAME = var.db_name
PROVISION_USER = var.db_user
PROVISION_USER_PASSWORD = var.db_user_password
PROVISION_USER_ROLE_TO_GRANT = var.db_user_role_to_grant
PROVISION_USER_PASSWORD_SSM_PARAM = var.db_user_password_ssm_param
CONNECT_DB_NAME = var.db_connect_name
}
}

Expand Down Expand Up @@ -243,6 +245,15 @@ data "aws_iam_policy_document" "default_permissions" {
]
resources = ["*"]
}

statement {
effect = "Allow"
actions = [
"rds-db:connect"
]
resources = ["arn:aws:rds-db:${var.region}:${var.account_id}:dbuser:${var.db_cluster_id}/root"]
}

}

data "aws_iam_policy_document" "lambda_kms_permissions" {
Expand Down Expand Up @@ -329,11 +340,8 @@ data "aws_iam_policy_document" "user_password_kms_permissions" {
}
}

module "aggregated_policy" {
source = "cloudposse/iam-policy-document-aggregator/aws"
version = "0.8.0"

source_documents = compact([
data "aws_iam_policy_document" "aggregated_policy" {
override_policy_documents = compact([
join("", data.aws_iam_policy_document.default_permissions.*.json),
join("", data.aws_iam_policy_document.lambda_kms_permissions.*.json),
join("", data.aws_iam_policy_document.master_password_ssm_permissions.*.json),
Expand Down Expand Up @@ -361,7 +369,7 @@ resource "aws_iam_policy" "default" {
path = "/"
description = "IAM policy to control access of Lambda function to AWS resources"

policy = module.aggregated_policy.result_document
policy = data.aws_iam_policy_document.aggregated_policy.json
}

resource "aws_iam_role_policy_attachment" "default_permissions" {
Expand Down
Binary file modified packaged/rds-lambda-db-provisioner.zip
Binary file not shown.
84 changes: 58 additions & 26 deletions source-code/main.py → source-code/main-backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class DBInfo:
provision_db_name: str
provision_user: str
provision_user_password: str
provision_user_role_to_grant: str


class DBProvisioner(object):
Expand Down Expand Up @@ -84,10 +85,12 @@ def _get_pg_databases_names(cursor) -> List[str]:
return rows

def provision_postgres_db(self, info: DBInfo):
self.logger.info("Connecting to '{}' database as user '{}'".format(info.connect_db_name, info.master_username))
self.logger.info("Connecting to '{}' database as user '{}'".format(
info.connect_db_name, info.master_username))
try:
connection_string = "host=%s user=%s password=%s dbname=%s" % \
(info.host, info.master_username, info.master_password, info.connect_db_name)
(info.host, info.master_username,
info.master_password, info.connect_db_name)
connection = psycopg2.connect(connection_string)
connection.autocommit = True
except Exception as e:
Expand All @@ -103,36 +106,53 @@ def provision_postgres_db(self, info: DBInfo):
if info.provision_user:
usernames = self._get_pg_usernames(cursor)
if info.provision_user in usernames:
self.logger.warning("User '{}' won't be created because it already exists".format(info.provision_user))
self.logger.warning(
"User '{}' won't be created because it already exists".format(info.provision_user))
else:
self.logger.info("Creating user '{}'".format(info.provision_user))

query = "CREATE USER {} WITH PASSWORD '{}' CREATEDB;".format(info.provision_user,
info.provision_user_password)
query += "SET ROLE {};".format(info.provision_user)
self.logger.info(
"Creating user '{}'".format(info.provision_user))

# query = "CREATE USER {} WITH LOGIN PASSWORD '{}';".format(info.provision_user,
# info.provision_user_password)
# query += "SET ROLE {};".format(info.provision_user)
# query += "GRANT {} TO {};".format(
# info.provision_user_role_to_grant, info.master_username)
query = "CREATE USER {};".format(info.provision_user)
query += "GRANT {} TO {};".format(
info.provision_user_role_to_grant, info.provision_user)
cursor.execute(query)

self.logger.info("User '{}' successfully created".format(info.provision_user))
self.logger.info(
"User '{}' successfully created".format(info.provision_user))

databases_names = self._get_pg_databases_names(cursor)

if info.provision_db_name in databases_names:
self.logger.warning(
"Database '{}' won't be created because it already exists".format(info.provision_db_name))
else:
self.logger.info("Creating database '{}'".format(info.provision_db_name))
self.logger.info("Creating database '{}'".format(
info.provision_db_name))

query = "CREATE DATABASE {};".format(info.provision_db_name)
cursor.execute(query)

self.logger.info("Database '{}' successfully created".format(info.provision_db_name))
self.logger.info("Database '{}' successfully created".format(
info.provision_db_name))

if info.provision_user:
query = "SET ROLE {};".format(info.master_username)
query += "GRANT {} TO {};".format(info.provision_user, info.master_username)
# query = "SET ROLE {};".format(info.master_username)
# query = "SET ROLE {};".format(info.provision_user)
# query += "GRANT {} TO {};".format(
# info.provision_user, info.master_username)
# query += "GRANT {} TO {};".format(
# info.provision_user_role_to_grant, info.provision_user)
query += "GRANT ALL PRIVILEGES ON DATABASE {} TO {};".format(
info.provision_db_name, info.provision_user)
cursor.execute(query)

self.logger.info("User '{}' is now member of '{}' role".format(info.master_username, info.provision_user))
self.logger.info("User '{}' is now member of '{}' role".format(
info.provision_user, info.provision_user_role_to_grant))

cursor.close()
connection.close()
Expand All @@ -156,7 +176,8 @@ def _get_mysql_databases_names(cursor) -> List[str]:
return rows

def provision_mysql_db(self, info: DBInfo):
self.logger.info("Connecting to '{}' database as user '{}'".format(info.connect_db_name, info.master_username))
self.logger.info("Connecting to '{}' database as user '{}'".format(
info.connect_db_name, info.master_username))
try:
connection = pymysql.connect(
host=info.host,
Expand All @@ -180,9 +201,11 @@ def provision_mysql_db(self, info: DBInfo):
if info.provision_user:
usernames = self._get_mysql_usernames(cursor)
if info.provision_user in usernames:
self.logger.warning("User '{}' won't be created because it already exists".format(info.provision_user))
self.logger.warning(
"User '{}' won't be created because it already exists".format(info.provision_user))
else:
self.logger.info("Creating user '{}'".format(info.provision_user))
self.logger.info(
"Creating user '{}'".format(info.provision_user))

query = "CREATE USER '{}'@'localhost' IDENTIFIED BY '{}';".format(
info.provision_user,
Expand All @@ -195,7 +218,8 @@ def provision_mysql_db(self, info: DBInfo):
)
cursor.execute(query)

self.logger.info("User '{}' successfully created".format(info.provision_user))
self.logger.info(
"User '{}' successfully created".format(info.provision_user))

databases_names = self._get_mysql_databases_names(cursor)

Expand All @@ -204,7 +228,8 @@ def provision_mysql_db(self, info: DBInfo):
info.provision_db_name
))
else:
self.logger.info("Creating database '{}'".format(info.provision_db_name))
self.logger.info("Creating database '{}'".format(
info.provision_db_name))

query = "CREATE DATABASE {};".format(info.provision_db_name)
cursor.execute(query)
Expand Down Expand Up @@ -233,19 +258,22 @@ def provision_mysql_db(self, info: DBInfo):
info.provision_user,
))

self.logger.info("Database '{}' successfully created".format(info.provision_db_name))
self.logger.info("Database '{}' successfully created".format(
info.provision_db_name))

cursor.close()
connection.close()

def provision(self):
instance = self.describe_instance(os.environ.get('DB_INSTANCE_ID'))

master_password_ssm_param_name = os.environ.get('DB_MASTER_PASSWORD_SSM_PARAM')
master_password_ssm_param_name = os.environ.get(
'DB_MASTER_PASSWORD_SSM_PARAM')
master_password = self.get_ssm_parameter_value(master_password_ssm_param_name) \
if master_password_ssm_param_name else os.environ.get('DB_MASTER_PASSWORD')

user_password_ssm_param_name = os.environ.get('PROVISION_USER_PASSWORD_SSM_PARAM')
user_password_ssm_param_name = os.environ.get(
'PROVISION_USER_PASSWORD_SSM_PARAM')
user_password = self.get_ssm_parameter_value(user_password_ssm_param_name) \
if user_password_ssm_param_name else os.environ.get('PROVISION_USER_PASSWORD')

Expand All @@ -254,19 +282,23 @@ def provision(self):
port=instance.get('Endpoint').get('Port'),
master_username=instance.get('MasterUsername'),
master_password=master_password,
connect_db_name=os.environ.get('CONNECT_DB_NAME', instance.get('DBName')),
connect_db_name=os.environ.get(
'CONNECT_DB_NAME', instance.get('DBName')),
provision_db_name=os.environ.get('PROVISION_DB_NAME'),
provision_user=os.environ.get('PROVISION_USER'),
provision_user_password=user_password
provision_user_password=user_password,
provision_user_role_to_grant=os.environ.get(
'PROVISION_USER_ROLE_TO_GRANT'),
)

engine: str = instance.get('Engine')
if engine == 'postgres':
if engine == 'postgres' or engine == 'aurora-postgresql':
self.provision_postgres_db(db_info)
elif engine == 'mysql':
self.provision_mysql_db(db_info)
else:
raise NotImplementedError('{} engine is not supported'.format(engine))
raise NotImplementedError(
'{} engine is not supported'.format(engine))


def lambda_handler(event, context):
Expand Down
Loading