Я пытаюсь получить список всех пользователей в каталоге Exchange с помощью конечной точки API https://graph.microsoft.com/v1.0/users/
Это работает с помощью Graph Explorer , я получаю предварительный просмотр со списком из 100 действительных результатов.
Однако при доступе к той же конечной точке из моего приложения Python, входе в систему с тем же пользователем, я получаю следующий ответ:
{'error': {'code': 'Authorization_RequestDenied', 'message': 'Insufficient privileges to complete the operation.', 'innerError': {'request-id': '6924dba1-83ee-4865-9dcf-76b6cafcb808', 'date': '2019-07-04T21:08:28'}}}
Это код Python, который я использую, большая часть которого скопирована из python-sample-console-app
CLIENT_ID = '765bdd8d-b33d-489b-a039-3cdf41223aa4'
AUTHORITY_URL = 'https://login.microsoftonline.com/common'
RESOURCE = 'https://graph.microsoft.com'
API_VERSION = 'v1.0'
import base64
import mimetypes
import os
import urllib
import webbrowser
from adal import AuthenticationContext
import pyperclip
import requests
def device_flow_session(client_id, auto=False):
"""Obtain an access token from Azure AD (via device flow) and create
a Requests session instance ready to make authenticated calls to
Microsoft Graph.
client_id = Application ID for registered "Azure AD only" V1-endpoint app
auto = whether to copy device code to clipboard and auto-launch browser
Returns Requests session object if user signed in successfully. The session
includes the access token in an Authorization header.
User identity must be an organizational account (ADAL does not support MSAs).
"""
ctx = AuthenticationContext(AUTHORITY_URL, api_version=None)
device_code = ctx.acquire_user_code(RESOURCE, client_id)
# display user instructions
if auto:
pyperclip.copy(device_code['user_code']) # copy user code to clipboard
webbrowser.open(device_code['verification_url']) # open browser
print(f'The code {device_code["user_code"]} has been copied to your clipboard, '
f'and your web browser is opening {device_code["verification_url"]}. '
'Paste the code to sign in.')
else:
print(device_code['message'])
token_response = ctx.acquire_token_with_device_code(RESOURCE,
device_code,
client_id)
if not token_response.get('accessToken', None):
return None
session = requests.Session()
session.headers.update({'Authorization': f'Bearer {token_response["accessToken"]}',
'SdkVersion': 'sample-python-adal',
'x-client-SKU': 'sample-python-adal'})
return session
def api_endpoint(url):
"""Convert a relative path such as /me/photo/$value to a full URI based
on the current RESOURCE and API_VERSION settings in config.py.
"""
if urllib.parse.urlparse(url).scheme in ['http', 'https']:
return url # url is already complete
return urllib.parse.urljoin(f'{RESOURCE}/{API_VERSION}/',
url.lstrip('/'))
session = device_flow_session(CLIENT_ID, True)
users = session.get(api_endpoint('users'))
print(users.json())
У меня также есть следующие разрешения, установленные на портале Microsoft Azure:
В частности, User.ReadBasic.All
должно быть достаточно, чтобы получить список всех пользователей только с базовыми свойствами, такими как имя и адрес электронной почты, но он по-прежнему не работает.
Видимо, в проблеме отсутствовало "согласие пользователя". Здесь происходит некоторая странность, потому что теоретически страница входа должна автоматически запрашивать согласие пользователя для всех настроенных разрешений API. После еще нескольких попыток я нашел следующие решения:
Решение 1
- Зарегистрируйте новое приложение на portal.azure.com
- Заранее добавьте необходимые настройки и разрешения API
- При изменении необходимых разрешений API пользователь больше не запрашивается
- Повторно создайте приложение на portal.azure.com в любое время, когда вам нужно получить доступ к новому API, и соответственно измените идентификатор приложения в вашем приложении
Решение 2
- Обнаружить ошибку Authorization_RequestDenied
- В этом случае откройте окно браузера со специальным URL-адресом для запроса согласия пользователя
- Попросить пользователя перезапустить приложение после того, как он успешно вошел в систему и дал согласие
- Вот соответствующий код
CONSENT_URL = f'https://login.microsoftonline.com/common/oauth2/authorize?client_id={CLIENT_ID}&response_type=code&response_mode=query&resource=https://graph.microsoft.com&state=12345&prompt=consent'
#...
response = session.get(api_endpoint('users')).json()
if 'error' in response and response['error']['code'] == 'Authorization_RequestDenied':
print("Access denied. Consent page has been opened in the webbrowser. Please give user consent, then start the script again!")
webbrowser.open(CONSENT_URL)
sys.exit(1)