-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented CI/CD pipeline which builds, packages and deploys a Pytho…
…n Flask encryption service to Azure Function Apps.
- Loading branch information
hvalfangst
committed
Dec 25, 2023
1 parent
607b5a2
commit 7cb8e3d
Showing
12 changed files
with
367 additions
and
1 deletion.
There are no files selected for viewing
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,7 @@ | ||
.git* | ||
.idea* | ||
.terraform* | ||
terraform | ||
local.settings.json | ||
test | ||
.venv |
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,75 @@ | ||
# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action | ||
# More GitHub Actions for Azure: https://github.com/Azure/actions | ||
# More info on Python, GitHub Actions, and Azure Functions: https://aka.ms/python-webapps-actions | ||
|
||
name: Build and deploy Python project to Azure Function App - hvalfangstlinuxfunctionapp | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
workflow_dispatch: | ||
|
||
env: | ||
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root | ||
PYTHON_VERSION: '3.10' # set this to the python version to use (supports 3.6, 3.7, 3.8) | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v4 | ||
|
||
- name: Setup Python version | ||
uses: actions/setup-python@v1 | ||
with: | ||
python-version: ${{ env.PYTHON_VERSION }} | ||
|
||
- name: Create and start virtual environment | ||
run: | | ||
python -m venv venv | ||
source venv/bin/activate | ||
- name: Install dependencies | ||
run: pip install -r requirements.txt | ||
|
||
# Optional: Add step to run tests here | ||
|
||
- name: Zip artifact for deployment | ||
run: zip release.zip ./* -r | ||
|
||
- name: Upload artifact for deployment job | ||
uses: actions/upload-artifact@v3 | ||
with: | ||
name: python-app | ||
path: | | ||
release.zip | ||
!venv/ | ||
deploy: | ||
runs-on: ubuntu-latest | ||
needs: build | ||
environment: | ||
name: 'Production' | ||
url: ${{ steps.deploy-to-function.outputs.webapp-url }} | ||
|
||
steps: | ||
- name: Download artifact from build job | ||
uses: actions/download-artifact@v3 | ||
with: | ||
name: python-app | ||
|
||
- name: Unzip artifact for deployment | ||
run: unzip release.zip | ||
|
||
- name: 'Deploy to Azure Functions' | ||
uses: Azure/functions-action@v1 | ||
id: deploy-to-function | ||
with: | ||
app-name: 'hvalfangstlinuxfunctionapp' | ||
slot-name: 'Production' | ||
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} | ||
scm-do-build-during-deployment: true | ||
enable-oryx-build: true | ||
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D07B4CD95A574A438600A5F0A9A78160 }} |
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,48 @@ | ||
bin | ||
obj | ||
csx | ||
.vs | ||
edge | ||
Publish | ||
|
||
*.user | ||
*.suo | ||
*.cscfg | ||
*.Cache | ||
project.lock.json | ||
|
||
/packages | ||
/TestResults | ||
|
||
/tools/NuGet.exe | ||
/App_Data | ||
/secrets | ||
/data | ||
.secrets | ||
appsettings.json | ||
local.settings.json | ||
|
||
node_modules | ||
dist | ||
|
||
# Local python packages | ||
.python_packages/ | ||
|
||
# Python Environments | ||
.env | ||
.venv | ||
env/ | ||
venv/ | ||
ENV/ | ||
env.bak/ | ||
venv.bak/ | ||
|
||
# Byte-compiled / optimized / DLL files | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
|
||
# Azurite artifacts | ||
__blobstorage__ | ||
__queuestorage__ | ||
__azurite_db*__.json |
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,42 @@ | ||
# Azure Function in Python with CI/CD | ||
|
||
Azure Function programmed in Python with the Flask HTTP framework, | ||
providing endpoints for encryption utilizing AES 256 CBC algorithm. | ||
A CI/CD pipeline has been implemented with GitHub Actions. | ||
It will build, package and deploy this application to Azure Function Apps | ||
on any repository pushes. Azure resources are provisioned with Terraform. | ||
|
||
## Requirements | ||
|
||
* x86-64 | ||
* Linux/Unix | ||
* [Python](https://www.python.org/downloads/) | ||
|
||
## Creating resources | ||
|
||
The shell script 'up' allocates Azure resources with Terraform. | ||
|
||
## Deleting resources | ||
|
||
The shell script 'down' deallocates Azure resources. | ||
|
||
|
||
## Guide | ||
|
||
### 1. Provision Azure Resources | ||
|
||
- Run the 'up' script to provision Azure resources. | ||
|
||
### 2. Access Azure Portal | ||
|
||
- Open your browser and navigate to the Azure Portal. | ||
|
||
### 3. Configure Deployment Credentials | ||
|
||
- Navigate to your newly created Function App 'hvalfangstfunctionapp'. | ||
- Click on 'Deployment Center' under 'Settings'. | ||
- Choose GitHub as the source and proceed to authenticate & authorize your GH account. | ||
- After your account has been validated you may now choose a target repository and branch. | ||
- Click 'Save'. | ||
- A new folder named '.github' will now be pushed to your repository on behalf of Azure. | ||
- This folder contains the GitHub Action Workflow definition, which enables CI/CD. |
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,73 @@ | ||
from flask import Flask, jsonify, request | ||
|
||
from flask_api.crypto_utils.module import aes_encrypt, aes_decrypt | ||
|
||
app = Flask(__name__) | ||
|
||
|
||
@app.route("/") | ||
def index(): | ||
return ( | ||
"Welcome to Hvalfangst Crypto API!\n\n" | ||
"Available Endpoints:\n" | ||
"1. POST /encrypt - Encrypt text\n" | ||
" Use this endpoint to encrypt plain text with a provided encryption key.\n\n" | ||
"2. POST /decrypt - Decrypt text\n" | ||
" Use this endpoint to decrypt encrypted text with a provided encryption key.\n" | ||
) | ||
|
||
|
||
@app.route("/encrypt", methods=["POST"]) | ||
def encrypt(): | ||
# Extract data from JSON body | ||
request_data = request.get_json() | ||
|
||
# Check if 'plain_text' and 'encryption_key' fields are present in the JSON | ||
if "plain_text" not in request_data or "encryption_key" not in request_data: | ||
return jsonify({"error": "Missing 'plain_text' or 'encryption_key' in the request body"}), 400 | ||
|
||
plain_text = request_data["plain_text"] | ||
encryption_key = request_data["encryption_key"] | ||
|
||
# Encrypt plain text using key provided in request body | ||
encrypted_text = aes_encrypt(encryption_key, plain_text) | ||
|
||
# Prepare response JSON | ||
response = { | ||
"mode": "encrypt", | ||
"plain_text": plain_text, | ||
"encrypted_text": encrypted_text | ||
} | ||
|
||
# Return JSON response | ||
return jsonify(response) | ||
|
||
|
||
@app.route("/decrypt", methods=["POST"]) | ||
def decrypt(): | ||
# Extract data from JSON body | ||
request_data = request.get_json() | ||
|
||
# Check if 'encrypted_text' and 'encryption_key' fields are present in the JSON | ||
if "encrypted_text" not in request_data or "encryption_key" not in request_data: | ||
return jsonify({"error": "Missing 'encrypted_text' or 'encryption_key' in the request body"}), 400 | ||
|
||
encrypted_text = request_data["encrypted_text"] | ||
encryption_key = request_data["encryption_key"] | ||
|
||
# Decrypt encrypted text using key provided in request body | ||
plain_text = aes_decrypt(encryption_key, encrypted_text) | ||
|
||
# Prepare response JSON | ||
response = { | ||
"mode": "decrypt", | ||
"encrypted_text": encrypted_text, | ||
"plain_text": plain_text | ||
} | ||
|
||
# Return JSON response | ||
return jsonify(response) | ||
|
||
|
||
if __name__ == "__main__": | ||
app.run() |
Empty file.
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,72 @@ | ||
from Crypto.Protocol.KDF import PBKDF2 | ||
from Crypto.Cipher import AES | ||
from Crypto.Random import get_random_bytes | ||
from Crypto.Hash import SHA256 | ||
from Crypto.Util.Padding import pad, unpad | ||
import base64 | ||
|
||
|
||
def base64_encode(input_bytes: bytes) -> str: | ||
return base64.b64encode(input_bytes).decode("utf-8") | ||
|
||
|
||
def base64_decode(input_str: str) -> bytes: | ||
return base64.b64decode(input_str.encode("utf-8")) | ||
|
||
|
||
def generate_salt_32_byte() -> bytes: | ||
return get_random_bytes(32) | ||
|
||
|
||
def aes_encrypt(encryption_key: str, plaintext: str) -> str: | ||
# Convert plaintext encryption key to bytes | ||
password_bytes = encryption_key.encode("ascii") | ||
# Generate random sequence of 32 bytes | ||
salt = generate_salt_32_byte() | ||
# Set key derivation interation count to 15000 | ||
pbkdf2_iterations = 15000 | ||
|
||
# Derive new key based on encryption key, salt and iterations | ||
derived_key = PBKDF2( | ||
password_bytes, salt, 32, count=pbkdf2_iterations, hmac_hash_module=SHA256 | ||
) | ||
|
||
cipher = AES.new(derived_key, AES.MODE_CBC) | ||
|
||
# Encrypt plaintext using our CBC cipher | ||
ciphertext = cipher.encrypt(pad(plaintext.encode("ascii"), AES.block_size)) | ||
|
||
# B64-encode iv, salt and ciphertext (from bytes to text) | ||
iv_base64 = base64_encode(cipher.iv) | ||
salt_base64 = base64_encode(salt) | ||
ciphertext_base64 = base64_encode(ciphertext) | ||
|
||
# Return tuple containing base64-encoded {salt, iv and ciphertext} | ||
return f"{salt_base64}:{iv_base64}:{ciphertext_base64}" | ||
|
||
|
||
def aes_decrypt(encryption_key: str, ciphertext_base64: str) -> str: | ||
# Convert plaintext encryption key to bytes | ||
password_bytes = encryption_key.encode("ascii") | ||
|
||
# B64-decode tuple containing ciphertext, salt and iv (from text to bytes) | ||
data = ciphertext_base64.split(":") | ||
salt = base64_decode(data[0]) | ||
iv = base64_decode(data[1]) | ||
ciphertext = base64_decode(data[2]) | ||
|
||
# Set key derivation interation count to 15000 | ||
pbkdf2_iterations = 15000 | ||
|
||
# Derive new key based on encryption key, salt and iterations | ||
derived_key = PBKDF2( | ||
password_bytes, salt, 32, count=pbkdf2_iterations, hmac_hash_module=SHA256 | ||
) | ||
|
||
cipher = AES.new(derived_key, AES.MODE_CBC, iv) | ||
|
||
# Decrypt ciphertext using derived_key key and iv | ||
decrypted_text = unpad(cipher.decrypt(ciphertext), AES.block_size) | ||
|
||
# Return original plain text as text | ||
return decrypted_text.decode("utf-8") |
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,8 @@ | ||
{ | ||
"version": "2.0", | ||
"extensions": { | ||
"http": { | ||
"routePrefix": "" | ||
} | ||
} | ||
} |
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,8 @@ | ||
# Do not include azure-functions-worker in this file | ||
# The Python Worker is managed by the Azure Functions platform | ||
# Manually managing azure-functions-worker may cause unexpected issues | ||
|
||
azure-functions | ||
Flask | ||
crypto | ||
pycryptodome |
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
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,8 @@ | ||
import azure.functions as func | ||
from flask_api import app | ||
|
||
|
||
def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: | ||
"""Each request is redirected to the WSGI handler, which serves our Flask API. | ||
""" | ||
return func.WsgiMiddleware(app.wsgi_app).handle(req, context) |
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,21 @@ | ||
{ | ||
"scriptFile": "__init__.py", | ||
"bindings": [ | ||
{ | ||
"authLevel": "anonymous", | ||
"type": "httpTrigger", | ||
"direction": "in", | ||
"name": "req", | ||
"methods": [ | ||
"get", | ||
"post" | ||
], | ||
"route": "/{*route}" | ||
}, | ||
{ | ||
"type": "http", | ||
"direction": "out", | ||
"name": "$return" | ||
} | ||
] | ||
} |