Compare commits

..

8 Commits

Author SHA1 Message Date
Balaga Gayatri
3bcabb32ed Update README.md 2021-10-11 21:27:58 +05:30
Usha N
8e3c83b515 Update README.md 2021-10-11 18:50:45 +05:30
Balaga Gayatri
ceaa639e34 Update README.md 2021-10-11 18:37:00 +05:30
Balaga Gayatri
0a49ca330b Update README.md 2021-10-11 18:14:16 +05:30
Balaga Gayatri
66ade64420 Update README.md 2021-10-11 17:56:16 +05:30
Usha N
5642aae9c6 Update README.md 2021-10-11 17:47:08 +05:30
Usha N
b609339187 Updated action.yml with OIDC changes 2021-10-11 17:21:39 +05:30
Balaga Gayatri
a4b1865541 Added OIDC support for login action (#147) 2021-10-11 16:58:00 +05:30
8 changed files with 510 additions and 344 deletions

358
README.md
View File

@@ -2,21 +2,149 @@
## Automate your GitHub workflows using Azure Actions ## Automate your GitHub workflows using Azure Actions
[GitHub Actions](https://help.github.com/en/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow. [GitHub Actions](https://help.github.com/en/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow.
With [GitHub Actions for Azure](https://github.com/Azure/actions/) you can create workflows that you can set up in your repository to build, test, package, release and **deploy** to Azure. With [GitHub Actions for Azure](https://github.com/Azure/actions/) you can create workflows that you can set up in your repository to build, test, package, release and **deploy** to Azure.
NOTE: you must have write permissions to the repository in question. If you're using a sample repository from Microsoft, be sure to first fork the repository to your own GitHub account.
Get started today with a [free Azure account](https://azure.com/free/open-source).
# GitHub Action for Azure Login # GitHub Action for Azure Login
With the Azure login Action, you can automate your workflow to do an Azure login using [Azure service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals) and run Az CLI and Azure PowerShell scripts.
By default, only az cli login will be done. In addition to az cli, you can login using Az module to run Azure PowerShell scripts by setting enable-AzPSSession to true. With the Azure login Action, you can automate your workflow to do an Azure login using [Azure service principal](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) and run Azure CLI and Azure PowerShell scripts. You can leverage this action for the public or soverign clouds including Azure Government and Azure Stack Hub (using the `environment` parameter).
Get started today with a [free Azure account](https://azure.com/free/open-source)! By default, the action only logs in with the Azure CLI (using the `az login` command). To log in with the Az PowerShell module, set `enable-AzPSSession` to true. To login to Azure tenants without any subscriptions, set the optional parameter `allow-no-subscriptions` to true.
To login into one of the Azure Government clouds, set the optional parameter environment with supported cloud names AzureUSGovernment or AzureChinaCloud. If this parameter is not specified, it takes the default value AzureCloud and connect to the Azure Public Cloud. Additionally the parameter creds takes the Azure service principal created in the particular cloud to connect (Refer to Configure deployment credentials section below for details).
To login using **Open ID Connect (OIDC) based federated identity credentials**, set the `client-id`, `tenant-id` and `subscription-id` of the Azure service principal associated with an OIDC Federated Identity Credential.
Follow <this> guidance, to create a new service principal and then to create a Federated credential in Azure portal needed to establish OIDC trust between GitHub deployment workflows and the specific Azure resources scoped by the service principal. Configure the Federated Credential with appropriate values of the GitHub Org, Repo and Environments based on the context used in the GitHub deployment workflows targeting Azure.
Note: Currently OIDC login is supported only with Azure CLI for public clouds. Support for Azure PowerShell and for other clouds like Government clouds, Azure Stacks would be added soon.
This repository contains GitHub Action for [Azure Login](https://github.com/Azure/login/blob/master/action.yml). This repository contains GitHub Action for [Azure Login](https://github.com/Azure/login/blob/master/action.yml).
## Sample workflow that uses Azure login action to run az cli ## Sample workflow that uses Azure login action to run az cli
```yaml
# File: .github/workflows/workflow.yml
on: [push]
name: AzureLoginSample
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az webapp list --query "[?state=='Running']"
```
## Sample workflow that uses Azure login action using OIDC/Federated Identity credentials to run az cli
```yaml
# File: .github/workflows/OIDC_workflow.yml
name: Run Azure Login with OIDC
on: [push]
permissions:
id-token: write
contents: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Installing CLI-beta for OIDC
run: |
CWD="$(pwd)"
python3 -m venv oidc-venv
. oidc-venv/bin/activate
echo "activated environment"
python3 -m pip install -q --upgrade pip
echo "started installing cli beta"
pip install -q --extra-index-url https://azurecliedge.blob.core.windows.net/federated-token/simple/ azure-cli
echo "***************installed cli beta*******************"
echo "$CWD/oidc-venv/bin" >> $GITHUB_PATH
- name: 'Az CLI login'
uses: azure/login@oidc-support
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }}
- name: 'Run az commands'
run: |
az account show
az group list
pwd
```
## Sample workflow that uses Azure login action to run Azure PowerShell
```yaml
# File: .github/workflows/workflow.yml
on: [push]
name: AzurePowerShellSample
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Login via Az module
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
enable-AzPSSession: true
- name: Run Az CLI script
run: |
az webapp list --query "[?state=='Running']"
- name: Run Azure PowerShell script
uses: azure/powershell@v1
with:
azPSVersion: '3.1.0'
inlineScript: |
Get-AzVM -ResourceGroupName "ActionsDemo"
```
## Sample to connect to Azure US Government cloud
```yaml
- name: Login to Azure US Gov Cloud with CLI
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }}
environment: 'AzureUSGovernment'
enable-AzPSSession: false
- name: Login to Azure US Gov Cloud with Az Powershell
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }}
environment: 'AzureUSGovernment'
enable-AzPSSession: true
```
Refer to the [Azure PowerShell](https://github.com/azure/powershell) Github action to run your Azure PowerShell scripts.
## Sample Azure Login workflow that to run az cli on Azure Stack Hub
```yaml ```yaml
# File: .github/workflows/workflow.yml # File: .github/workflows/workflow.yml
@@ -30,111 +158,171 @@ jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: azure/login@v1 - uses: azure/login@v1
with: with:
creds: ${{ secrets.AZURE_CREDENTIALS }} creds: ${{ secrets.AZURE_CREDENTIALS }}
environment: 'AzureStack'
- run: | - run: |
az webapp list --query "[?state=='Running']" az webapp list --query "[?state=='Running']"
``` ```
Refer to the [Azure Stack Hub Login Action Tutorial](https://docs.microsoft.com/en-us/azure-stack/user/ci-cd-github-action-login-cli?view=azs-2008) for more detailed instructions.
## Sample workflow that uses Azure login action to run Azure PowerShell ## Configure deployment credentials:
### Configure OIDC federated credentials:
To login using **Open ID Connect (OIDC) based federated identity credentials**, in the workflow, set the values of `client-id`, `tenant-id` and `subscription-id` of the Azure service principal associated with an OIDC Federated Identity Credential as individual repository secrets.
Follow <this> guidance, to create a new service principal and then to create a Federated credential in Azure portal needed to establish OIDC trust between GitHub deployment workflows and the specific Azure resources scoped by the service principal. Configure the Federated Credential with appropriate values of the GitHub Org, Repo and Environments based on the context used in the GitHub deployment workflows targeting Azure.
Note: Currently OIDC login is supported only with Azure CLI for public clouds. Support for Azure PowerShell and for other clouds like Government clouds, Azure Stacks would be added soon. Inorder to login in that case you need to use the approach as shown below for configuring credentials for using non-OIDC login.
### Configure non-OIDC credentials:
If the credentials are supplied as as a single JSON object secret then the login action will use non-OIDC approach. Azure login Action in this case depends on a [secret](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) named `AZURE_CREDENTIALS` in your repository. The value of this secret is expected to be a JSON object that represents a service principal (an identifer for an application or process) that authenticates the workflow with Azure.
To function correctly, this service principal must be assigned the [Contributor]((https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#contributor)) role for the web app or the resource group that contains the web app.
The following steps describe how to create the service principal, assign the role, and create a single secret in your repository with the resulting credentials.
1. Open the Azure Cloud Shell at [https://shell.azure.com](https://shell.azure.com). You can alternately use the [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) if you've installed it locally. (For more information on Cloud Shell, see the [Cloud Shell Overview](https://docs.microsoft.com/azure/cloud-shell/overview).)
1.1 **(Required ONLY when environment is Azure Stack Hub)** Run the following command to set the SQL Management endpoint to 'not supported'
```bash
az cloud update -n {environmentName} --endpoint-sql-management https://notsupported
```
2. Use the [az ad sp create-for-rbac](https://docs.microsoft.com/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac) command to create a service principal and assign a Contributor role:
For web apps (also more secure)
```azurecli
az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Web/sites/{app-name}
```
For usage with other Azure services (Storage Accounts, Active Directory, etc.)
```azurecli
az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}
```
Replace the following:
* `{sp-name}` with a suitable name for your service principal, such as the name of the app itself. The name must be unique within your organization.
* `{subscription-id}` with the subscription ID you want to use (found in Subscriptions in portal)
* `{resource-group}` the resource group containing the web app.
* [optional] `{app-name}` if you wish to have a tighter & more secure scope, use the first option and replace this with the name of the web app.
More info can be found [here](https://docs.microsoft.com/en-us/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac).
This command invokes Azure Active Directory (via the `ad` part of the command) to create a service principal (via `sp`) specifically for [Role-Based Access Control (RBAC)](https://docs.microsoft.com/azure/role-based-access-control/overview) (via `create-for-rbac`).
The `--role` argument specifies the permissions to grant to the service principal at the specified `--scope`. In this case, you grant the built-in [Contributor](https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#contributor) role at the scope of the web app in the specified resource group in the specified subscription. If desired, you can omit the part of the scope starting with `/providers/...` to grant the service principal the Contributor role for the entire resource group. For security purposes, however, it's always preferable to grant permissions at the most restrictive scope possible.
3. When complete, the `az ad sp create-for-rbac` command displays JSON output in the following form (which is specified by the `--sdk-auth` argument):
```json
{
"clientId": "<GUID>",
"clientSecret": "<CLIENT_SECRET_VALUE>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)
}
```
4. In your repository, use **Add secret** to create a new secret named `AZURE_CREDENTIALS` (as shown in the example workflow), or using whatever name is in your workflow file.
NOTE: While adding secret `AZURE_CREDENTIALS` make sure to add like this
{"clientId": "<GUID>",
"clientSecret": "<CLIENT_SECRET_VALUE>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)}
instead of
{
"clientId": "<GUID>",
"clientSecret": "<CLIENT_SECRET_VALUE>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)
}
to prevent unnecessary masking of `{ } ` in your logs which are in dictionary form.
5. Paste the entire JSON object produced by the `az ad sp create-for-rbac` command as the secret value and save the secret.
NOTE: to manage service principals created with `az ad sp create-for-rbac`, visit the [Azure portal](https://portal.azure.com), navigate to your Azure Active Directory, then select **Manage** > **App registrations** on the left-hand menu. Your service principal should appear in the list. Select a principal to navigate to its properties. You can also manage role assignments using the [az role assignment](https://docs.microsoft.com/cli/azure/role/assignment?view=azure-cli-latest) command.
NOTE: Currently there is no support for passing in the Subscription ID, Tenant ID, Client ID, and Client Secret as individual parameters instead of bundled in a single JSON object (creds).
However, a simple workaround for users who need this option can be:
```yaml
- uses: Azure/login@v1.1
with:
creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}'
```
In a similar way, any additional parameter can be addded to creds such as resourceManagerEndpointUrl for Azure Stack, for example.
NOTE: If you want to hand craft your JSON object instead of using the output from the CLI command (for example, after using the UI to create the App Registration and Role assignment) the following fields are required:
```json
{
"clientId": "<GUID>",
"tenantId": "<GUID>",
"clientSecret": "<CLIENT_SECRET_VALUE>",
"subscriptionId": "<GUID>",
"resourceManagerEndpointUrl": "<URL>}
```
The resourceManagerEndpointUrl will be `https://management.azure.com/` if you are using the public azure cloud.
## Support for using `allow-no-subscriptions` flag with az login
Capability has been added to support access to tenants without subscriptions. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default.
```yaml ```yaml
# File: .github/workflows/workflow.yml # File: .github/workflows/workflow.yml
on: [push] on: [push]
name: AzurePowerShellLoginSample name: AzureLoginWithNoSubscriptions
jobs: jobs:
build: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Login via Az module - uses: azure/login@v1
uses: azure/login@v1
with: with:
creds: ${{secrets.AZURE_CREDENTIALS}} creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true allow-no-subscriptions: true
- run: |
Get-AzVM -ResourceGroupName "ResourceGroup11"
``` ```
## Az logout and security hardening
Refer [Azure PowerShell](https://github.com/azure/powershell) Github action to run your Azure PowerShell scripts. This action doesn't implement ```az logout``` by default at the end of execution. However there is no way of tampering the credentials or account information because the github hosted runner is on a VM that will get reimaged for every customer run which gets everything deleted. But if the runner is self-hosted which is not github provided it is recommended to manually logout at the end of the workflow as shown below. More details on security of the runners can be found [here](https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#hardening-for-self-hosted-runners).
## Configure deployment credentials:
For any credentials like Azure Service Principal, Publish Profile etc add them as [secrets](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables) in the GitHub repository and then use them in the workflow.
The above example uses user-level credentials i.e., Azure Service Principal for deployment.
Follow the steps to configure the secret:
* Define a new secret under your repository settings, Add secret menu
* Store the output of the below [az cli](https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest) command as the value of secret variable, for example 'AZURE_CREDENTIALS'
```bash
az ad sp create-for-rbac --name "myApp" --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
# Replace {subscription-id}, {resource-group} with the subscription, resource group details
# The command should output a JSON object similar to this:
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)
}
``` ```
* Now in the workflow file in your branch: `.github/workflows/workflow.yml` replace the secret in Azure login action with your secret (Refer to the example above) - name: Azure CLI script
uses: azure/CLI@v1
with:
# Azure Login metadata file azcliversion: 2.0.72
inlineScript: |
```yaml az logout
az cache purge
# action.yml az account clear
# Login to Azure subscription
name: 'Azure Login'
description: 'AuthenticatetoAzureandrunyourAzCLIorAzPowerShellbasedActionsorscripts.github.com/Azure/Actions'
inputs:
creds:
description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS'
required: true
enable-AzPSSession:
description: 'SetthisvaluetotruetoenableAzurePowerShellLogininadditiontoAzCLIlogin'
required: false
default: false
branding:
icon: 'login.svg'
color: 'blue'
runs:
using: 'node12'
main: 'lib/main.js'
``` ```
# Contributing # Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide For detailed developer guidelines, visit [developer guidelines for azure actions](https://github.com/Azure/actions/blob/main/docs/developer-guildelines.md).
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

View File

@@ -6,13 +6,13 @@ inputs:
description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS' description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS'
required: false required: false
client-id: client-id:
description: 'ClientId of the Azure Service principal created.' description: 'ClientId of the Azure Service principal associated with OIDC Federated credential '
required: false required: false
tenant-id: tenant-id:
description: 'TenantId of the Azure Service principal created.' description: 'TenantId of the Azure Service principal associated with OIDC Federated credential.'
required: false required: false
subscription-id: subscription-id:
description: 'Azure subscriptionId' description: 'Azure subscriptionId scoped to the service principal with associated OIDC Federated credential'
required: false required: false
enable-AzPSSession: enable-AzPSSession:
description: 'SetthisvaluetotruetoenableAzurePowerShellLogininadditiontoAzCLIlogin' description: 'SetthisvaluetotruetoenableAzurePowerShellLogininadditiontoAzCLIlogin'

View File

@@ -38,10 +38,9 @@ const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellTo
const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder")); const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder"));
const Constants_1 = __importDefault(require("./Constants")); const Constants_1 = __importDefault(require("./Constants"));
class ServicePrincipalLogin { class ServicePrincipalLogin {
constructor(servicePrincipalId, servicePrincipalKey, federatedToken, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl) { constructor(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl) {
this.servicePrincipalId = servicePrincipalId; this.servicePrincipalId = servicePrincipalId;
this.servicePrincipalKey = servicePrincipalKey; this.servicePrincipalKey = servicePrincipalKey;
this.federatedToken = federatedToken;
this.tenantId = tenantId; this.tenantId = tenantId;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.environment = environment; this.environment = environment;
@@ -69,7 +68,6 @@ class ServicePrincipalLogin {
const args = { const args = {
servicePrincipalId: this.servicePrincipalId, servicePrincipalId: this.servicePrincipalId,
servicePrincipalKey: this.servicePrincipalKey, servicePrincipalKey: this.servicePrincipalKey,
federatedToken: this.federatedToken,
subscriptionId: this.subscriptionId, subscriptionId: this.subscriptionId,
environment: this.environment, environment: this.environment,
scopeLevel: ServicePrincipalLogin.scopeLevel, scopeLevel: ServicePrincipalLogin.scopeLevel,

View File

@@ -35,17 +35,9 @@ class ScriptBuilder {
if (args.environment.toLowerCase() == "azurestack") { if (args.environment.toLowerCase() == "azurestack") {
command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`;
} }
// Separate command script for OIDC and non-OIDC command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
if (!!args.federatedToken) { (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
command += `Connect-AzAccount -ServicePrincipal -ApplicationId '${args.servicePrincipalId}' -Tenant '${tenantId}' -FederatedToken '${args.federatedToken}' \ -Environment '${args.environment}' | out-null;`;
-Environment '${args.environment}' | out-null;`;
}
else {
command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
(New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
-Environment '${args.environment}' | out-null;`;
}
// command to set the subscription
if (args.scopeLevel === Constants_1.default.Subscription && !args.allowNoSubscriptionsLogin) { if (args.scopeLevel === Constants_1.default.Subscription && !args.allowNoSubscriptionsLogin) {
command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`; command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`;
} }

View File

@@ -79,12 +79,11 @@ function main() {
var subscriptionId = core.getInput('subscription-id', { required: false }); var subscriptionId = core.getInput('subscription-id', { required: false });
var resourceManagerEndpointUrl = "https://management.azure.com/"; var resourceManagerEndpointUrl = "https://management.azure.com/";
var enableOIDC = true; var enableOIDC = true;
var federatedToken = null;
// If any of the individual credentials (clent_id, tenat_id, subscription_id) is present. // If any of the individual credentials (clent_id, tenat_id, subscription_id) is present.
if (servicePrincipalId || tenantId || subscriptionId) { if (servicePrincipalId || tenantId || subscriptionId) {
//If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs. //If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs.
if (!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin))) if (!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin)))
throw new Error("Few credentials are missing. ClientId,tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set."); throw new Error("Few credentials are missing.ClientId,tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set.");
} }
else { else {
if (creds) { if (creds) {
@@ -115,10 +114,12 @@ function main() {
if (enableOIDC) { if (enableOIDC) {
console.log('Using OIDC authentication...'); console.log('Using OIDC authentication...');
//generating ID-token //generating ID-token
federatedToken = yield core.getIDToken('api://AzureADTokenExchange'); var idToken = yield core.getIDToken('api://AzureADTokenExchange');
if (!!federatedToken) { if (!!idToken) {
if (environment != "azurecloud") if (environment != "azurecloud")
throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`); throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`);
if (enableAzPSSession)
throw new Error(`Powershell login is not supported with OIDC.`);
} }
else { else {
throw new Error("Could not get ID token for authentication."); throw new Error("Could not get ID token for authentication.");
@@ -164,7 +165,7 @@ function main() {
commonArgs = commonArgs.concat("--allow-no-subscriptions"); commonArgs = commonArgs.concat("--allow-no-subscriptions");
} }
if (enableOIDC) { if (enableOIDC) {
commonArgs = commonArgs.concat("--federated-token", federatedToken); commonArgs = commonArgs.concat("--federated-token", idToken);
} }
else { else {
commonArgs = commonArgs.concat("-p", servicePrincipalKey); commonArgs = commonArgs.concat("-p", servicePrincipalKey);
@@ -181,8 +182,7 @@ function main() {
if (enableAzPSSession) { if (enableAzPSSession) {
// Attempting Az PS login // Attempting Az PS login
console.log(`Running Azure PS Login`); console.log(`Running Azure PS Login`);
var spnlogin; const spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl);
spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, federatedToken, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl);
yield spnlogin.initialize(); yield spnlogin.initialize();
yield spnlogin.login(); yield spnlogin.login();
} }

View File

@@ -15,20 +15,17 @@ export class ServicePrincipalLogin implements IAzurePowerShellSession {
subscriptionId: string; subscriptionId: string;
resourceManagerEndpointUrl: string; resourceManagerEndpointUrl: string;
allowNoSubscriptionsLogin: boolean; allowNoSubscriptionsLogin: boolean;
federatedToken: string;
constructor(servicePrincipalId: string, constructor(servicePrincipalId: string,
servicePrincipalKey: string, servicePrincipalKey: string,
federatedToken: string, tenantId: string,
tenantId: string,
subscriptionId: string, subscriptionId: string,
allowNoSubscriptionsLogin: boolean, allowNoSubscriptionsLogin: boolean,
environment: string, environment: string,
resourceManagerEndpointUrl: string) { resourceManagerEndpointUrl: string) {
this.servicePrincipalId = servicePrincipalId; this.servicePrincipalId = servicePrincipalId;
this.servicePrincipalKey = servicePrincipalKey; this.servicePrincipalKey = servicePrincipalKey;
this.federatedToken = federatedToken;
this.tenantId = tenantId; this.tenantId = tenantId;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.environment = environment; this.environment = environment;
@@ -55,7 +52,6 @@ export class ServicePrincipalLogin implements IAzurePowerShellSession {
const args: any = { const args: any = {
servicePrincipalId: this.servicePrincipalId, servicePrincipalId: this.servicePrincipalId,
servicePrincipalKey: this.servicePrincipalKey, servicePrincipalKey: this.servicePrincipalKey,
federatedToken: this.federatedToken,
subscriptionId: this.subscriptionId, subscriptionId: this.subscriptionId,
environment: this.environment, environment: this.environment,
scopeLevel: ServicePrincipalLogin.scopeLevel, scopeLevel: ServicePrincipalLogin.scopeLevel,

View File

@@ -8,23 +8,17 @@ export default class ScriptBuilder {
getAzPSLoginScript(scheme: string, tenantId: string, args: any): string { getAzPSLoginScript(scheme: string, tenantId: string, args: any): string {
let command = `Clear-AzContext -Scope Process; let command = `Clear-AzContext -Scope Process;
Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`;
if (scheme === Constants.ServicePrincipal) { if (scheme === Constants.ServicePrincipal) {
if (args.environment.toLowerCase() == "azurestack") { if (args.environment.toLowerCase() == "azurestack") {
command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`;
} }
// Separate command script for OIDC and non-OIDC
if (!!args.federatedToken) { command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
command += `Connect-AzAccount -ServicePrincipal -ApplicationId '${args.servicePrincipalId}' -Tenant '${tenantId}' -FederatedToken '${args.federatedToken}' \ (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
-Environment '${args.environment}' | out-null;`; -Environment '${args.environment}' | out-null;`;
}
else {
command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
(New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
-Environment '${args.environment}' | out-null;`;
}
// command to set the subscription
if (args.scopeLevel === Constants.Subscription && !args.allowNoSubscriptionsLogin) { if (args.scopeLevel === Constants.Subscription && !args.allowNoSubscriptionsLogin) {
command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`; command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`;
} }
@@ -41,7 +35,7 @@ export default class ScriptBuilder {
$output['${Constants.Error}'] = $_.exception.Message $output['${Constants.Error}'] = $_.exception.Message
} }
return ConvertTo-Json $output`; return ConvertTo-Json $output`;
core.debug(`Azure PowerShell Login Script: ${this.script}`); core.debug(`Azure PowerShell Login Script: ${this.script}`);
return this.script; return this.script;
} }

View File

@@ -1,214 +1,212 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import * as io from '@actions/io'; import * as io from '@actions/io';
import { FormatType, SecretParser } from 'actions-secret-parser'; import { FormatType, SecretParser } from 'actions-secret-parser';
import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin'; import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin';
var azPath: string; var azPath: string;
var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : ""; var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : "";
var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT}` : ""; var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT}` : "";
async function main() { async function main() {
try { try {
// Set user agent variable // Set user agent variable
var isAzCLISuccess = false; var isAzCLISuccess = false;
let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`; let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`;
let actionName = 'AzureLogin'; let actionName = 'AzureLogin';
let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`; let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
let azurePSHostEnv = (!!azPSHostEnv ? `${azPSHostEnv}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`; let azurePSHostEnv = (!!azPSHostEnv ? `${azPSHostEnv}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString);
core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv);
azPath = await io.which("az", true); azPath = await io.which("az", true);
core.debug(`az cli version used: ${azPath}`); core.debug(`az cli version used: ${azPath}`);
let azureSupportedCloudName = new Set([ let azureSupportedCloudName = new Set([
"azureusgovernment", "azureusgovernment",
"azurechinacloud", "azurechinacloud",
"azuregermancloud", "azuregermancloud",
"azurecloud", "azurecloud",
"azurestack"]); "azurestack"]);
let output: string = ""; let output: string = "";
const execOptions: any = { const execOptions: any = {
listeners: { listeners: {
stdout: (data: Buffer) => { stdout: (data: Buffer) => {
output += data.toString(); output += data.toString();
} }
} }
}; };
await executeAzCliCommand("--version", true, execOptions); await executeAzCliCommand("--version", true, execOptions);
core.debug(`az cli version used:\n${output}`); core.debug(`az cli version used:\n${output}`);
let creds = core.getInput('creds', { required: false }); let creds = core.getInput('creds', { required: false });
let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null; let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null;
let environment = core.getInput("environment").toLowerCase(); let environment = core.getInput("environment").toLowerCase();
const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true";
const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true";
//Check for the credentials in individual parameters in the workflow. //Check for the credentials in individual parameters in the workflow.
var servicePrincipalId = core.getInput('client-id', { required: false });; var servicePrincipalId = core.getInput('client-id', { required: false });;
var servicePrincipalKey = null; var servicePrincipalKey = null;
var tenantId = core.getInput('tenant-id', { required: false }); var tenantId = core.getInput('tenant-id', { required: false });
var subscriptionId = core.getInput('subscription-id', { required: false }); var subscriptionId = core.getInput('subscription-id', { required: false });
var resourceManagerEndpointUrl = "https://management.azure.com/"; var resourceManagerEndpointUrl = "https://management.azure.com/";
var enableOIDC = true; var enableOIDC = true;
var federatedToken = null;
// If any of the individual credentials (clent_id, tenat_id, subscription_id) is present.
// If any of the individual credentials (clent_id, tenat_id, subscription_id) is present. if (servicePrincipalId || tenantId || subscriptionId) {
if (servicePrincipalId || tenantId || subscriptionId) {
//If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs.
//If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs. if(!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin)))
if (!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin))) throw new Error("Few credentials are missing.ClientId,tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set.");
throw new Error("Few credentials are missing. ClientId,tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set."); }
} else{
else { if (creds) {
if (creds) { core.debug('using creds JSON...');
core.debug('using creds JSON...'); enableOIDC = false;
enableOIDC = false; servicePrincipalId = secrets.getSecret("$.clientId", true);
servicePrincipalId = secrets.getSecret("$.clientId", true); servicePrincipalKey = secrets.getSecret("$.clientSecret", true);
servicePrincipalKey = secrets.getSecret("$.clientSecret", true); tenantId = secrets.getSecret("$.tenantId", true);
tenantId = secrets.getSecret("$.tenantId", true); subscriptionId = secrets.getSecret("$.subscriptionId", true);
subscriptionId = secrets.getSecret("$.subscriptionId", true); resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false);
resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); }
} else {
else { throw new Error("Credentials are not passed for Login action.");
throw new Error("Credentials are not passed for Login action."); }
} }
} //generic checks
//generic checks //servicePrincipalKey is only required in non-oidc scenario.
//servicePrincipalKey is only required in non-oidc scenario. if (!servicePrincipalId || !tenantId || !(servicePrincipalKey || enableOIDC)) {
if (!servicePrincipalId || !tenantId || !(servicePrincipalKey || enableOIDC)) { throw new Error("Not all values are present in the credentials. Ensure clientId, clientSecret and tenantId are supplied.");
throw new Error("Not all values are present in the credentials. Ensure clientId, clientSecret and tenantId are supplied."); }
} if (!subscriptionId && !allowNoSubscriptionsLogin) {
if (!subscriptionId && !allowNoSubscriptionsLogin) { throw new Error("Not all values are present in the credentials. Ensure subscriptionId is supplied.");
throw new Error("Not all values are present in the credentials. Ensure subscriptionId is supplied."); }
} if (!azureSupportedCloudName.has(environment)) {
if (!azureSupportedCloudName.has(environment)) { throw new Error("Unsupported value for environment is passed.The list of supported values for environment are azureusgovernment', azurechinacloud, azuregermancloud, azurecloud or azurestack");
throw new Error("Unsupported value for environment is passed.The list of supported values for environment are azureusgovernment', azurechinacloud, azuregermancloud, azurecloud or azurestack"); }
}
// OIDC specific checks
// OIDC specific checks if (enableOIDC) {
if (enableOIDC) { console.log('Using OIDC authentication...')
console.log('Using OIDC authentication...') //generating ID-token
//generating ID-token var idToken = await core.getIDToken('api://AzureADTokenExchange');
federatedToken = await core.getIDToken('api://AzureADTokenExchange'); if (!!idToken) {
if (!!federatedToken) { if (environment != "azurecloud")
if (environment != "azurecloud") throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`);
throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`); if (enableAzPSSession)
} throw new Error(`Powershell login is not supported with OIDC.`);
else { }
throw new Error("Could not get ID token for authentication."); else {
} throw new Error("Could not get ID token for authentication.");
} }
}
// Attempting Az cli login
if (environment == "azurestack") { // Attempting Az cli login
if (!resourceManagerEndpointUrl) { if (environment == "azurestack") {
throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined."); if (!resourceManagerEndpointUrl) {
} throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined.");
}
console.log(`Unregistering cloud: "${environment}" first if it exists`);
try { console.log(`Unregistering cloud: "${environment}" first if it exists`);
await executeAzCliCommand(`cloud set -n AzureCloud`, true); try {
await executeAzCliCommand(`cloud unregister -n "${environment}"`, false); await executeAzCliCommand(`cloud set -n AzureCloud`, true);
} await executeAzCliCommand(`cloud unregister -n "${environment}"`, false);
catch (error) { }
console.log(`Ignore cloud not registered error: "${error}"`); catch (error) {
} console.log(`Ignore cloud not registered error: "${error}"`);
}
console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`);
try { console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`);
let baseUri = resourceManagerEndpointUrl; try {
if (baseUri.endsWith('/')) { let baseUri = resourceManagerEndpointUrl;
baseUri = baseUri.substring(0, baseUri.length - 1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below if (baseUri.endsWith('/')) {
} baseUri = baseUri.substring(0, baseUri.length - 1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below
let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with . }
let suffixStorage = baseUri.substring(baseUri.indexOf('.') + 1); // storage suffix starts without . let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with .
let profileVersion = "2019-03-01-hybrid"; let suffixStorage = baseUri.substring(baseUri.indexOf('.') + 1); // storage suffix starts without .
await executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false); let profileVersion = "2019-03-01-hybrid";
} await executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false);
catch (error) { }
core.error(`Error while trying to register cloud "${environment}": "${error}"`); catch (error) {
} core.error(`Error while trying to register cloud "${environment}": "${error}"`);
}
console.log(`Done registering cloud: "${environment}"`)
} console.log(`Done registering cloud: "${environment}"`)
}
await executeAzCliCommand(`cloud set -n "${environment}"`, false);
console.log(`Done setting cloud: "${environment}"`); await executeAzCliCommand(`cloud set -n "${environment}"`, false);
console.log(`Done setting cloud: "${environment}"`);
// Attempting Az cli login
var commonArgs = ["--service-principal", // Attempting Az cli login
"-u", servicePrincipalId, var commonArgs = ["--service-principal",
"--tenant", tenantId "-u", servicePrincipalId,
]; "--tenant", tenantId
if (allowNoSubscriptionsLogin) { ];
commonArgs = commonArgs.concat("--allow-no-subscriptions"); if (allowNoSubscriptionsLogin) {
} commonArgs = commonArgs.concat("--allow-no-subscriptions");
if (enableOIDC) { }
commonArgs = commonArgs.concat("--federated-token", federatedToken); if (enableOIDC) {
} commonArgs = commonArgs.concat("--federated-token", idToken);
else { }
commonArgs = commonArgs.concat("-p", servicePrincipalKey); else {
} commonArgs = commonArgs.concat("-p", servicePrincipalKey);
await executeAzCliCommand(`login`, true, {}, commonArgs); }
await executeAzCliCommand(`login`, true, {}, commonArgs);
if (!allowNoSubscriptionsLogin) {
var args = [ if(!allowNoSubscriptionsLogin){
"--subscription", var args = [
subscriptionId "--subscription",
]; subscriptionId
await executeAzCliCommand(`account set`, true, {}, args); ];
} await executeAzCliCommand(`account set`, true, {}, args);
isAzCLISuccess = true; }
if (enableAzPSSession) { isAzCLISuccess = true;
// Attempting Az PS login if (enableAzPSSession) {
console.log(`Running Azure PS Login`); // Attempting Az PS login
var spnlogin: ServicePrincipalLogin; console.log(`Running Azure PS Login`);
const spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(
spnlogin = new ServicePrincipalLogin( servicePrincipalId,
servicePrincipalId, servicePrincipalKey,
servicePrincipalKey, tenantId,
federatedToken, subscriptionId,
tenantId, allowNoSubscriptionsLogin,
subscriptionId, environment,
allowNoSubscriptionsLogin, resourceManagerEndpointUrl);
environment, await spnlogin.initialize();
resourceManagerEndpointUrl); await spnlogin.login();
await spnlogin.initialize(); }
await spnlogin.login();
} console.log("Login successful.");
}
console.log("Login successful."); catch (error) {
} if (!isAzCLISuccess) {
catch (error) { core.error("Az CLI Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows");
if (!isAzCLISuccess) { }
core.error("Az CLI Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"); else {
} core.error(`Azure PowerShell Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"`);
else { }
core.error(`Azure PowerShell Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"`); core.setFailed(error);
} }
core.setFailed(error); finally {
} // Reset AZURE_HTTP_USER_AGENT
finally { core.exportVariable('AZURE_HTTP_USER_AGENT', prefix);
// Reset AZURE_HTTP_USER_AGENT core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azPSHostEnv);
core.exportVariable('AZURE_HTTP_USER_AGENT', prefix); }
core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azPSHostEnv); }
}
} async function executeAzCliCommand(
command: string,
async function executeAzCliCommand( silent?: boolean,
command: string, execOptions: any = {},
silent?: boolean, args: any = []) {
execOptions: any = {}, execOptions.silent = !!silent;
args: any = []) { try {
execOptions.silent = !!silent; await exec.exec(`"${azPath}" ${command}`, args, execOptions);
try { }
await exec.exec(`"${azPath}" ${command}`, args, execOptions); catch (error) {
} throw new Error(error);
catch (error) { }
throw new Error(error); }
}
} main();
main();