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

Add resource MySQLUserGrant to grant user privileges #8

Open
wants to merge 6 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
.noseids
*.zip
deps/
.vscode/
.vscode/*
!.vscode/settings.json
# Byte-compiled / optimized / DLL files
.pytest_cache/
__pycache__/
Expand Down
4 changes: 2 additions & 2 deletions .release
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
release=0.2.3
tag=v0.2.3
release=0.2.4
tag=v0.2.4
pre_tag_command=sed -i '' -e 's/lambdas\/cfn-mysql-user-provider.*\.zip/lambdas\/cfn-mysql-user-provider-@@RELEASE@@.zip/g' cloudformation/cfn-resource-provider.yaml README.md
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"python.venvPath": "${workspaceFolder}/.venv",
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true,
"python.pythonPath": ".venv/bin/python3"
}
13 changes: 9 additions & 4 deletions Dockerfile.lambda
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
FROM python:3.6
FROM python:3.7

RUN apt-get update && apt-get install -y zip

WORKDIR /lambda

ADD requirements.txt /tmp
RUN pip install --quiet -t /lambda -r /tmp/requirements.txt
ADD setup.cfg /tmp
ADD setup.py /tmp
ADD src/ /tmp/src
RUN pip install --quiet -t /lambda /tmp

ADD mysql_user_provider.py /lambda/

ADD src/ /lambda/
RUN find /lambda -type d | xargs -n 1 -I {} chmod ugo+rx "{}" && \
find /lambda -type f | xargs -n 1 -I {} chmod ugo+r "{}"

Expand Down
81 changes: 43 additions & 38 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ NAME=cfn-mysql-user-provider
AWS_REGION=eu-central-1
S3_BUCKET_PREFIX=binxio-public
S3_BUCKET=$(S3_BUCKET_PREFIX)-$(AWS_REGION)
S3_COPY_ARGS="--acl public-read"

ALL_REGIONS=$(shell printf "import boto3\nprint('\\\n'.join(map(lambda r: r['RegionName'], boto3.client('ec2').describe_regions()['Regions'])))\n" | python | grep -v '^$(AWS_REGION)$$')

Expand All @@ -12,32 +13,30 @@ help:
@echo 'make release - builds a zip file and deploys it to s3.'
@echo 'make clean - the workspace.'
@echo 'make test - execute the tests, requires a working AWS connection.'
@echo 'make deploy - lambda to bucket $(S3_BUCKET)'
@echo 'make deploy - lambda to bucket $(S3_BUCKET)'
@echo 'make deploy-all-regions - lambda to all regions with bucket prefix $(S3_BUCKET_PREFIX)'
@echo 'make deploy-provider - deploys the provider.'
@echo 'make delete-provider - deletes the provider.'
@echo 'make demo - deploys the provider and the demo cloudformation stack.'
@echo 'make delete-demo - deletes the demo cloudformation stack.'

deploy: target/$(NAME)-$(VERSION).zip
aws s3 --region $(AWS_REGION) \
cp --acl \
public-read target/$(NAME)-$(VERSION).zip \
aws s3 cp ${S3_COPY_ARGS} \
target/$(NAME)-$(VERSION).zip \
s3://$(S3_BUCKET)/lambdas/$(NAME)-$(VERSION).zip
aws s3 --region $(AWS_REGION) \
cp --acl public-read \
aws s3 cp ${S3_COPY_ARGS} \
s3://$(S3_BUCKET)/lambdas/$(NAME)-$(VERSION).zip \
s3://$(S3_BUCKET)/lambdas/$(NAME)-latest.zip

deploy-all-regions: deploy
@for REGION in $(ALL_REGIONS); do \
echo "copying to region $$REGION.." ; \
aws s3 --region $(AWS_REGION) \
cp --acl public-read \
cp ${S3_COPY_ARGS} \
s3://$(S3_BUCKET_PREFIX)-$(AWS_REGION)/lambdas/$(NAME)-$(VERSION).zip \
s3://$(S3_BUCKET_PREFIX)-$$REGION/lambdas/$(NAME)-$(VERSION).zip; \
aws s3 --region $$REGION \
cp --acl public-read \
cp ${S3_COPY_ARGS} \
s3://$(S3_BUCKET_PREFIX)-$$REGION/lambdas/$(NAME)-$(VERSION).zip \
s3://$(S3_BUCKET_PREFIX)-$$REGION/lambdas/$(NAME)-latest.zip; \
done
Expand All @@ -46,57 +45,63 @@ do-push: deploy

do-build: target/$(NAME)-$(VERSION).zip

target/$(NAME)-$(VERSION).zip: src/*.py requirements.txt
target/$(NAME)-$(VERSION).zip: mysql_user_provider.py
mkdir -p target
docker build --build-arg ZIPFILE=$(NAME)-$(VERSION).zip -t $(NAME)-lambda:$(VERSION) -f Dockerfile.lambda . && \
ID=$$(docker create $(NAME)-lambda:$(VERSION) /bin/true) && \
docker export $$ID | (cd target && tar -xvf - $(NAME)-$(VERSION).zip) && \
docker rm -f $$ID && \
chmod ugo+r target/$(NAME)-$(VERSION).zip

venv: requirements.txt
virtualenv -p python3 venv && \
. ./venv/bin/activate && \
venv:
python3 -m venv .venv && \
. ./.venv/bin/activate && \
pip install --quiet --upgrade pip && \
pip install --quiet -r requirements.txt
pip install --quiet -e . && \
pip install --quiet -r tests/test-requirements.txt

clean:
rm -rf venv target
rm -rf src/*.pyc tests/*.pyc
rm -rf .venv target
find src/ -name '*.pyc' -delete
find tests/ -name '*.pyc' -delete

test: venv
for i in $$PWD/cloudformation/*; do \
aws cloudformation validate-template --template-body file://$$i > /dev/null || exit 1; \
done
. ./venv/bin/activate && \
pip install --quiet -r requirements.txt -r test-requirements.txt && \
cd src && \
PYTHONPATH=$(PWD)/src pytest ../tests/test*.py

autopep:
autopep8 --experimental --in-place --max-line-length 132 src/*.py tests/*.py
. ./.venv/bin/activate && \
pip install --quiet -r tests/test-requirements.txt && \
py.test tests

deploy-provider:
autopep: venv
. ./.venv/bin/activate && \
pip install --quiet -r requirements.txt && \
autopep8 --experimental --in-place --max-line-length 132 mysql_user_provider.py src/cfn_mysql_user_provider/*.py tests/*.py

deploy-provider: deploy
@set -x ;if aws cloudformation get-template-summary --stack-name $(NAME) >/dev/null 2>&1 ; then \
export CFN_COMMAND=update; \
else \
export CFN_COMMAND=create; \
fi ;\
export VPC_ID=$$(aws ec2 --output text --query 'Vpcs[?IsDefault].VpcId' describe-vpcs) ; \
export SUBNET_IDS=$$(aws ec2 --output text --query 'RouteTables[?Routes[?GatewayId == null]].Associations[].SubnetId' \
describe-route-tables --filters Name=vpc-id,Values=$$VPC_ID | tr '\t' ','); \
export SG_ID=$$(aws ec2 --output text --query "SecurityGroups[*].GroupId" \
describe-security-groups --group-names default --filters Name=vpc-id,Values=$$VPC_ID); \
export VPC_ID=$$(aws ec2 describe-vpcs --output text --query 'Vpcs[?IsDefault].VpcId') ; \
export SUBNET_IDS=$$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$$VPC_ID \
--output text --query 'Subnets[].SubnetId' | tr '\t' ','); \
export SG_ID=$$(aws ec2 describe-security-groups --group-names default --filters Name=vpc-id,Values=$$VPC_ID \
--output text --query "SecurityGroups[*].GroupId"); \
([[ -z $$VPC_ID ]] || [[ -z $$SUBNET_IDS ]] || [[ -z $$SG_ID ]]) && \
echo "Either there is no default VPC in your account, less then two subnets or no default security group available in the default VPC" && exit 1 ; \
echo "$$CFN_COMMAND provider in default VPC $$VPC_ID, subnets $$SUBNET_IDS using security group $$SG_ID." ; \
aws cloudformation $$CFN_COMMAND-stack \
--capabilities CAPABILITY_IAM \
--stack-name $(NAME) \
--template-body file://cloudformation/cfn-resource-provider.yaml \
--parameters ParameterKey=VPC,ParameterValue=$$VPC_ID \
ParameterKey=Subnets,ParameterValue=\"$$SUBNET_IDS\" \
ParameterKey=SecurityGroup,ParameterValue=$$SG_ID ;\
--parameters ParameterKey=VPC,ParameterValue=\"$$VPC_ID\" \
ParameterKey=Subnets,ParameterValue=\"$$SUBNET_IDS\" \
ParameterKey=SecurityGroup,ParameterValue=\"$$SG_ID\" \
ParameterKey=LambdaS3Bucket,ParameterValue=\"${S3_BUCKET}\" \
ParameterKey=LambdaVersion,ParameterValue=\"${VERSION}\" ;\
aws cloudformation wait stack-$$CFN_COMMAND-complete --stack-name $(NAME) ;

delete-provider:
Expand All @@ -107,22 +112,22 @@ demo:
@if aws cloudformation get-template-summary --stack-name $(NAME)-demo >/dev/null 2>&1 ; then \
export CFN_COMMAND=update; export CFN_TIMEOUT="" ;\
else \
export CFN_COMMAND=create; export CFN_TIMEOUT="--timeout-in-minutes 10" ;\
export CFN_COMMAND=create; export CFN_TIMEOUT="--timeout-in-minutes 30" ;\
fi ;\
export VPC_ID=$$(aws ec2 --output text --query 'Vpcs[?IsDefault].VpcId' describe-vpcs) ; \
export SUBNET_IDS=$$(aws ec2 --output text --query 'RouteTables[?Routes[?GatewayId == null]].Associations[].SubnetId' \
describe-route-tables --filters Name=vpc-id,Values=$$VPC_ID | tr '\t' ','); \
export SG_ID=$$(aws ec2 --output text --query "SecurityGroups[*].GroupId" \
describe-security-groups --group-names default --filters Name=vpc-id,Values=$$VPC_ID); \
export VPC_ID=$$(aws ec2 describe-vpcs --output text --query 'Vpcs[?IsDefault].VpcId') ; \
export SUBNET_IDS=$$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$$VPC_ID \
--output text --query 'Subnets[].SubnetId' | tr '\t' ','); \
export SG_ID=$$(aws ec2 describe-security-groups --group-names default --filters Name=vpc-id,Values=$$VPC_ID \
--output text --query "SecurityGroups[*].GroupId"); \
echo "$$CFN_COMMAND demo in default VPC $$VPC_ID, subnets $$SUBNET_IDS using security group $$SG_ID." ; \
([[ -z $$VPC_ID ]] || [[ -z $$SUBNET_IDS ]] || [[ -z $$SG_ID ]]) && \
echo "Either there is no default VPC in your account, no two subnets or no default security group available in the default VPC" && exit 1 ; \
aws cloudformation $$CFN_COMMAND-stack --stack-name $(NAME)-demo \
--template-body file://cloudformation/demo-stack.yaml \
$$CFN_TIMEOUT \
--parameters ParameterKey=VPC,ParameterValue=$$VPC_ID \
--parameters ParameterKey=VPC,ParameterValue=\"$$VPC_ID\" \
ParameterKey=Subnets,ParameterValue=\"$$SUBNET_IDS\" \
ParameterKey=SecurityGroup,ParameterValue=$$SG_ID ;\
ParameterKey=SecurityGroup,ParameterValue=\"$$SG_ID\" ;\
aws cloudformation wait stack-$$CFN_COMMAND-complete --stack-name $(NAME)-demo ;

delete-demo:
Expand Down
7 changes: 5 additions & 2 deletions cloudformation/cfn-resource-provider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Parameters:
LambdaS3Bucket:
Type: String
Default: ''
LambdaVersion:
Type: String
Default: '0.2.3'
Conditions:
UsePublicBucket: !Equals
- !Ref 'LambdaS3Bucket'
Expand Down Expand Up @@ -66,7 +69,7 @@ Resources:
- UsePublicBucket
- !Sub 'binxio-public-${AWS::Region}'
- !Ref 'LambdaS3Bucket'
S3Key: lambdas/cfn-mysql-user-provider-0.2.3.zip
S3Key: !Sub 'lambdas/cfn-mysql-user-provider-${LambdaVersion}.zip'
VpcConfig:
SecurityGroupIds:
- !Ref 'SecurityGroup'
Expand All @@ -75,4 +78,4 @@ Resources:
Handler: mysql_user_provider.handler
MemorySize: 128
Role: !GetAtt 'LambdaRole.Arn'
Runtime: python3.6
Runtime: python3.7
77 changes: 48 additions & 29 deletions cloudformation/demo-stack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,36 +7,13 @@ Parameters:
Type: List<AWS::EC2::Subnet::Id>
SecurityGroup:
Type: AWS::EC2::SecurityGroup::Id

Resources:
DBSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnets available for the RDS DB Instance
SubnetIds: !Ref 'Subnets'
Database:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 10
DBInstanceClass: db.t2.micro
Engine: mysql
EngineVersion: 5.7.21
VPCSecurityGroups:
- !Ref 'DatabaseSecurityGroup'
DBName: root
MasterUsername: root
MasterUserPassword: !GetAtt 'DBPassword.Secret'
MultiAZ: 'false'
Port: '3306'
PubliclyAccessible: 'false'
DBSubnetGroupName: !Ref 'DBSubnetGroup'
DBParameterGroupName: !Ref 'DatabaseParameterGroup'
DeletionPolicy: Snapshot
DatabaseParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: Parameters for MySQL
Family: MySQL5.7
Parameters: {}
DatabaseSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
Expand All @@ -58,16 +35,36 @@ Resources:
Alphabet: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
ReturnSecret: true
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-secret-provider'
Database:
Type: AWS::RDS::DBInstance
Properties:
AllocatedStorage: 30
DBInstanceClass: db.t3.medium
Engine: mysql
EngineVersion: 5.7.28
VPCSecurityGroups:
- !Ref 'DatabaseSecurityGroup'
DBName: root
MasterUsername: root
MasterUserPassword: !GetAtt 'DBPassword.Secret'
MultiAZ: 'false'
Port: '3306'
PubliclyAccessible: 'false'
DBSubnetGroupName: !Ref 'DBSubnetGroup'
DBParameterGroupName: !Ref 'DatabaseParameterGroup'
DeletionPolicy: Snapshot
DatabaseParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: Parameters for MySQL
Family: mysql5.7
Parameters: {}

KongPassword:
Type: Custom::Secret
Properties:
Name: !Sub '/${AWS::StackName}/mysql/kong/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-secret-provider'
KongReaderPassword:
Type: Custom::Secret
Properties:
Name: !Sub '/${AWS::StackName}/mysql/kongreader/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-secret-provider'
KongUser:
Type: Custom::MySQLUser
DependsOn:
Expand All @@ -84,6 +81,12 @@ Resources:
DBName: root
PasswordParameterName: !Sub '/${AWS::StackName}/mysql/root/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-mysql-user-provider-${VPC}'

KongReaderPassword:
Type: Custom::Secret
Properties:
Name: !Sub '/${AWS::StackName}/mysql/kongreader/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-secret-provider'
KongReaderUser:
Type: Custom::MySQLUser
DependsOn:
Expand All @@ -101,3 +104,19 @@ Resources:
DBName: root
PasswordParameterName: !Sub '/${AWS::StackName}/mysql/root/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-mysql-user-provider-${VPC}'
KongReaderUserGrant:
Type: Custom::MySQLUserGrant
DependsOn:
- KongReaderUser
Properties:
Grant:
- 'Select'
'On': 'kong.*'
User: kongreader
Database:
User: root
Host: !GetAtt 'Database.Endpoint.Address'
Port: !GetAtt 'Database.Endpoint.Port'
DBName: root
PasswordParameterName: !Sub '/${AWS::StackName}/mysql/root/PGPASSWORD'
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-mysql-user-provider-${VPC}'
48 changes: 48 additions & 0 deletions docs/MySQLUserGrant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Custom::MySQLUserGrant
The `Custom::MySQLUserGrant`resource grants a MySQL user with or without grant options.


## Syntax
To declare this entity in your AWS CloudFormation template, use the following syntax:

```yaml
Type: Custom::MySQLUserGrant
Properties:
Grant: [STRING]
On: STRING
User: STRING
WithGrantOption: true|false
Database:
Host: STRING
Port: INTEGER
Database: STRING
User: STRING
Password: STRING
PasswordParameterName: STRING
PasswordSecretName: STRING
ServiceToken: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:binxio-cfn-mysql-provider-vpc-${AppVPC}'
```

By default WithGrantOption is set to `false`. This means the user is unable to grant other users. To allow the user to grant other users, specify `true`.

## Properties
You can specify the following properties:

- `Grant` - the privileges to grant
- `On` - the privilege level to grant, use *.* for global grants. Update requires replacement.
- `User` - the user to grant, use user@host-syntax. Update requires replacement.
- `WithGrantOption` - if the user is allows to grant others, defaults to `false`
- `Database` - to create the user grant in. Update requires replacement.
- `Host` - the database server is listening on.
- `Port` - port the database server is listening on.
- `Database` - name to connect to.
- `User` - name of the database owner.
- `Password` - to identify the user with.
- `PasswordParameterName` - name of the ssm parameter containing the password of the user
- `PasswordSecretName` - friendly name or the ARN of the secret in secrets manager containing the password of the user

Either `Password`, `PasswordParameterName` or `PasswordSecretName` is required.

## Return values
There are no return values from this resources.

Loading