azure-devops-cli-extension
azure-devops-cli-extension copied to clipboard
[Feature Request] CLI for creating Personal Access Tokens
Is your feature request related to a problem? Please describe. Some sort of CLI replacement for the UI at https://dev.azure.com/strivr/_usersSettings/tokens
Describe the solution you'd like
az devops pat create
@lukeschlather Azure-devops does not support creating of Personal Access tokens using an API. Only list functionality is supported.
Was this done or not? I can't seem to find any documentation or anything about this.
There is an API now. It is rather cumbersome. Also I'm not sure it's possible to integrate the API into the CLI because the API doesn't support PAT authentication. I cobbled this together from the docs. Doesn't actually need a secret, just an application client_id that has been set to Public access in the Azure Portal.
import atexit
import datetime
import json
import logging
import os
import pprint
import sys
import msal
import requests
# SCOPES = [
# # This scope is not strictly speaking necessary.
# "User.ReadBasic.All",
# "499b84ac-1321-427f-aa17-267ca6975798/.default"
# ]
# apparently Azure Devops team is too good for human-readable scopes.
SCOPES = ["499b84ac-1321-427f-aa17-267ca6975798/.default"]
ENDPOINT = "https://graph.microsoft.com/v1.0/users"
# Optional logging
# logging.basicConfig(level=logging.DEBUG) # Enable DEBUG log for entire script
# logging.getLogger("msal").setLevel(logging.INFO) # Optionally disable MSAL DEBUG logs
class AuthError(RuntimeError):
pass
class MicrosoftGraphClient:
# https://portal.azure.com/ => Azure Active Directory => Sidebar App Registrations
# Azure Devops PAT generator
# Secret file should be json with 2 fields: tenant_id, client_id
def __init__(self, collection, app_secret_file="app_secret.json"):
self.pat_endpoint = f'https://vssps.dev.azure.com/{collection}/_apis/Tokens/Pats?api-version=6.1-preview'
with open(app_secret_file) as raw_secret:
secrets = json.loads(raw_secret.read())
self.authority = "https://login.microsoftonline.com/" + secrets["tenant_id"]
self.client_id = secrets["client_id"]
self.cache = msal.SerializableTokenCache()
if os.path.exists("my_cache.bin"):
self.cache.deserialize(open("my_cache.bin", "r").read())
atexit.register(lambda:
open("my_cache.bin", "w").write(self.cache.serialize())
# The following optional line persists only when state changed
if self.cache.has_state_changed else None
)
# Create a preferably long-lived app instance which maintains a token cache.
client_app = msal.PublicClientApplication(
self.client_id,
#client_credential=self.client_secret,
authority=self.authority,
token_cache=self.cache
)
# The pattern to acquire a token looks like this.
result = None
# Note: If your device-flow app does not have any interactive ability, you can
# completely skip the following cache part. But here we demonstrate it anyway.
# We now check the cache to see if we have some end users signed in before.
accounts = client_app.get_accounts()
if accounts:
logging.info("Account(s) exists in cache, probably with token too. Let's try.")
print("Pick the account you want to use to proceed:")
for a in accounts:
print(a["username"])
# Assuming the end user chose this one
chosen = accounts[0]
# Now let's try to find a token in cache for this account
result = client_app.acquire_token_silent(SCOPES, account=chosen)
if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
# this doesn't work... says it needs a client secret... but you can't use one.
flow = client_app.initiate_device_flow(scopes=SCOPES)
if "user_code" not in flow:
raise ValueError(
"Fail to create device flow. Err: %s" % json.dumps(flow, indent=4))
print(flow["message"])
sys.stdout.flush() # Some terminal needs this to ensure the message is shown
# Ideally you should wait here, in order to save some unnecessary polling
input("Press Enter after signing in from another device to proceed, CTRL+C to abort.")
result = client_app.acquire_token_by_device_flow(flow) # By default it will block
if "access_token" not in result:
print(result.get("error"))
print(result.get("error_description"))
print(result.get("correlation_id")) # You may need this when reporting a bug
raise AuthError(result.get("error"))
self.access_token = result['access_token']
def make_graph_call(self, endpoint):
graph_data = requests.get(
endpoint,
headers={'Authorization': 'Bearer ' + self.access_token}
).json()
print("Graph API call result: %s" % json.dumps(graph_data, indent=2))
def get_pats(self):
pats = requests.get(
self.pat_endpoint,
headers={'Authorization': 'Bearer ' + self.access_token}
).json()
return pats
def create_pat(self, body):
# two years is the max in the UI. A little less so we don't have to
# worry about timezone and leap year shenanigans.
max_validity = ( datetime.datetime.today() + datetime.timedelta(days=(365*4)) ).isoformat()
defaults = {
"validTo": max_validity,
"allOrgs": False
}
body = {
**defaults,
**body,
}
pats = requests.post(
self.pat_endpoint,
json=body,
headers={'Authorization': 'Bearer ' + self.access_token}
).json()
return pat
def delete_pat(self, authorization_id):
pat = requests.delete(
self.pat_endpoint,
params={
'authorizationId': authorization_id
},
headers={'Authorization': 'Bearer ' + self.access_token}
)
return pat
client = MicrosoftGraphClient(app_secret_file="/secrets/pat-generator-client-secret/app_secret.json")
all_pats = client.get_pats()
pprint.pprint(all_pats)
this is how you can create a PAT
1. Update these values
azdoOrganizationName="OrgName" patDisplayName="PAT"
2. Run az login if not using cloud shell
3. Run the below command
az rest --method post --uri "https://vssps.dev.azure.com/$azdoOrganizationName/_apis/Tokens/Pats?api-version=6.1-preview" --resource "https://management.core.windows.net/" --body '{ "displayName": "$patDisplayName" }' --headers Content-Type=application/json
4. Validate you token
az rest --method get --uri "https://vssps.dev.azure.com/$azdoOrganizationName/_apis/Tokens/Pats?api-version=6.1-preview" --resource "https://management.core.windows.net/" --headers Content-Type=application/json