diff --git a/.funcignore b/.funcignore new file mode 100644 index 0000000..fa6cc3f --- /dev/null +++ b/.funcignore @@ -0,0 +1,4 @@ +.git* +.vscode +local.settings.json +test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a032d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ + +# Azure Functions artifacts +bin +obj +appsettings.json +local.settings.json +OrgList.csv \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f915119 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "ms-azuretools.vscode-azurefunctions", + "ms-vscode.PowerShell" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..62ef028 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach to PowerShell Functions", + "type": "PowerShell", + "request": "attach", + "customPipeName": "AzureFunctionsPSWorker", + "runspaceId": 1, + "preLaunchTask": "func: host start" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0b0f81f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "azureFunctions.deploySubpath": ".", + "azureFunctions.projectLanguage": "PowerShell", + "azureFunctions.projectRuntime": "~3", + "debug.internalConsoleOptions": "neverOpen" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..17983f9 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "func", + "command": "host start", + "problemMatcher": "$func-watch", + "isBackground": true + } + ] +} \ No newline at end of file diff --git a/AzGlueForwarder/OrgList.csv.example b/AzGlueForwarder/OrgList.csv.example new file mode 100644 index 0000000..f9f9de3 --- /dev/null +++ b/AzGlueForwarder/OrgList.csv.example @@ -0,0 +1,3 @@ +IP,ITGlueOrgID +1.1.1.1,123456 +2.2.2.2,123457 \ No newline at end of file diff --git a/AzGlueForwarder/function.json b/AzGlueForwarder/function.json new file mode 100644 index 0000000..781e1c9 --- /dev/null +++ b/AzGlueForwarder/function.json @@ -0,0 +1,21 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "route": "{*path}", + "name": "Request", + "methods": [ + "get", + "patch", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "Response" + } + ] +} diff --git a/Azglue.ps1 b/AzGlueForwarder/run.ps1 similarity index 97% rename from Azglue.ps1 rename to AzGlueForwarder/run.ps1 index 80bbf18..426759c 100644 --- a/Azglue.ps1 +++ b/AzGlueForwarder/run.ps1 @@ -1,69 +1,69 @@ -using namespace System.Net -param($Request, $TriggerMetadata) -#Check if AZapiKey is correct -if ($request.Headers.'x-api-key' -eq $ENV:AzAPIKey) { - #Comparing the client IP to the Organization list, and checking if it exists. - $ClientIP = ($request.headers.'X-Forwarded-For' -split ':')[0] - $CompareList = import-csv "AzGlueForwarder\OrgList.csv" -delimiter "," - $AllowedOrgs = $comparelist | where-object { $_.ip -eq $ClientIP } - if (!$AllowedOrgs) { - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - headers = @{'content-type' = 'application\json' } - StatusCode = [httpstatuscode]::OK - Body = @{"Error" = "401 - No match found in allowed list" } | convertto-json - }) - exit 1 - } - - #Sending request to ITGlue - #$resource = $request.params.path -replace "AzGlueForwarder/", "" - $resource = $request.url -replace "https://$($ENV:WEBSITE_HOSTNAME)/API", "" - #Replace x-api-key with actual key - $ITGHeaders = @{ - "x-api-key" = $ENV:ITGlueAPIKey - } - $Method = $($Request.method) - $ITGBody = $($Request.body) - #write-host ($AllowedOrgs | out-string) - $SuccessfullQuery = $false - $attempt = 3 - while ($attempt -gt 0 -and -not $SuccessfullQuery) { - try { - $ITGlueRequest = Invoke-RestMethod -Method $Method -ContentType "application/vnd.api+json" -Uri "$($ENV:ITGlueURI)/$resource" -Body $ITGBody -Headers $ITGHeaders - $SuccessfullQuery = $true - } - catch { - $ITGlueRequest = @{'Errorcode' = $_.Exception.Response.StatusCode.value__ } - $rand = get-random -Minimum 0 -Maximum 10 - start-sleep $rand - $attempt-- - if ($attempt -eq 0) { $ITGlueRequest = @{'Errorcode' = "Error code $($_.Exception.Response.StatusCode.value__) - Made 3 attempts and upload failed. $($_.Exception.Message) / Resource was $($ENV:ITGlueURI)/$resource" } } - } - } - - #Checking if we can strip the data that does not belong to this client. - #Important so passwords/items can only be retrieved belonging to this organisation. - #Can't do it for all requests, such as get-organisation, but for senstive data it works perfectly. :) - - if ($($ITGlueRequest.data.attributes.'organization-id')) { - write-host ($AllowedOrgs.ITGlueOrgID) - $ITGlueRequest.data = $ITGlueRequest.data | where-object { $_.attributes.'organization-id' -in $($AllowedOrgs.ITGlueOrgID) } - } - - #Sending the final object back to the client. - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - headers = @{'content-type' = 'application\json' } - StatusCode = [httpstatuscode]::OK - Body = $ITGlueRequest - }) - - -} -else { - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - headers = @{'content-type' = 'application\json' } - StatusCode = [httpstatuscode]::OK - Body = @{"Error" = "401 - No API Key entered or API key incorrect." } | convertto-json - }) - +using namespace System.Net +param($Request, $TriggerMetadata) +#Check if AZapiKey is correct +if ($request.Headers.'x-api-key' -eq $ENV:AzAPIKey) { + #Comparing the client IP to the Organization list, and checking if it exists. + $ClientIP = ($request.headers.'X-Forwarded-For' -split ':')[0] + $CompareList = import-csv "AzGlueForwarder\OrgList.csv" -delimiter "," + $AllowedOrgs = $comparelist | where-object { $_.ip -eq $ClientIP } + if (!$AllowedOrgs) { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + headers = @{'content-type' = 'application\json' } + StatusCode = [httpstatuscode]::OK + Body = @{"Error" = "401 - No match found in allowed list" } | convertto-json + }) + exit 1 + } + + #Sending request to ITGlue + #$resource = $request.params.path -replace "AzGlueForwarder/", "" + $resource = $request.url -replace "https://$($ENV:WEBSITE_HOSTNAME)/API", "" + #Replace x-api-key with actual key + $ITGHeaders = @{ + "x-api-key" = $ENV:ITGlueAPIKey + } + $Method = $($Request.method) + $ITGBody = $($Request.body) + #write-host ($AllowedOrgs | out-string) + $SuccessfullQuery = $false + $attempt = 3 + while ($attempt -gt 0 -and -not $SuccessfullQuery) { + try { + $ITGlueRequest = Invoke-RestMethod -Method $Method -ContentType "application/vnd.api+json" -Uri "$($ENV:ITGlueURI)/$resource" -Body $ITGBody -Headers $ITGHeaders + $SuccessfullQuery = $true + } + catch { + $ITGlueRequest = @{'Errorcode' = $_.Exception.Response.StatusCode.value__ } + $rand = get-random -Minimum 0 -Maximum 10 + start-sleep $rand + $attempt-- + if ($attempt -eq 0) { $ITGlueRequest = @{'Errorcode' = "Error code $($_.Exception.Response.StatusCode.value__) - Made 3 attempts and upload failed. $($_.Exception.Message) / Resource was $($ENV:ITGlueURI)/$resource" } } + } + } + + #Checking if we can strip the data that does not belong to this client. + #Important so passwords/items can only be retrieved belonging to this organisation. + #Can't do it for all requests, such as get-organisation, but for senstive data it works perfectly. :) + + if ($($ITGlueRequest.data.attributes.'organization-id')) { + write-host ($AllowedOrgs.ITGlueOrgID) + $ITGlueRequest.data = $ITGlueRequest.data | where-object { $_.attributes.'organization-id' -in $($AllowedOrgs.ITGlueOrgID) } + } + + #Sending the final object back to the client. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + headers = @{'content-type' = 'application\json' } + StatusCode = [httpstatuscode]::OK + Body = $ITGlueRequest + }) + + +} +else { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + headers = @{'content-type' = 'application\json' } + StatusCode = [httpstatuscode]::OK + Body = @{"Error" = "401 - No API Key entered or API key incorrect." } | convertto-json + }) + } \ No newline at end of file diff --git a/host.json b/host.json new file mode 100644 index 0000000..3485772 --- /dev/null +++ b/host.json @@ -0,0 +1,10 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[1.*, 2.0.0)" + }, + "managedDependency": { + "enabled": true + } +} diff --git a/local.settings.json.example b/local.settings.json.example new file mode 100644 index 0000000..897f037 --- /dev/null +++ b/local.settings.json.example @@ -0,0 +1,10 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "", + "FUNCTIONS_WORKER_RUNTIME": "powershell", + "AzAPIKey": "", + "ITGlueAPIKey": "", + "ITGlueURI": "https://api.itglue.com" + } +} diff --git a/profile.ps1 b/profile.ps1 new file mode 100644 index 0000000..dbbf093 --- /dev/null +++ b/profile.ps1 @@ -0,0 +1,21 @@ +# Azure Functions profile.ps1 +# +# This profile.ps1 will get executed every "cold start" of your Function App. +# "cold start" occurs when: +# +# * A Function App starts up for the very first time +# * A Function App starts up after being de-allocated due to inactivity +# +# You can define helper functions, run commands, or specify environment variables +# NOTE: any variables defined that are not environment variables will get reset after the first execution + +# Authenticate with Azure PowerShell using MSI. +# Remove this if you are not planning on using MSI or Azure PowerShell. +if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts)) { + Connect-AzAccount -Identity +} + +# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell. +# Enable-AzureRmAlias + +# You can also define functions or aliases that can be referenced in any of your PowerShell functions. diff --git a/proxies.json b/proxies.json new file mode 100644 index 0000000..b385252 --- /dev/null +++ b/proxies.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/proxies", + "proxies": {} +} diff --git a/requirements.psd1 b/requirements.psd1 new file mode 100644 index 0000000..7a8a591 --- /dev/null +++ b/requirements.psd1 @@ -0,0 +1,6 @@ +# This file enables modules to be automatically managed by the Functions service. +# See https://aka.ms/functionsmanageddependency for additional information. +# +@{ + 'Az' = '4.*' +} \ No newline at end of file