Skip to content

Commit

Permalink
DRIVERS-2883 Add OIDC Support for Azure Functions (#455)
Browse files Browse the repository at this point in the history
  • Loading branch information
blink1073 authored Aug 1, 2024
1 parent 57705f7 commit 3c6aed2
Show file tree
Hide file tree
Showing 11 changed files with 455 additions and 0 deletions.
115 changes: 115 additions & 0 deletions .evergreen/auth_oidc/azure_func/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Azure Function Code

Scripts to handle testing an OIDC on Azure Functions.

https://learn.microsoft.com/en-us/azure/azure-functions/

## Prerequisites

### Overall
- Admin creates an Application in Azure
- Admin adds an Application URL to the Application
- Admin creates a Resource Group
- Admin creates a managed identity in the Resource Group
- Admin adds a role of "Managed Identity Operator" for the Application in the Resource Group
- Admin creates an Identity Provider in Atlas Cloud-Dev that has the following fields:
- iss: https://sts.windows.net/<subscription>
- aud: <application-url>
- Admin adds the Identity Provider to the Project
- Admin creates a Database User for the IdP:
- sub: <managed-identity-client-id>
- Admin creates a storage account in the resource group

### Per Driver

- Admin creates a Function App for the driver in the shared resource group
- Choose "Consumption" plan
- Create a unique name for the driver
- Choose the appropriate Runtime stack and version for the driver
- Admin adds the managed identity as a User-assigned identity on the Function App
- Admin adds the "Contributor" privileged role for the application in the Function App
- Admin adds environment variable for RESOURCE and CLIENT_ID.
- Admin adds the team member as a "Contributor" to the Function App
- Admin gives the function name to the driver team

- Driver team runs the following to set up a local function instance:
- Follow the quick-start instructions for [Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/)
- Stop at the "Create supporting Azure resources" stage, since they were created by the Admin
- Run the login.sh script
- Run `func azure functionapp publish <func-name>` and verify that it deploys
- Run the invoke script and verify that it invokes


## Self-Test

There is a self-test that runs during setup that invokes the `oidcselftest` function in the `$AZUREOIDC_FUNC_SELF_TEST` Function App
that is run durint setup.

You can also manually invoke the `gettoken` function by running the following:

```bash
source ./secrets-export.sh
pushd self-test
export FUNC_NAME=gettoken
export FUNC_APP_NAME=$AZUREOIDC_FUNC_SELF_TEST
export FUNC_RUNTIME=python
bash ../run-driver-test.sh
popd
```

## Driver Test

Drivers should use a task group to ensure resources are properly torn down. An example is as follows:

```yaml
- name: testoidc_azure_func_task_group
setup_group_can_fail_task: true
setup_group_timeout_secs: 1800
teardown_group_can_fail_task: true
teardown_group_timeout_secs: 1800
setup_group:
- func: fetch source
- func: <other setup function>
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure_func/setup.sh
teardown_group:
- command: subprocess.exec
params:
binary: bash
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure_func/teardown.sh
- func: <other teardown function>
tasks:
- oidc-auth-test-azure-func
```
Where the test func does something like the following:
```bash
pushd <driver-oidc-func-dir>
export FUNC_NAME=<driver-oidc-func-test>
export FUNC_APP_NAME=${DRIVER_FUNC_FROM_ENVIRONMENT}
export FUNC_RUNTIME=<runtime>
bash ${DRIVERS_TOOLS/.evergreen/auth_oidc/azure_func/run-driver-test.sh
popd
```

The <driver-oidc-func-dir> should contain an Azure function that runs a write operation
on a driver similar to the following:

```python
@app.route(route='pythonoidctest')
def oidcselftest(req: func.HttpRequest) -> func.HttpResponse:
resource=os.environ['APPSETTING_RESOURCE']
client_id= os.environ['APPSETTING_CLIENT_ID']
req_body = req.get_json()
uri = req_body.get('MONGODB_URI')
props = dict(ENVIRONMENT='azure', TOKEN_RESOURCE=resource)
client = MongoClient(uri, username=client_id, authMechanism="MONGODB-OIDC", authMechanismProperties=props)
c.test.test.insert_one({})
c.close()
return func.HttpResponse('Success!')
```
34 changes: 34 additions & 0 deletions .evergreen/auth_oidc/azure_func/invoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

set -o errexit

SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
. $SCRIPT_DIR/../../handle-paths.sh
pushd $SCRIPT_DIR

# Handle secrets from vault.
source ./secrets-export.sh

if [ -z "$FUNC_APP_NAME" ]; then
echo "Missing FUNC_APP_NAME!"
exit 1
fi

if [ -z "$FUNC_NAME" ]; then
echo "Missing FUNC_NAME!"
exit 1
fi

if [ -z "$MONGODB_URI" ]; then
echo "Missing MONGODB_URI!"
fi

CODE=$(az functionapp function keys list -g $AZUREOIDC_FUNC_RESOURCE_GROUP -n $FUNC_APP_NAME --function-name $FUNC_NAME | jq -r '.default')
URL=https://$FUNC_APP_NAME.azurewebsites.net/api/$FUNC_NAME?code=$CODE
DATA="{\"MONGODB_URI\": \"$MONGODB_URI\" }"
curl -i \
-X POST \
-d "$DATA" \
-H "x-functions-key: $CODE" \
$URL
20 changes: 20 additions & 0 deletions .evergreen/auth_oidc/azure_func/login.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

set -o errexit

SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
. $SCRIPT_DIR/../../handle-paths.sh
pushd $SCRIPT_DIR

# Handle secrets from vault.
if [ ! -f secrets-export.sh ]; then
. $DRIVERS_TOOLS/.evergreen/secrets_handling/setup-secrets.sh drivers/azureoidc
fi
source ./secrets-export.sh

export AZUREKMS_TENANTID=$AZUREOIDC_TENANTID
export AZUREKMS_SECRET=$AZUREOIDC_SECRET
export AZUREKMS_CLIENTID=$AZUREOIDC_APPID
"$DRIVERS_TOOLS"/.evergreen/csfle/azurekms/login.sh

popd
26 changes: 26 additions & 0 deletions .evergreen/auth_oidc/azure_func/run-driver-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash

set -o errexit

SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
. $SCRIPT_DIR/../../handle-paths.sh

# Handle secrets from vault.
source $SCRIPT_DIR/secrets-export.sh

VARLIST=(
FUNC_APP_NAME
FUNC_NAME
FUNC_RUNTIME
MONGODB_URI
)

# Ensure that all variables required to run the test are set, otherwise throw
# an error.
for VARNAME in ${VARLIST[*]}; do
[[ -z "${!VARNAME:-}" ]] && echo "ERROR: $VARNAME not set" && exit 1;
done

func init --$FUNC_RUNTIME
func azure functionapp publish $FUNC_APP_NAME
bash $SCRIPT_DIR/invoke.sh
59 changes: 59 additions & 0 deletions .evergreen/auth_oidc/azure_func/self-test/function_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import azure.functions as func
import logging
import os
from urllib.request import urlopen, Request
import json
from pymongo import MongoClient
from pymongo.auth_oidc import OIDCCallback, OIDCCallbackContext, OIDCCallbackResult

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

def _get_token():
resource=os.environ['APPSETTING_RESOURCE']
client_id= os.environ['APPSETTING_CLIENT_ID']
url = os.environ['IDENTITY_ENDPOINT']
url += '?api-version=2019-08-01'
url += f'&resource={resource}'
url += f'&client_id={client_id}'

headers = { "X-IDENTITY-HEADER": os.environ['IDENTITY_HEADER'] }

request = Request(url, headers=headers)
logging.info('Making a token request.')
with urlopen(request, timeout=30) as response:
body = response.read().decode('utf8')
return json.loads(body)['access_token']


class MyCallback(OIDCCallback):
def fetch(self, context: OIDCCallbackContext) -> OIDCCallbackResult:
return OIDCCallbackResult(access_token=_get_token())


@app.route(route="gettoken")
def gettoken(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Handling a gettoken request.')
try:
token = _get_token()
except Exception as e:
return func.HttpResponse(str(e), status_code=500)
logging.info('Returning the token.')
return func.HttpResponse(token)


@app.route(route='oidcselftest')
def oidcselftest(req: func.HttpRequest) -> func.HttpResponse:
logging.info('Handling an oidcselftest request.')
try:
req_body = req.get_json()
uri = req_body.get('MONGODB_URI')
props = dict(OIDC_CALLBACK=MyCallback())
logging.info('Testing MONGODB-OIDC on azure functions...')
c = MongoClient(f'{uri}/?authMechanism=MONGODB-OIDC', authMechanismProperties=props)
c.test.test.insert_one({})
c.close()
except Exception as e:
return func.HttpResponse(str(e), status_code=500)
logging.info('Testing MONGODB-OIDC on azure functions... done.')
logging.info('Self test complete!')
return func.HttpResponse('Success!')
15 changes: 15 additions & 0 deletions .evergreen/auth_oidc/azure_func/self-test/host.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
6 changes: 6 additions & 0 deletions .evergreen/auth_oidc/azure_func/self-test/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 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
pymongo
108 changes: 108 additions & 0 deletions .evergreen/auth_oidc/azure_func/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env bash

set -o errexit

SCRIPT_DIR=$(dirname ${BASH_SOURCE[0]})
. $SCRIPT_DIR/../../handle-paths.sh
pushd $SCRIPT_DIR

# Ensure Azure Functions Core Tools is/can be installed.
DOCS_URL=https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local
OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]')
if ! command -v func &> /dev/null; then
if [ "$OS_NAME" != "linux" ]; then
echo "See $DOCS_URL for Azure Functions Core Tools installation"
exit 1
fi
fi

########################
# Log in to azure and set up secrets.
# Ensure clean secrets.
rm -f secrets-export.sh
bash ./login.sh
source ./secrets-export.sh

########################
# Start an Atlas Cluster

# Get the utility functions
. ../../atlas/atlas-utils.sh

# Generate a random cluster name.
# See: https://docs.atlas.mongodb.com/reference/atlas-limits/#label-limits
DEPLOYMENT_NAME="$RANDOM-DRIVERGCP"
echo "export CLUSTER_NAME=$DEPLOYMENT_NAME" >> "secrets-export.sh"

# Set the create cluster configuration.
export DEPLOYMENT_DATA=$(cat <<EOF
{
"autoScaling" : {
"autoIndexingEnabled" : false,
"compute" : {
"enabled" : true,
"scaleDownEnabled" : true
},
"diskGBEnabled" : true
},
"backupEnabled" : false,
"biConnector" : {
"enabled" : false,
"readPreference" : "secondary"
},
"clusterType" : "REPLICASET",
"diskSizeGB" : 10.0,
"encryptionAtRestProvider" : "NONE",
"mongoDBMajorVersion" : "7.0",
"name" : "${DEPLOYMENT_NAME}",
"numShards" : 1,
"paused" : false,
"pitEnabled" : false,
"providerBackupEnabled" : false,
"providerSettings" : {
"providerName" : "AWS",
"autoScaling" : {
"compute" : {
"maxInstanceSize" : "M20",
"minInstanceSize" : "M10"
}
},
"diskIOPS" : 3000,
"encryptEBSVolume" : true,
"instanceSizeName" : "M10",
"regionName" : "US_EAST_1",
"volumeType" : "STANDARD"
},
"replicationFactor" : 3,
"rootCertType" : "ISRGROOTX1",
"terminationProtectionEnabled" : false,
"versionReleaseSystem" : "LTS"
}
EOF
)

export ATLAS_PUBLIC_API_KEY=$OIDC_ATLAS_PUBLIC_API_KEY
export ATLAS_PRIVATE_API_KEY=$OIDC_ATLAS_PRIVATE_API_KEY
export ATLAS_GROUP_ID=$OIDC_ATLAS_GROUP_ID

create_deployment

# Ensure Azure Functions Core Tools is installed.
URL=https://github.com/Azure/azure-functions-core-tools/releases/download/4.0.5907/Azure.Functions.Cli.linux-x64.4.0.5907.zip
if ! command -v func &> /dev/null; then
curl -L -o /tmp/azure-functions-cli.zip $URL
unzip -q -d /tmp/azure-functions-cli /tmp/azure-functions-cli.zip
pushd /tmp/azure-functions-cli
mkdir -p $DRIVERS_TOOLS/.bin
chmod +x func
chmod +x gozip
mv * $DRIVERS_TOOLS/.bin
popd
fi

########################
# Wait for the Atlas Cluster
export MONGODB_URI=$(check_deployment)
echo "export MONGODB_URI=$MONGODB_URI" >> ./secrets-export.sh

popd
Loading

0 comments on commit 3c6aed2

Please sign in to comment.