Как я могу получить доступ к файлам OneDrive нескольких пользователей через пользовательский демон и API REST Graph?
Мы работаем над сервисом-демоном, который периодически автоматически подключается к Microsoft Graph API, чтобы перечислять любые файлы на всех дисках пользователя с конфиденциальным содержимым. Мы настроили пользовательское приложение в нашей учетной записи клиента Azure/Office365, для которого включено много привилегий (все привилегии Graph и Sharepoint (плюс некоторые другие), для тестирования).
Используя инструмент Graph Explorer и мою личную учетную запись, я могу перечислять файлы в своей учетной записи на диске, используя /me/drive/root/children
конечная точка и /users('<user-id>')/drive/root/children
конечная точка (когда идентификатор пользователя мой). Когда я пытаюсь подключиться с помощью curl и grant_type
из client_credentials
, с использованием client_id
а также client_secret
из нашего пользовательского приложения в Azure, /users('<user-id>')/drive
возвращает правильный идентификатор диска, но /users('<user-id>')/drive/root/children
просто возвращает пустой список детей.
Есть ли какое-то разрешение, которое мне не хватает, которое нам нужно где-то установить?
Это ограничение текущего состояния Graph API?
Это ограничение client_credentials
тип гранта?
2 ответа
Это ограничение текущего состояния API -интерфейса Graph - не существует области разрешений только для приложения, которая будет использоваться с потоком учетных данных клиента, который позволял бы приложению получать доступ к диску / файлам любого пользователя. Области "Файлы.*" Могут использоваться только как делегированные разрешения - см. https://graph.microsoft.io/en-us/docs/authorization/permission_scopes.
Сегодня это возможно (с разрешениями приложений) с помощью нового Microsoft App Dev Portal и следуя приведенным здесь инструкциям. Или, если вы создали (зарегистрировали) свое приложение на портале Azure, вы должны использовать сертификат X509 вместо общего секрета (секрета клиента). Наиболее полезные ресурсы, по крайней мере для меня, чтобы получить эту работу:
- https://msdn.microsoft.com/en-us/office/office365/howto/building-service-apps-in-office-365
- https://blogs.msdn.microsoft.com/richard_dizeregas_blog/2015/05/03/performing-app-only-operations-on-sharepoint-online-through-azure-ad/
Вот некоторый код Python (для второго случая), который генерирует URL-адрес для посещения пользователем, чтобы он мог авторизовать ваше приложение и для запроса токена доступа:
import calendar
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from datetime import datetime, timedelta
import jwt
from jwt.exceptions import InvalidTokenError
from oauthlib.common import generate_nonce, generate_token
from oauthlib.oauth2 import BackendApplicationClient
import requests
from requests_oauthlib import OAuth2Session
import uuid
def to_unix(obj):
if isinstance(obj, datetime):
if obj.utcoffset() is not None:
obj = obj - obj.utcoffset()
millis = calendar.timegm(obj.timetuple()) + obj.microsecond / 1e6
return millis
def validate_id_token(token):
'''Validates the given id token.
Args:
token (str): An encoded ID token.
Returns:
The decoded token which is a dict.
'''
# Extract kid from token header
try:
header = jwt.get_unverified_header(token)
except InvalidTokenError as e:
raise Exception('No valid id token provided.')
})
else:
kid = header.get('kid', '')
if not kid:
raise Exception("Unable to find 'kid' claim in token header.")
# Fetch public key info
url = 'https://login.microsoftonline.com/common/discovery/keys'
try:
response = requests.get(url)
except RequestException as e:
raise Exception('Failed to get public key info: %s' % e)
else:
if not response.ok:
raise Exception('Failed to get public key info: %s' %
response.content)
else:
public_keys = response.json().get('keys', [])
# Find public key, used to sign id token
public_key = None
for k in public_keys:
if kid == k['kid']:
public_key = k['x5c'][0]
break
if not public_key:
raise Exception("Unable to find public key for given kid '%s'" % kid)
# Verify id token signature
# NOTE: The x5c value is actually a X509 certificate. The public key
# could also be generated from the n (modulos) and e (exponent) values.
# But that's more involved.
cert_string = ('-----BEGIN CERTIFICATE-----\n' +
public_key +
'\n-----END CERTIFICATE-----').encode('UTF-8')
try:
cert = x509.load_pem_x509_certificate(
cert_string, default_backend())
except ValueError as e:
raise Exception('Failed to load certificate for token signature'
'verification: %s' % e)
else:
public_key = cert.public_key()
try:
decoded = jwt.decode(token, public_key, audience=self.key)
except InvalidTokenError as e:
raise Exception('Failed to decode token: %s' % e)
else:
return decoded
def generate_client_assertion(tenant_id, fp_hash, private_key, private_key_passphrase):
"""Generate a client assertion (jwt token).
This token is required to fetch an oauth app-only access token.
Args:
fp_hash (str): Base64 encoded SHA1 has of certificate fingerprint
private_key (str): Private key used to sign the jwt token
tenant_id (str): The tenant to which this token is bound.
Returns:
On success a tuple of the client assertion and the token type
indicator.
"""
valid_from = str(int(ts.to_unix(datetime.utcnow() - timedelta(0, 1))))
expires_at = str(int(ts.to_unix(datetime.utcnow() + timedelta(7))))
jwt_payload = {
'aud': ('https://login.microsoftonline.com/%s/'
'oauth2/token' % tenant_id),
'iss': client_id,
'sub': client_id,
'jti': str(uuid.uuid1()),
'nbf': valid_from,
'exp': expires_at,
}
headers = {
'x5t': fp_hash
}
if not private_key_passphrase:
secret = private_key
else:
try:
secret = serialization.load_pem_private_key(
str(private_key), password=str(private_key_passphrase),
backend=default_backend())
except Exception as e:
raise Exception('Failed to load private key: %s' % e)
try:
client_assertion = jwt.encode(jwt_payload, secret,
algorithm='RS256', headers=headers)
except ValueError as e:
raise Exception('Failed to encode jwt_payload: %s' % e)
client_assertion_type = ('urn:ietf:params:oauth:client-assertion-type:'
'jwt-bearer')
return client_assertion, client_assertion_type
def generate_auth_url(client_id, redirect_uri):
nonce = generate_nonce()
state = generate_token()
query_params = {
'client_id': client_id,
'nonce': nonce,
'prompt': 'admin_consent',
'redirect_uri': redirect_uri,
'response_mode': 'fragment',
'response_type': 'id_token',
'scope': 'openid',
'state': state
}
tenant = 'common'
auth_url = ('https://login.microsoftonline.com/%s'
'/oauth2/authorize?%s') % (tenant, urllib.urlencode(query_params))
return nonce, auth_url
def get_access_token(client_id, id_token, nonce=None):
'''id_token is returned w/ the url after the user authorized the app'''
decoded_id_token = validate_id_token(id_token)
# Compare the nonce values, to mitigate token replay attacks
if not nonce:
raise Exception("No nonce value provided.")
elif nonce != decoded_id_token['nonce']:
raise Exception("Nonce values don't match!")
# Prepare the JWT token for fetching an access token
tenant_id = decoded_id_token['tid']
client_assertion, client_assertion_type = generate_client_assertion(tenant_id)
# Fetch the access token
client = BackendApplicationClient(self.key)
oauth = OAuth2Session(client=client)
resource = 'https://graph.microsoft.com/'
url = https://login.microsoftonline.com/common/oauth2/token
query_params = {
'client_id': client_id,
'client_assertion': client_assertion,
'client_assertion_type': client_assertion_type,
'resource': resource
}
try:
fetch_token_response = oauth.fetch_token(url, **query_params)
except Exception as e:
raise Exception('Failed to obtain access token: %s' % e)
else:
return fetch_token_response