Как я могу получить доступ к файлам 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 вместо общего секрета (секрета клиента). Наиболее полезные ресурсы, по крайней мере для меня, чтобы получить эту работу:

Вот некоторый код 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
Другие вопросы по тегам