diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py index e3edb1e1a34..ed1923cec35 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_constants.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_constants.py @@ -37,6 +37,12 @@ "USSec West", "USSec East" } +GITHUB_OAUTH_CLIENT_ID = "8d8e1f6000648c575489" +GITHUB_OAUTH_SCOPES = [ + "admin:repo_hook", + "repo", + "workflow" +] class FUNCTIONS_STACKS_API_KEYS(): diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py b/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py new file mode 100644 index 00000000000..b1c28159f10 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/appservice/_github_oauth.py @@ -0,0 +1,83 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from azure.cli.core.azclierror import (ValidationError, CLIInternalError, UnclassifiedUserFault) +from knack.log import get_logger + +from ._constants import (GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_SCOPES) + +logger = get_logger(__name__) + + +''' +Get Github personal access token following Github oauth for command line tools +https://docs.github.com/en/developers/apps/authorizing-oauth-apps#device-flow +''' + + +def get_github_access_token(cmd, scope_list=None): + if scope_list: + for scope in scope_list: + if scope not in GITHUB_OAUTH_SCOPES: + raise ValidationError("Requested github oauth scope is invalid") + scope_list = ' '.join(scope_list) + + authorize_url = 'https://github.com/login/device/code' + authorize_url_data = { + 'scope': scope_list, + 'client_id': GITHUB_OAUTH_CLIENT_ID + } + + import base64 + import json + import requests + import time + from urllib.parse import urlparse, parse_qs + + try: + response = requests.post(authorize_url, data=authorize_url_data) + parsed_response = parse_qs(response.content.decode('ascii')) + + device_code = parsed_response['device_code'][0] + user_code = parsed_response['user_code'][0] + verification_uri = parsed_response['verification_uri'][0] + interval = int(parsed_response['interval'][0]) + expires_in_seconds = int(parsed_response['expires_in'][0]) + logger.warning('Please navigate to %s and enter the user code %s to activate and ' + 'retrieve your github personal access token', verification_uri, user_code) + + timeout = time.time() + expires_in_seconds + logger.warning("Waiting up to '%s' minutes for activation", str(expires_in_seconds // 60)) + + confirmation_url = 'https://github.com/login/oauth/access_token' + confirmation_url_data = { + 'client_id': GITHUB_OAUTH_CLIENT_ID, + 'device_code': device_code, + 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code' + } + + pending = True + while pending: + time.sleep(interval) + + if time.time() > timeout: + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') + + confirmation_response = requests.post(confirmation_url, data=confirmation_url_data) + parsed_confirmation_response = parse_qs(confirmation_response.content.decode('ascii')) + + if 'error' in parsed_confirmation_response and parsed_confirmation_response['error'][0]: + if parsed_confirmation_response['error'][0] == 'slow_down': + interval += 5 # if slow_down error is received, 5 seconds is added to minimum polling interval + elif parsed_confirmation_response['error'][0] != 'authorization_pending': + pending = False + + if 'access_token' in parsed_confirmation_response and parsed_confirmation_response['access_token'][0]: + return parsed_confirmation_response['access_token'][0] + except Exception as e: + raise CLIInternalError( + 'Error: {}. Please try again, or retrieve personal access token from the Github website'.format(e)) + + raise UnclassifiedUserFault('Activation did not happen in time. Please try again') diff --git a/src/azure-cli/azure/cli/command_modules/appservice/_params.py b/src/azure-cli/azure/cli/command_modules/appservice/_params.py index 401f2110609..c2492eda865 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/_params.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/_params.py @@ -289,6 +289,7 @@ def load_arguments(self, _): c.argument('repository_type', help='repository type', arg_type=get_enum_type(['git', 'mercurial', 'vsts', 'github', 'externalgit', 'localgit'])) c.argument('git_token', help='Git access token required for auto sync') + c.argument('github_action', options_list=['--github-action'], help='If using github action, default to False') with self.argument_context(scope + ' identity') as c: c.argument('scope', help="The scope the managed identity has access to") c.argument('role', help="Role name or id the managed identity will be assigned") diff --git a/src/azure-cli/azure/cli/command_modules/appservice/custom.py b/src/azure-cli/azure/cli/command_modules/appservice/custom.py index dc296e399e5..0e5901202e2 100644 --- a/src/azure-cli/azure/cli/command_modules/appservice/custom.py +++ b/src/azure-cli/azure/cli/command_modules/appservice/custom.py @@ -1561,7 +1561,7 @@ def config_source_control(cmd, resource_group_name, name, repo_url, repository_t manual_integration=None, git_token=None, slot=None, cd_app_type=None, app_working_dir=None, nodejs_task_runner=None, python_framework=None, python_version=None, cd_account_create=None, cd_project_url=None, test=None, - slot_swap=None, private_repo_username=None, private_repo_password=None): + slot_swap=None, private_repo_username=None, private_repo_password=None, github_action=None): client = web_client_factory(cmd.cli_ctx) location = _get_location_from_webapp(client, resource_group_name, name) @@ -1602,7 +1602,7 @@ def config_source_control(cmd, resource_group_name, name, repo_url, repository_t source_control = SiteSourceControl(location=location, repo_url=repo_url, branch=branch, is_manual_integration=manual_integration, - is_mercurial=(repository_type != 'git')) + is_mercurial=(repository_type != 'git'), is_git_hub_action=bool(github_action)) # SCC config can fail if previous commands caused SCMSite shutdown, so retry here. for i in range(5): diff --git a/src/azure-cli/requirements.py3.Darwin.txt b/src/azure-cli/requirements.py3.Darwin.txt index e6d92db98f2..1dee597174a 100644 --- a/src/azure-cli/requirements.py3.Darwin.txt +++ b/src/azure-cli/requirements.py3.Darwin.txt @@ -118,6 +118,7 @@ pbr==5.3.1 portalocker==1.7.1 psutil==5.8.0 pycparser==2.19 +PyGithub==1.38 PyJWT==1.7.1 PyNaCl==1.4.0 pyOpenSSL==19.0.0 diff --git a/src/azure-cli/requirements.py3.Linux.txt b/src/azure-cli/requirements.py3.Linux.txt index 1d8c9053d49..29c34cd7030 100644 --- a/src/azure-cli/requirements.py3.Linux.txt +++ b/src/azure-cli/requirements.py3.Linux.txt @@ -118,6 +118,7 @@ pbr==5.3.1 portalocker==1.7.1 psutil==5.8.0 pycparser==2.19 +PyGithub==1.38 PyJWT==1.7.1 PyNaCl==1.4.0 pyOpenSSL==19.0.0 diff --git a/src/azure-cli/requirements.py3.windows.txt b/src/azure-cli/requirements.py3.windows.txt index b6451eedd77..5c419d8c0bb 100644 --- a/src/azure-cli/requirements.py3.windows.txt +++ b/src/azure-cli/requirements.py3.windows.txt @@ -117,6 +117,7 @@ pbr==5.3.1 portalocker==1.7.1 psutil==5.8.0 pycparser==2.19 +PyGithub==1.38 PyJWT==1.7.1 PyNaCl==1.4.0 pyOpenSSL==19.0.0 diff --git a/src/azure-cli/setup.py b/src/azure-cli/setup.py index d20db31a37c..d5b760952ed 100644 --- a/src/azure-cli/setup.py +++ b/src/azure-cli/setup.py @@ -137,6 +137,7 @@ 'jsmin~=2.2.2', 'jsondiff==1.2.0', 'packaging~=20.9', + 'PyGithub==1.38', 'pytz==2019.1', 'scp~=0.13.2', 'semver==2.13.0',