From 213de0bda43c7f53ff948d15663d9d978fa23b23 Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Wed, 3 Jan 2024 02:50:11 -0800 Subject: [PATCH 1/2] generated with codegen at box/box-codegen@766152d and spec at box/box-openapi@e121116 --- box_sdk_gen/managers/terms_of_services.py | 6 +- box_sdk_gen/schemas.py | 203 ++++++++++++---------- docs/storage_policies.md | 12 +- docs/terms_of_services.md | 2 +- test/storage_policies.py | 26 +++ 5 files changed, 147 insertions(+), 102 deletions(-) create mode 100644 test/storage_policies.py diff --git a/box_sdk_gen/managers/terms_of_services.py b/box_sdk_gen/managers/terms_of_services.py index 6a9a60a..889bb35 100644 --- a/box_sdk_gen/managers/terms_of_services.py +++ b/box_sdk_gen/managers/terms_of_services.py @@ -14,8 +14,6 @@ from box_sdk_gen.schemas import ClientError -from box_sdk_gen.schemas import Task - from box_sdk_gen.schemas import TermsOfService from box_sdk_gen.auth import Authentication @@ -110,7 +108,7 @@ def create_terms_of_service( text: str, tos_type: Optional[CreateTermsOfServiceTosType] = None, extra_headers: Optional[Dict[str, Optional[str]]] = None, - ) -> Task: + ) -> TermsOfService: """ Creates a terms of service for a given enterprise @@ -143,7 +141,7 @@ def create_terms_of_service( network_session=self.network_session, ), ) - return deserialize(response.data, Task) + return deserialize(response.data, TermsOfService) def get_terms_of_service_by_id( self, diff --git a/box_sdk_gen/schemas.py b/box_sdk_gen/schemas.py index 9ece1c3..8976fb1 100644 --- a/box_sdk_gen/schemas.py +++ b/box_sdk_gen/schemas.py @@ -1295,85 +1295,6 @@ def __init__( self.file_version = file_version -class FileScopeScopeField(str, Enum): - ANNOTATION_EDIT = 'annotation_edit' - ANNOTATION_VIEW_ALL = 'annotation_view_all' - ANNOTATION_VIEW_SELF = 'annotation_view_self' - BASE_EXPLORER = 'base_explorer' - BASE_PICKER = 'base_picker' - BASE_PREVIEW = 'base_preview' - BASE_UPLOAD = 'base_upload' - ITEM_DELETE = 'item_delete' - ITEM_DOWNLOAD = 'item_download' - ITEM_PREVIEW = 'item_preview' - ITEM_RENAME = 'item_rename' - ITEM_SHARE = 'item_share' - - -class FileScope(BaseObject): - def __init__( - self, - scope: Optional[FileScopeScopeField] = None, - object: Optional[FileMini] = None, - **kwargs - ): - """ - :param scope: The file scopes for the file access - :type scope: Optional[FileScopeScopeField], optional - """ - super().__init__(**kwargs) - self.scope = scope - self.object = object - - -class AccessTokenTokenTypeField(str, Enum): - BEARER = 'bearer' - - -class AccessTokenIssuedTokenTypeField(str, Enum): - URN_IETF_PARAMS_OAUTH_TOKEN_TYPE_ACCESS_TOKEN = ( - 'urn:ietf:params:oauth:token-type:access_token' - ) - - -class AccessToken(BaseObject): - def __init__( - self, - access_token: Optional[str] = None, - expires_in: Optional[int] = None, - token_type: Optional[AccessTokenTokenTypeField] = None, - restricted_to: Optional[List[FileScope]] = None, - refresh_token: Optional[str] = None, - issued_token_type: Optional[AccessTokenIssuedTokenTypeField] = None, - **kwargs - ): - """ - :param access_token: The requested access token. - :type access_token: Optional[str], optional - :param expires_in: The time in seconds by which this token will expire. - :type expires_in: Optional[int], optional - :param token_type: The type of access token returned. - :type token_type: Optional[AccessTokenTokenTypeField], optional - :param restricted_to: The permissions that this access token permits, - providing a list of resources (files, folders, etc) - and the scopes permitted for each of those resources. - :type restricted_to: Optional[List[FileScope]], optional - :param refresh_token: The refresh token for this access token, which can be used - to request a new access token when the current one expires. - :type refresh_token: Optional[str], optional - :param issued_token_type: The type of downscoped access token returned. This is only - returned if an access token has been downscoped. - :type issued_token_type: Optional[AccessTokenIssuedTokenTypeField], optional - """ - super().__init__(**kwargs) - self.access_token = access_token - self.expires_in = expires_in - self.token_type = token_type - self.restricted_to = restricted_to - self.refresh_token = refresh_token - self.issued_token_type = issued_token_type - - class FilesUnderRetention(BaseObject): def __init__( self, @@ -1560,6 +1481,85 @@ def __init__( self.name = name +class FileOrFolderScopeScopeField(str, Enum): + ANNOTATION_EDIT = 'annotation_edit' + ANNOTATION_VIEW_ALL = 'annotation_view_all' + ANNOTATION_VIEW_SELF = 'annotation_view_self' + BASE_EXPLORER = 'base_explorer' + BASE_PICKER = 'base_picker' + BASE_PREVIEW = 'base_preview' + BASE_UPLOAD = 'base_upload' + ITEM_DELETE = 'item_delete' + ITEM_DOWNLOAD = 'item_download' + ITEM_PREVIEW = 'item_preview' + ITEM_RENAME = 'item_rename' + ITEM_SHARE = 'item_share' + + +class FileOrFolderScope(BaseObject): + def __init__( + self, + scope: Optional[FileOrFolderScopeScopeField] = None, + object: Optional[Union[FolderMini, FileMini]] = None, + **kwargs + ): + """ + :param scope: The scopes for the resource access + :type scope: Optional[FileOrFolderScopeScopeField], optional + """ + super().__init__(**kwargs) + self.scope = scope + self.object = object + + +class AccessTokenTokenTypeField(str, Enum): + BEARER = 'bearer' + + +class AccessTokenIssuedTokenTypeField(str, Enum): + URN_IETF_PARAMS_OAUTH_TOKEN_TYPE_ACCESS_TOKEN = ( + 'urn:ietf:params:oauth:token-type:access_token' + ) + + +class AccessToken(BaseObject): + def __init__( + self, + access_token: Optional[str] = None, + expires_in: Optional[int] = None, + token_type: Optional[AccessTokenTokenTypeField] = None, + restricted_to: Optional[List[FileOrFolderScope]] = None, + refresh_token: Optional[str] = None, + issued_token_type: Optional[AccessTokenIssuedTokenTypeField] = None, + **kwargs + ): + """ + :param access_token: The requested access token. + :type access_token: Optional[str], optional + :param expires_in: The time in seconds by which this token will expire. + :type expires_in: Optional[int], optional + :param token_type: The type of access token returned. + :type token_type: Optional[AccessTokenTokenTypeField], optional + :param restricted_to: The permissions that this access token permits, + providing a list of resources (files, folders, etc) + and the scopes permitted for each of those resources. + :type restricted_to: Optional[List[FileOrFolderScope]], optional + :param refresh_token: The refresh token for this access token, which can be used + to request a new access token when the current one expires. + :type refresh_token: Optional[str], optional + :param issued_token_type: The type of downscoped access token returned. This is only + returned if an access token has been downscoped. + :type issued_token_type: Optional[AccessTokenIssuedTokenTypeField], optional + """ + super().__init__(**kwargs) + self.access_token = access_token + self.expires_in = expires_in + self.token_type = token_type + self.restricted_to = restricted_to + self.refresh_token = refresh_token + self.issued_token_type = issued_token_type + + class IntegrationMappingBaseIntegrationTypeField(str, Enum): SLACK = 'slack' @@ -2991,17 +2991,12 @@ class TermsOfServiceBaseTypeField(str, Enum): class TermsOfServiceBase(BaseObject): - def __init__( - self, - id: Optional[str] = None, - type: Optional[TermsOfServiceBaseTypeField] = None, - **kwargs - ): + def __init__(self, id: str, type: TermsOfServiceBaseTypeField, **kwargs): """ :param id: The unique identifier for this terms of service. - :type id: Optional[str], optional + :type id: str :param type: `terms_of_service` - :type type: Optional[TermsOfServiceBaseTypeField], optional + :type type: TermsOfServiceBaseTypeField """ super().__init__(**kwargs) self.id = id @@ -3047,17 +3042,21 @@ class TermsOfServiceTosTypeField(str, Enum): class TermsOfService(TermsOfServiceBase): def __init__( self, + id: str, + type: TermsOfServiceBaseTypeField, status: Optional[TermsOfServiceStatusField] = None, enterprise: Optional[TermsOfServiceEnterpriseField] = None, tos_type: Optional[TermsOfServiceTosTypeField] = None, text: Optional[str] = None, created_at: Optional[str] = None, modified_at: Optional[str] = None, - id: Optional[str] = None, - type: Optional[TermsOfServiceBaseTypeField] = None, **kwargs ): """ + :param id: The unique identifier for this terms of service. + :type id: str + :param type: `terms_of_service` + :type type: TermsOfServiceBaseTypeField :param status: Whether these terms are enabled or not :type status: Optional[TermsOfServiceStatusField], optional :param tos_type: Whether to apply these terms to managed users or external users @@ -3069,10 +3068,6 @@ def __init__( :type created_at: Optional[str], optional :param modified_at: When the legal item was modified. :type modified_at: Optional[str], optional - :param id: The unique identifier for this terms of service. - :type id: Optional[str], optional - :param type: `terms_of_service` - :type type: Optional[TermsOfServiceBaseTypeField], optional """ super().__init__(id=id, type=type, **kwargs) self.status = status @@ -5901,7 +5896,7 @@ def __init__( access_token: Optional[str] = None, expires_in: Optional[int] = None, token_type: Optional[FileFullExpiringEmbedLinkTokenTypeField] = None, - restricted_to: Optional[List[FileScope]] = None, + restricted_to: Optional[List[FileOrFolderScope]] = None, url: Optional[str] = None, **kwargs ): @@ -5915,7 +5910,7 @@ def __init__( :param restricted_to: The permissions that this access token permits, providing a list of resources (files, folders, etc) and the scopes permitted for each of those resources. - :type restricted_to: Optional[List[FileScope]], optional + :type restricted_to: Optional[List[FileOrFolderScope]], optional :param url: The actual expiring embed URL for this file, constructed from the file ID and access tokens specified in this object. :type url: Optional[str], optional @@ -10495,6 +10490,7 @@ def __init__( login_required: Optional[bool] = None, verification_phone_number: Optional[str] = None, password: Optional[str] = None, + signer_group_id: Optional[str] = None, **kwargs ): """ @@ -10537,6 +10533,10 @@ def __init__( :param password: If set, the signer is required to enter the password before they are able to sign a document. This field is write only. :type password: Optional[str], optional + :param signer_group_id: If set, signers who have the same group ID will be assigned to the same input. + A signer group is expected to have more than one signer. When a group contains fewer than two signers, + it will be converted to a single signer and the group will be removed. + :type signer_group_id: Optional[str], optional """ super().__init__(**kwargs) self.email = email @@ -10549,6 +10549,7 @@ def __init__( self.login_required = login_required self.verification_phone_number = verification_phone_number self.password = password + self.signer_group_id = signer_group_id class SignRequestPrefillTag(BaseObject): @@ -10689,6 +10690,7 @@ def __init__( login_required: Optional[bool] = None, verification_phone_number: Optional[str] = None, password: Optional[str] = None, + signer_group_id: Optional[str] = None, **kwargs ): """ @@ -10744,6 +10746,10 @@ def __init__( :param password: If set, the signer is required to enter the password before they are able to sign a document. This field is write only. :type password: Optional[str], optional + :param signer_group_id: If set, signers who have the same group ID will be assigned to the same input. + A signer group is expected to have more than one signer. When a group contains fewer than two signers, + it will be converted to a single signer and the group will be removed. + :type signer_group_id: Optional[str], optional """ super().__init__( email=email, @@ -10756,6 +10762,7 @@ def __init__( login_required=login_required, verification_phone_number=verification_phone_number, password=password, + signer_group_id=signer_group_id, **kwargs ) self.has_viewed_document = has_viewed_document @@ -11224,6 +11231,7 @@ def __init__( role: Optional[TemplateSignerRoleField] = None, is_in_person: Optional[bool] = None, order: Optional[int] = None, + signer_group_id: Optional[str] = None, **kwargs ): """ @@ -11241,6 +11249,10 @@ def __init__( :type is_in_person: Optional[bool], optional :param order: Order of the signer :type order: Optional[int], optional + :param signer_group_id: If set, signers who have the same group ID will be assigned to the same input. + A signer group is expected to have more than one signer. When a group contains fewer than two signers, + it will be converted to a single signer and the group will be removed. + :type signer_group_id: Optional[str], optional """ super().__init__(**kwargs) self.inputs = inputs @@ -11248,6 +11260,7 @@ def __init__( self.role = role self.is_in_person = is_in_person self.order = order + self.signer_group_id = signer_group_id class SignTemplateTypeField(str, Enum): diff --git a/docs/storage_policies.md b/docs/storage_policies.md index b8fb217..64e9d20 100644 --- a/docs/storage_policies.md +++ b/docs/storage_policies.md @@ -12,7 +12,11 @@ This operation is performed by calling function `get_storage_policies`. See the endpoint docs at [API Reference](https://developer.box.com/reference/get-storage-policies/). -_Currently we don't have an example for calling `get_storage_policies` in integration tests_ + + +```python +client.storage_policies.get_storage_policies() +``` ### Arguments @@ -40,7 +44,11 @@ This operation is performed by calling function `get_storage_policy_by_id`. See the endpoint docs at [API Reference](https://developer.box.com/reference/get-storage-policies-id/). -_Currently we don't have an example for calling `get_storage_policy_by_id` in integration tests_ + + +```python +client.storage_policies.get_storage_policy_by_id(storage_policy_id=storage_policy.id) +``` ### Arguments diff --git a/docs/terms_of_services.md b/docs/terms_of_services.md index d2c84de..6680b32 100644 --- a/docs/terms_of_services.md +++ b/docs/terms_of_services.md @@ -56,7 +56,7 @@ _Currently we don't have an example for calling `create_terms_of_service` in int ### Returns -This function returns a value of type `Task`. +This function returns a value of type `TermsOfService`. Returns a new task object diff --git a/test/storage_policies.py b/test/storage_policies.py new file mode 100644 index 0000000..ea4d3da --- /dev/null +++ b/test/storage_policies.py @@ -0,0 +1,26 @@ +from box_sdk_gen.utils import to_string + +from box_sdk_gen.client import BoxClient + +from box_sdk_gen.schemas import StoragePolicies + +from box_sdk_gen.schemas import StoragePolicy + +from box_sdk_gen.utils import get_env_var + +from test.commons import get_default_client_as_user + +user_id: str = get_env_var('USER_ID') + + +def testGetStoragePolicies(): + client: BoxClient = get_default_client_as_user(user_id) + storage_policies: StoragePolicies = client.storage_policies.get_storage_policies() + storage_policy: StoragePolicy = storage_policies.entries[0] + assert to_string(storage_policy.type) == 'storage_policy' + get_storage_policy: StoragePolicy = ( + client.storage_policies.get_storage_policy_by_id( + storage_policy_id=storage_policy.id + ) + ) + assert get_storage_policy.id == storage_policy.id From 921463790a0ea8ffc4047778077041fcac57daeb Mon Sep 17 00:00:00 2001 From: box-sdk-build Date: Mon, 8 Jan 2024 01:11:33 -0800 Subject: [PATCH 2/2] generated with codegen at box/box-codegen@2dac8b6 and spec at box/box-openapi@e121116 --- box_sdk_gen/jwt_auth.py | 577 ++++++++++++++++++++++++-------------- box_sdk_gen/utils.py | 108 +++++++ docs/authentication.md | 63 ++++- docs/task_assignments.md | 30 +- docs/terms_of_services.md | 18 +- test/auth.py | 60 +++- test/commons.py | 25 +- test/task_assignments.py | 85 ++++++ test/terms_of_services.py | 39 +++ 9 files changed, 769 insertions(+), 236 deletions(-) create mode 100644 test/task_assignments.py create mode 100644 test/terms_of_services.py diff --git a/box_sdk_gen/jwt_auth.py b/box_sdk_gen/jwt_auth.py index 8add85a..8f70c6b 100644 --- a/box_sdk_gen/jwt_auth.py +++ b/box_sdk_gen/jwt_auth.py @@ -1,27 +1,157 @@ -from datetime import datetime, timedelta -import random -import string - -from typing import Optional, Any - -try: - import jwt - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import serialization -except ImportError: - jwt, default_backend, serialization = None, None, None - -from .auth import Authentication -from .token_storage import TokenStorage, InMemoryTokenStorage -from .auth_schemas import ( - TokenRequestBoxSubjectType, - TokenRequest, - TokenRequestGrantType, -) -from .fetch import fetch, FetchResponse, FetchOptions -from .network import NetworkSession -from .schemas import AccessToken -from .json_data import json_to_serialized_data +from typing import Dict + +from box_sdk_gen.base_object import BaseObject + +from typing import Optional + +from box_sdk_gen.serialization import deserialize + +from typing import List + +from box_sdk_gen.utils import to_string + +from box_sdk_gen.managers.authorization import RequestAccessTokenGrantType + +from box_sdk_gen.managers.authorization import RequestAccessTokenSubjectTokenType + +from box_sdk_gen.auth import Authentication + +from box_sdk_gen.network import NetworkSession + +from box_sdk_gen.schemas import AccessToken + +from box_sdk_gen.schemas import PostOAuth2Token + +from box_sdk_gen.schemas import PostOAuth2Revoke + +from box_sdk_gen.token_storage import TokenStorage + +from box_sdk_gen.token_storage import InMemoryTokenStorage + +from box_sdk_gen.json_data import json_to_serialized_data + +from box_sdk_gen.json_data import SerializedData + +from box_sdk_gen.utils import get_uuid + +from box_sdk_gen.utils import read_text_from_file + +from box_sdk_gen.utils import is_browser + +from box_sdk_gen.utils import get_epoch_time_in_seconds + +from box_sdk_gen.utils import create_jwt_assertion + +from box_sdk_gen.utils import JwtSignOptions + +from box_sdk_gen.utils import JwtKey + +from box_sdk_gen.utils import JwtAlgorithm + +from box_sdk_gen.managers.authorization import AuthorizationManager + +from box_sdk_gen.utils import to_string + +from box_sdk_gen.json_data import sd_to_json + +box_jwt_audience: str = 'https://api.box.com/oauth2/token' + + +class JwtConfigAppSettingsAppAuth(BaseObject): + _fields_to_json_mapping: Dict[str, str] = { + 'public_key_id': 'publicKeyID', + 'private_key': 'privateKey', + **BaseObject._fields_to_json_mapping, + } + _json_to_fields_mapping: Dict[str, str] = { + 'publicKeyID': 'public_key_id', + 'privateKey': 'private_key', + **BaseObject._json_to_fields_mapping, + } + + def __init__(self, public_key_id: str, private_key: str, passphrase: str, **kwargs): + """ + :param public_key_id: Public key ID + :type public_key_id: str + :param private_key: Private key + :type private_key: str + :param passphrase: Passphrase + :type passphrase: str + """ + super().__init__(**kwargs) + self.public_key_id = public_key_id + self.private_key = private_key + self.passphrase = passphrase + + +class JwtConfigAppSettings(BaseObject): + _fields_to_json_mapping: Dict[str, str] = { + 'client_id': 'clientID', + 'client_secret': 'clientSecret', + 'app_auth': 'appAuth', + **BaseObject._fields_to_json_mapping, + } + _json_to_fields_mapping: Dict[str, str] = { + 'clientID': 'client_id', + 'clientSecret': 'client_secret', + 'appAuth': 'app_auth', + **BaseObject._json_to_fields_mapping, + } + + def __init__( + self, + client_id: str, + client_secret: str, + app_auth: JwtConfigAppSettingsAppAuth, + **kwargs + ): + """ + :param client_id: App client ID + :type client_id: str + :param client_secret: App client secret + :type client_secret: str + :param app_auth: App auth settings + :type app_auth: JwtConfigAppSettingsAppAuth + """ + super().__init__(**kwargs) + self.client_id = client_id + self.client_secret = client_secret + self.app_auth = app_auth + + +class JwtConfigFile(BaseObject): + _fields_to_json_mapping: Dict[str, str] = { + 'enterprise_id': 'enterpriseID', + 'user_id': 'userID', + 'box_app_settings': 'boxAppSettings', + **BaseObject._fields_to_json_mapping, + } + _json_to_fields_mapping: Dict[str, str] = { + 'enterpriseID': 'enterprise_id', + 'userID': 'user_id', + 'boxAppSettings': 'box_app_settings', + **BaseObject._json_to_fields_mapping, + } + + def __init__( + self, + box_app_settings: JwtConfigAppSettings, + enterprise_id: Optional[str] = None, + user_id: Optional[str] = None, + **kwargs + ): + """ + :param box_app_settings: App settings + :type box_app_settings: JwtConfigAppSettings + :param enterprise_id: Enterprise ID + :type enterprise_id: Optional[str], optional + :param user_id: User ID + :type user_id: Optional[str], optional + """ + super().__init__(**kwargs) + self.box_app_settings = box_app_settings + self.enterprise_id = enterprise_id + self.user_id = user_id class JWTConfig: @@ -34,263 +164,276 @@ def __init__( private_key_passphrase: str, enterprise_id: Optional[str] = None, user_id: Optional[str] = None, - jwt_algorithm: str = 'RS256', + jwt_algorithm: Optional[JwtAlgorithm] = 'RS256', token_storage: TokenStorage = None, - **_kwargs ): """ - :param client_id: - Box API key used for identifying the application the user is authenticating with. - :param client_secret: - Box API secret used for making auth requests. - :param jwt_key_id: - Key ID for the JWT assertion. - :param private_key: - Contents of RSA private key, used for signing the JWT assertion. - :param private_key_passphrase: - Passphrase used to unlock the private key. - :param enterprise_id: - The ID of the Box Developer Edition enterprise. - - May be `None`, if the caller knows that it will not be - authenticating as an enterprise instance / service account. - - If `user_id` is passed, this value is not used, unless - `authenticate_enterprise()` is called to authenticate as the enterprise instance. - :param user_id: - The user id to authenticate. This value is not required. But if it is provided, then the user - will be auto-authenticated at the time of the first API call. - - Should be `None` if the intention is to authenticate as the - enterprise instance / service account. If both `enterprise_id` and - `user_id` are non-`None`, the `user` takes precedense when `refresh()` - is called. - - - - :param jwt_algorithm: - Which algorithm to use for signing the JWT assertion. Must be one of 'RS256', 'RS384', 'RS512'. - :param token_storage: - Object responsible for storing token. If no custom implementation provided, - the token will be stored in memory. + :param client_id: App client ID + :type client_id: str + :param client_secret: App client secret + :type client_secret: str + :param jwt_key_id: Public key ID + :type jwt_key_id: str + :param private_key: Private key + :type private_key: str + :param private_key_passphrase: Passphrase + :type private_key_passphrase: str + :param enterprise_id: Enterprise ID + :type enterprise_id: Optional[str], optional + :param user_id: User ID + :type user_id: Optional[str], optional """ if token_storage is None: token_storage = InMemoryTokenStorage() - if not enterprise_id and not user_id: - raise Exception("Enterprise ID or User ID is needed") - self.client_id = client_id self.client_secret = client_secret - self.enterprise_id = enterprise_id - self.user_id = user_id self.jwt_key_id = jwt_key_id self.private_key = private_key self.private_key_passphrase = private_key_passphrase + self.enterprise_id = enterprise_id + self.user_id = user_id self.jwt_algorithm = jwt_algorithm self.token_storage = token_storage - @classmethod + @staticmethod def from_config_json_string( - cls, config_json_string: str, token_storage: TokenStorage = None, **kwargs: Any + config_json_string: str, token_storage: Optional[TokenStorage] = None ) -> 'JWTConfig': """ Create an auth instance as defined by a string content of JSON file downloaded from the Box Developer Console. + See https://developer.box.com/en/guides/authentication/jwt/ for more information. - :param config_json_string: - String content of JSON file containing the configuration. - :param token_storage: - Object responsible for storing token. If no custom implementation provided, - the token will be stored in memory. - :return: - Auth instance configured as specified by the config dictionary. + :param config_json_string: String content of JSON file containing the configuration. + :type config_json_string: str + :param token_storage: Object responsible for storing token. If no custom implementation provided, the token will be stored in memory.g + :type token_storage: Optional[TokenStorage], optional """ - config_dict: dict = json_to_serialized_data(config_json_string) - if 'boxAppSettings' not in config_dict: - raise ValueError('boxAppSettings not present in configuration') - return cls( - client_id=config_dict['boxAppSettings']['clientID'], - client_secret=config_dict['boxAppSettings']['clientSecret'], - enterprise_id=config_dict.get('enterpriseID', None), - jwt_key_id=config_dict['boxAppSettings']['appAuth'].get( - 'publicKeyID', None - ), - private_key=config_dict['boxAppSettings']['appAuth'].get( - 'privateKey', None - ), - private_key_passphrase=config_dict['boxAppSettings']['appAuth'].get( - 'passphrase', None - ), - token_storage=token_storage, - **kwargs + config_json: JwtConfigFile = deserialize( + json_to_serialized_data(config_json_string), JwtConfigFile + ) + new_config = ( + JWTConfig( + client_id=config_json.box_app_settings.client_id, + client_secret=config_json.box_app_settings.client_secret, + enterprise_id=config_json.enterprise_id, + user_id=config_json.user_id, + jwt_key_id=config_json.box_app_settings.app_auth.public_key_id, + private_key=config_json.box_app_settings.app_auth.private_key, + private_key_passphrase=config_json.box_app_settings.app_auth.passphrase, + token_storage=token_storage, + ) + if not token_storage == None + else JWTConfig( + client_id=config_json.box_app_settings.client_id, + client_secret=config_json.box_app_settings.client_secret, + enterprise_id=config_json.enterprise_id, + user_id=config_json.user_id, + jwt_key_id=config_json.box_app_settings.app_auth.public_key_id, + private_key=config_json.box_app_settings.app_auth.private_key, + private_key_passphrase=config_json.box_app_settings.app_auth.passphrase, + ) ) + return new_config - @classmethod + @staticmethod def from_config_file( - cls, config_file_path: str, token_storage: TokenStorage = None, **kwargs: Any + config_file_path: str, token_storage: Optional[TokenStorage] = None ) -> 'JWTConfig': """ Create an auth instance as defined by a JSON file downloaded from the Box Developer Console. + See https://developer.box.com/en/guides/authentication/jwt/ for more information. - :param config_file_path: - Path to the JSON file containing the configuration. - :param token_storage: - Object responsible for storing token. If no custom implementation provided, - the token will be stored in memory. - :return: - Auth instance configured as specified by the JSON file. + :param config_file_path: Path to the JSON file containing the configuration. + :type config_file_path: str + :param token_storage: Object responsible for storing token. If no custom implementation provided, the token will be stored in memory. + :type token_storage: Optional[TokenStorage], optional """ - with open(config_file_path, encoding='utf-8') as config_file: - return cls.from_config_json_string( - config_file.read(), token_storage, **kwargs - ) + config_json_string: str = read_text_from_file(config_file_path) + return JWTConfig.from_config_json_string(config_json_string, token_storage) class BoxJWTAuth(Authentication): - def __init__(self, config: JWTConfig): + def __init__(self, config: JWTConfig, **kwargs): """ - :param config: - Configuration object of Client Credentials Grant auth. + :param config: An object containing all JWT configuration to use for authentication + :type config: JWTConfig """ - if None in (default_backend, serialization, jwt): - raise Exception( - 'Missing dependencies required for JWTAuth. To install them use' - ' command: `pip install box-sdk-gen[jwt]`' - ) - + super().__init__(**kwargs) self.config = config - self.token_storage = config.token_storage - - if config.enterprise_id: - self.subject_type = TokenRequestBoxSubjectType.ENTERPRISE - self.subject_id = self.config.enterprise_id - else: - self.subject_id = self.config.user_id - self.subject_type = TokenRequestBoxSubjectType.USER - - self._rsa_private_key = self._get_rsa_private_key( - config.private_key, config.private_key_passphrase + self.token_storage = self.config.token_storage + self.subject_id = ( + self.config.enterprise_id + if not self.config.enterprise_id == None + else self.config.user_id + ) + self.subject_type = ( + 'enterprise' if not self.config.enterprise_id == None else 'user' ) - def retrieve_token( + def refresh_token( self, network_session: Optional[NetworkSession] = None ) -> AccessToken: """ - Return a current token or get a new one when not available. + Get new access token using JWT auth. :param network_session: An object to keep network session state - :return: Access token + :type network_session: Optional[NetworkSession], optional """ - token = self.token_storage.get() - if token is None: - return self.refresh_token(network_session=network_session) + if is_browser(): + raise Exception('JWT auth is not supported in browser environment.') + alg: JwtAlgorithm = ( + self.config.jwt_algorithm + if not self.config.jwt_algorithm == None + else 'RS256' + ) + claims: Dict = { + 'exp': get_epoch_time_in_seconds() + 30, + 'box_sub_type': self.subject_type, + } + jwt_options: JwtSignOptions = JwtSignOptions( + algorithm=alg, + audience=box_jwt_audience, + subject=self.subject_id, + issuer=self.config.client_id, + jwtid=get_uuid(), + keyid=self.config.jwt_key_id, + ) + jwt_key: JwtKey = JwtKey( + key=self.config.private_key, passphrase=self.config.private_key_passphrase + ) + assertion: str = create_jwt_assertion(claims, jwt_key, jwt_options) + auth_manager: AuthorizationManager = ( + AuthorizationManager(network_session=network_session) + if not network_session == None + else AuthorizationManager() + ) + token: AccessToken = auth_manager.request_access_token( + grant_type=RequestAccessTokenGrantType.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_JWT_BEARER.value, + assertion=assertion, + client_id=self.config.client_id, + client_secret=self.config.client_secret, + ) + self.token_storage.store(token) return token - def refresh_token( + def retrieve_token( self, network_session: Optional[NetworkSession] = None ) -> AccessToken: """ - Fetch a new access token + Get the current access token. If the current access token is expired or not found, this method will attempt to refresh the token. :param network_session: An object to keep network session state - :return: New access token + :type network_session: Optional[NetworkSession], optional """ - system_random = random.SystemRandom() - jti_length = system_random.randint(16, 128) - ascii_alphabet = string.ascii_letters + string.digits - ascii_len = len(ascii_alphabet) - jti = ''.join( - ascii_alphabet[int(system_random.random() * ascii_len)] - for _ in range(jti_length) - ) - now_time = datetime.utcnow() - now_plus_30 = now_time + timedelta(seconds=30) - assertion = jwt.encode( - { - 'iss': self.config.client_id, - 'sub': self.subject_id, - 'box_sub_type': self.subject_type, - 'aud': 'https://api.box.com/oauth2/token', - 'jti': jti, - 'exp': int((now_plus_30 - datetime(1970, 1, 1)).total_seconds()), - }, - self._rsa_private_key, - algorithm=self.config.jwt_algorithm, - headers={ - 'kid': self.config.jwt_key_id, - }, - ) + old_token = self.token_storage.get() + if old_token == None: + new_token: AccessToken = self.refresh_token(network_session) + return new_token + return old_token - request_body = TokenRequest( - grant_type=TokenRequestGrantType.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_JWT_BEARER, - client_id=self.config.client_id, - client_secret=self.config.client_secret, - assertion=assertion, - ) + def as_user(self, user_id: str) -> 'BoxJWTAuth': + """ + Create a new BoxJWTAuth instance that uses the provided user ID as the subject of the JWT assertion. - response: FetchResponse = fetch( - 'https://api.box.com/oauth2/token', - FetchOptions( - method='POST', - data=request_body.to_dict(), - content_type='application/x-www-form-urlencoded', - network_session=network_session, - ), - ) + May be one of this application's created App User. Depending on the configured User Access Level, may also be any other App User or Managed User in the enterprise. - new_token = AccessToken.from_dict(response.data) - self.token_storage.store(new_token) - return new_token - def as_user(self, user_id: str): - """ - Set authentication as user. The new token will be automatically fetched with a next API call. + - May be one of this application's created App User. Depending on the - configured User Access Level, may also be any other App User or Managed - User in the enterprise. - - :param user_id: - The id of the user to authenticate. + :param user_id: The id of the user to authenticate + :type user_id: str """ - self.subject_id = user_id - self.subject_type = TokenRequestBoxSubjectType.USER + new_config: JWTConfig = JWTConfig( + client_id=self.config.client_id, + client_secret=self.config.client_secret, + enterprise_id=None, + user_id=user_id, + jwt_key_id=self.config.jwt_key_id, + private_key=self.config.private_key, + private_key_passphrase=self.config.private_key_passphrase, + token_storage=self.token_storage, + ) + new_auth: 'BoxJWTAuth' = BoxJWTAuth(config=new_config) self.token_storage.clear() + return new_auth - def as_enterprise(self, enterprise_id: str): + def as_enterprise(self, user_id: str) -> 'BoxJWTAuth': """ - Set authentication as enterprise. The new token will be automatically fetched with a next API call. - - :param enterprise_id: - The ID of the Box Developer Edition enterprise. + Create a new BoxJWTAuth instance that uses the provided enterprise ID as the subject of the JWT assertion. + :param user_id: The id of the enterprise to authenticate + :type user_id: str """ - self.subject_id = enterprise_id - self.subject_type = TokenRequestBoxSubjectType.ENTERPRISE + new_config: JWTConfig = JWTConfig( + client_id=self.config.client_id, + client_secret=self.config.client_secret, + enterprise_id=user_id, + user_id=None, + jwt_key_id=self.config.jwt_key_id, + private_key=self.config.private_key, + private_key_passphrase=self.config.private_key_passphrase, + token_storage=self.token_storage, + ) + new_auth: 'BoxJWTAuth' = BoxJWTAuth(config=new_config) self.token_storage.clear() + return new_auth - @classmethod - def _get_rsa_private_key( - cls, - private_key: str, - passphrase: str, - ) -> Any: - encoded_private_key = cls._encode_str_ascii_or_raise(private_key) - encoded_passphrase = cls._encode_str_ascii_or_raise(passphrase) - - return serialization.load_pem_private_key( - encoded_private_key, - password=encoded_passphrase, - backend=default_backend(), + def downscope_token( + self, + scopes: List[str], + resource: Optional[str] = None, + shared_link: Optional[str] = None, + network_session: Optional[NetworkSession] = None, + ) -> AccessToken: + """ + Downscope access token to the provided scopes. Returning a new access token with the provided scopes, with the original access token unchanged. + :param scopes: The scope(s) to apply to the resulting token. + :type scopes: List[str] + :param resource: The file or folder to get a downscoped token for. If None and shared_link None, the resulting token will not be scoped down to just a single item. + :type resource: Optional[str], optional + :param shared_link: The shared link to get a downscoped token for. If None and item None, the resulting token will not be scoped down to just a single item. + :type shared_link: Optional[str], optional + :param network_session: An object to keep network session state + :type network_session: Optional[NetworkSession], optional + """ + token: Optional[AccessToken] = self.token_storage.get() + if token == None: + raise Exception( + 'No access token is available. Make an API call to retrieve a token' + ' before calling this method.' + ) + auth_manager: AuthorizationManager = ( + AuthorizationManager(network_session=network_session) + if not network_session == None + else AuthorizationManager() ) + downscoped_token: AccessToken = auth_manager.request_access_token( + grant_type=RequestAccessTokenGrantType.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE.value, + subject_token=token.access_token, + subject_token_type=RequestAccessTokenSubjectTokenType.URN_IETF_PARAMS_OAUTH_TOKEN_TYPE_ACCESS_TOKEN.value, + resource=resource, + scope=to_string(scopes), + box_shared_link=shared_link, + ) + return downscoped_token - @staticmethod - def _encode_str_ascii_or_raise(passphrase: str) -> bytes: - try: - return passphrase.encode('ascii') - except UnicodeError as unicode_error: - raise TypeError( - "private_key and private_key_passphrase must contain binary data" - " (bytes/str), not a text/unicode string" - ) from unicode_error + def revoke_token(self, network_session: Optional[NetworkSession] = None) -> None: + """ + Revoke the current access token and remove from token storage. + :param network_session: An object to keep network session state + :type network_session: Optional[NetworkSession], optional + """ + old_token: Optional[AccessToken] = self.token_storage.get() + if old_token == None: + return None + auth_manager: AuthorizationManager = ( + AuthorizationManager(network_session=network_session) + if not network_session == None + else AuthorizationManager() + ) + auth_manager.revoke_access_token( + self.config.client_id, self.config.client_secret, old_token.access_token + ) + return self.token_storage.clear() diff --git a/box_sdk_gen/utils.py b/box_sdk_gen/utils.py index b650569..da641ec 100644 --- a/box_sdk_gen/utils.py +++ b/box_sdk_gen/utils.py @@ -2,10 +2,18 @@ import hashlib import os import uuid +from time import time from enum import Enum from io import SEEK_END, SEEK_SET, BufferedIOBase, BytesIO from typing import Any, Callable, Dict, Iterable, Optional, TypeVar +try: + import jwt + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization +except ImportError: + jwt, default_backend, serialization = None, None, None + from .base_object import BaseObject from .json_data import sd_to_json from .serialization import serialize @@ -173,3 +181,103 @@ def reduce_iterator( result = reducer(result, item) return result + + +def read_text_from_file(file_path: str) -> str: + with open(file_path, 'r') as file: + return file.read() + + +def is_browser() -> bool: + return False + + +def get_epoch_time_in_seconds() -> int: + return int(time()) + + +class JwtAlgorithm(str, Enum): + HS256 = 'HS256' + HS384 = 'HS384' + HS512 = 'HS512' + RS256 = 'RS256' + RS384 = 'RS384' + RS512 = 'RS512' + ES256 = 'ES256' + ES384 = 'ES384' + ES512 = 'ES512' + PS256 = 'PS256' + PS384 = 'PS384' + PS512 = 'PS512' + none = 'none' + + +class JwtSignOptions(BaseObject): + def __init__( + self, + algorithm: JwtAlgorithm, + headers: Dict[str, str] = None, + audience: Optional[str] = None, + issuer: Optional[str] = None, + subject: Optional[str] = None, + jwtid: Optional[str] = None, + keyid: Optional[str] = None, + **kwargs + ): + super().__init__(**kwargs) + if headers is None: + headers = {} + self.algorithm = algorithm + self.headers = headers + self.audience = audience + self.issuer = issuer + self.subject = subject + self.jwtid = jwtid + self.keyid = keyid + + +class JwtKey(BaseObject): + def __init__(self, key: str, passphrase: str, **kwargs): + super().__init__(**kwargs) + self.key = key + self.passphrase = passphrase + + +def encode_str_ascii_or_raise(passphrase: str) -> bytes: + try: + return passphrase.encode('ascii') + except UnicodeError as unicode_error: + raise TypeError( + "private_key and private_key_passphrase must contain binary data" + " (bytes/str), not a text/unicode string" + ) from unicode_error + + +def get_rsa_private_key( + private_key: str, + passphrase: str, +) -> Any: + encoded_private_key = encode_str_ascii_or_raise(private_key) + encoded_passphrase = encode_str_ascii_or_raise(passphrase) + + return serialization.load_pem_private_key( + encoded_private_key, + password=encoded_passphrase, + backend=default_backend(), + ) + + +def create_jwt_assertion(claims: dict, key: JwtKey, options: JwtSignOptions) -> str: + return jwt.encode( + { + 'iss': options.issuer, + 'sub': options.subject, + 'box_sub_type': claims['box_sub_type'], + 'aud': options.audience, + 'jti': options.jwtid, + 'exp': claims['exp'], + }, + get_rsa_private_key(key.key, key.passphrase), + algorithm=options.algorithm, + headers={'kid': options.keyid}, + ) diff --git a/docs/authentication.md b/docs/authentication.md index 39532be..14c986b 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -12,7 +12,14 @@ - [Obtaining Service Account token](#obtaining-service-account-token) - [Obtaining User token](#obtaining-user-token) - [Switching between Service Account and User](#switching-between-service-account-and-user) - - [BoxOAuth 2.0 Auth](#oauth-20-auth) + - [OAuth 2.0 Auth](#oauth-20-auth) +- [Revoke token](#revoke-token) +- [Downscope token](#downscope-token) +- [Token storage](#token-storage) + - [In-memory token storage](#in-memory-token-storage) + - [File token storage](#file-token-storage) + - [File with in-memory token storage](#file-with-in-memory-token-storage) + - [Custom storage](#custom-storage) @@ -112,8 +119,8 @@ make calls as that user. See the [API documentation](https://developer.box.com/) for detailed instructions on how to use app auth. Clients for making calls as an App User can be created with the same JSON JWT config file generated through the -[Box Developer Console][dev_console], but it is also required to call `auth.as_user('USER_ID')`, with -a user ID you want to authenticate. +[Box Developer Console][dev_console]. Calling `auth.as_user('USER_ID')` method will return a new auth object, +which is authenticated as the user with provided id, leaving the original object unchanged. ```python from box_sdk_gen.client import BoxClient @@ -121,8 +128,8 @@ from box_sdk_gen.jwt_auth import BoxJWTAuth, JWTConfig jwt_config = JWTConfig.from_config_file(config_file_path='/path/to/settings.json') auth = BoxJWTAuth(config=jwt_config) -auth.as_user('USER_ID') -user_client = BoxClient(auth=auth) +user_auth = auth.as_user('USER_ID') +user_client = BoxClient(auth=user_auth) ``` Alternatively, clients for making calls as an App User can be created with the same `JWTConfig` @@ -312,6 +319,52 @@ if __name__ == '__main__': app.run(port=4999) ``` +# Revoke token + +Access tokens for a client can be revoked when needed. This call invalidates old token, but you can still +reuse the `auth` object to retrieve a new token. If you make any new call after revoking the token, +a new token will be automatically retrieved. This method is currently only available for JWT authentication. + +To revoke current client's tokens in the storage use the following code: + + + +```python +client.auth.revoke_token() +``` + +# Downscope token + +You can exchange a client's access token for one with a lower scope, in order +to restrict the permissions for a child client or to pass to a less secure +location (e.g. a browser-based app). This method is currently only available for JWT authentication. + +A downscoped token does not include a refresh token. +In such a scenario, to obtain a new downscoped token, refresh the original token +and utilize the newly acquired token to obtain the downscoped token. + +More information about downscoping tokens can be found [here](https://developer.box.com/guides/authentication/tokens/downscope/). +If you want to learn more about available scopes please go [here](https://developer.box.com/guides/api-calls/permissions-and-errors/scopes/#scopes-for-downscoping). + +For example to get a new token with only `item_preview` scope, restricted to a single file, suitable for the +[Content Preview UI Element](https://developer.box.com/en/guides/embed/ui-elements/preview/) you can use the following code. +You can also initialize `DeveloperTokenAuth` with the retrieved access token and use it to create a new Client. + + + +```python +from box_sdk_gen.developer_token_auth import BoxDeveloperTokenAuth +from box_sdk_gen.schemas import AccessToken + +resource = 'https://api.box.com/2.0/files/123456789' +downscoped_token: AccessToken = auth.downscope_token( + scopes=['item_preview'], + resource=resource, +) +downscoped_auth = BoxDeveloperTokenAuth(downscoped_token.access_token) +client = BoxClient(auth=downscoped_auth) +``` + # Token storage ## In-memory token storage diff --git a/docs/task_assignments.md b/docs/task_assignments.md index a051bf1..d5b29a0 100644 --- a/docs/task_assignments.md +++ b/docs/task_assignments.md @@ -15,7 +15,11 @@ This operation is performed by calling function `get_task_assignments`. See the endpoint docs at [API Reference](https://developer.box.com/reference/get-tasks-id-assignments/). -_Currently we don't have an example for calling `get_task_assignments` in integration tests_ + + +```python +client.task_assignments.get_task_assignments(task_id=task.id) +``` ### Arguments @@ -43,7 +47,11 @@ This operation is performed by calling function `create_task_assignment`. See the endpoint docs at [API Reference](https://developer.box.com/reference/post-task-assignments/). -_Currently we don't have an example for calling `create_task_assignment` in integration tests_ + + +```python +client.task_assignments.create_task_assignment(task=CreateTaskAssignmentTask(type=CreateTaskAssignmentTaskTypeField.TASK.value, id=task.id), assign_to=CreateTaskAssignmentAssignTo(id=current_user.id)) +``` ### Arguments @@ -69,7 +77,11 @@ This operation is performed by calling function `get_task_assignment_by_id`. See the endpoint docs at [API Reference](https://developer.box.com/reference/get-task-assignments-id/). -_Currently we don't have an example for calling `get_task_assignment_by_id` in integration tests_ + + +```python +client.task_assignments.get_task_assignment_by_id(task_assignment_id=task_assignment.id) +``` ### Arguments @@ -95,7 +107,11 @@ This operation is performed by calling function `update_task_assignment_by_id`. See the endpoint docs at [API Reference](https://developer.box.com/reference/put-task-assignments-id/). -_Currently we don't have an example for calling `update_task_assignment_by_id` in integration tests_ + + +```python +client.task_assignments.update_task_assignment_by_id(task_assignment_id=task_assignment.id, message='updated message', resolution_state=UpdateTaskAssignmentByIdResolutionState.APPROVED.value) +``` ### Arguments @@ -123,7 +139,11 @@ This operation is performed by calling function `delete_task_assignment_by_id`. See the endpoint docs at [API Reference](https://developer.box.com/reference/delete-task-assignments-id/). -_Currently we don't have an example for calling `delete_task_assignment_by_id` in integration tests_ + + +```python +client.task_assignments.delete_task_assignment_by_id(task_assignment_id=task_assignment.id) +``` ### Arguments diff --git a/docs/terms_of_services.md b/docs/terms_of_services.md index 6680b32..79cebf1 100644 --- a/docs/terms_of_services.md +++ b/docs/terms_of_services.md @@ -15,7 +15,11 @@ This operation is performed by calling function `get_terms_of_service`. See the endpoint docs at [API Reference](https://developer.box.com/reference/get-terms-of-services/). -_Currently we don't have an example for calling `get_terms_of_service` in integration tests_ + + +```python +client.terms_of_services.get_terms_of_service() +``` ### Arguments @@ -41,7 +45,11 @@ This operation is performed by calling function `create_terms_of_service`. See the endpoint docs at [API Reference](https://developer.box.com/reference/post-terms-of-services/). -_Currently we don't have an example for calling `create_terms_of_service` in integration tests_ + + +```python +client.terms_of_services.create_terms_of_service(status=CreateTermsOfServiceStatus.ENABLED.value, tos_type=CreateTermsOfServiceTosType.MANAGED.value, text='Test TOS') +``` ### Arguments @@ -93,7 +101,11 @@ This operation is performed by calling function `update_terms_of_service_by_id`. See the endpoint docs at [API Reference](https://developer.box.com/reference/put-terms-of-services-id/). -_Currently we don't have an example for calling `update_terms_of_service_by_id` in integration tests_ + + +```python +client.terms_of_services.update_terms_of_service_by_id(terms_of_service_id=tos.id, status=UpdateTermsOfServiceByIdStatus.DISABLED.value, text='Disabled TOS') +``` ### Arguments diff --git a/test/auth.py b/test/auth.py index c2a55f5..80a5f4b 100644 --- a/test/auth.py +++ b/test/auth.py @@ -1,3 +1,15 @@ +import pytest + +from typing import Optional + +from box_sdk_gen.schemas import Files + +from box_sdk_gen.managers.uploads import UploadFileAttributes + +from box_sdk_gen.managers.uploads import UploadFileAttributesParentField + +from box_sdk_gen.schemas import FileFull + from box_sdk_gen.schemas import AccessToken from box_sdk_gen.utils import decode_base_64 @@ -43,16 +55,56 @@ def test_jwt_auth(): ) auth: BoxJWTAuth = BoxJWTAuth(config=jwt_config) client: BoxClient = BoxClient(auth=auth) - auth.as_user(user_id) - current_user: UserFull = client.users.get_user_me() + user_auth: BoxJWTAuth = auth.as_user(user_id) + user_client: BoxClient = BoxClient(auth=user_auth) + current_user: UserFull = user_client.users.get_user_me() assert current_user.id == user_id - auth.as_enterprise(enterprise_id) - new_user: UserFull = client.users.get_user_me(fields=['enterprise']) + enterprise_auth: BoxJWTAuth = auth.as_enterprise(enterprise_id) + enterprise_client: BoxClient = BoxClient(auth=enterprise_auth) + new_user: UserFull = enterprise_client.users.get_user_me(fields=['enterprise']) assert not new_user.enterprise == None assert new_user.enterprise.id == enterprise_id assert not new_user.id == user_id +def test_jwt_auth_downscope(): + jwt_config: JWTConfig = JWTConfig.from_config_json_string( + decode_base_64(get_env_var('JWT_CONFIG_BASE_64')) + ) + auth: BoxJWTAuth = BoxJWTAuth(config=jwt_config) + parent_client: BoxClient = BoxClient(auth=auth) + uploaded_files: Files = parent_client.uploads.upload_file( + attributes=UploadFileAttributes( + name=get_uuid(), parent=UploadFileAttributesParentField(id='0') + ), + file=generate_byte_stream(1024 * 1024), + ) + file: FileFull = uploaded_files.entries[0] + resource_path: str = ''.join(['https://api.box.com/2.0/files/', file.id]) + downscoped_token: AccessToken = auth.downscope_token(['item_rename'], resource_path) + assert not downscoped_token.access_token == None + downscoped_client: BoxClient = BoxClient( + auth=BoxDeveloperTokenAuth(token=downscoped_token.access_token) + ) + downscoped_client.files.update_file_by_id(file_id=file.id, name=get_uuid()) + with pytest.raises(Exception): + downscoped_client.files.delete_file_by_id(file_id=file.id) + parent_client.files.delete_file_by_id(file_id=file.id) + + +def test_jwt_auth_revoke(): + jwt_config: JWTConfig = JWTConfig.from_config_json_string( + decode_base_64(get_env_var('JWT_CONFIG_BASE_64')) + ) + auth: BoxJWTAuth = BoxJWTAuth(config=jwt_config) + auth.retrieve_token() + token_from_storage_before_revoke: Optional[AccessToken] = auth.token_storage.get() + auth.revoke_token() + token_from_storage_after_revoke: Optional[AccessToken] = auth.token_storage.get() + assert not token_from_storage_before_revoke == None + assert token_from_storage_after_revoke == None + + def test_oauth_auth_authorizeUrl(): config: OAuthConfig = OAuthConfig( client_id='OAUTH_CLIENT_ID', client_secret='OAUTH_CLIENT_SECRET' diff --git a/test/commons.py b/test/commons.py index cd7c654..5b2bfa4 100644 --- a/test/commons.py +++ b/test/commons.py @@ -14,6 +14,14 @@ from box_sdk_gen.managers.uploads import UploadFileAttributesParentField +from box_sdk_gen.schemas import TermsOfService + +from box_sdk_gen.schemas import TermsOfServices + +from box_sdk_gen.managers.terms_of_services import CreateTermsOfServiceStatus + +from box_sdk_gen.managers.terms_of_services import CreateTermsOfServiceTosType + from box_sdk_gen.schemas import ClassificationTemplateFieldsOptionsField from box_sdk_gen.managers.classifications import AddClassificationRequestBody @@ -89,8 +97,8 @@ def get_jwt_auth() -> BoxJWTAuth: def get_default_client_as_user(user_id: str) -> BoxClient: auth: BoxJWTAuth = get_jwt_auth() - auth.as_user(user_id) - return BoxClient(auth=auth) + auth_user: BoxJWTAuth = auth.as_user(user_id) + return BoxClient(auth=auth_user) def get_default_client() -> BoxClient: @@ -119,6 +127,19 @@ def upload_new_file() -> FileFull: return uploaded_files.entries[0] +def get_or_create_terms_of_services() -> TermsOfService: + client: BoxClient = get_default_client() + tos: TermsOfServices = client.terms_of_services.get_terms_of_service() + number_of_tos: int = len(tos.entries) + if number_of_tos == 0: + return client.terms_of_services.create_terms_of_service( + status=CreateTermsOfServiceStatus.ENABLED.value, + tos_type=CreateTermsOfServiceTosType.MANAGED.value, + text='Test TOS', + ) + return tos.entries[0] + + def get_or_create_classification( classification_template: ClassificationTemplate, ) -> ClassificationTemplateFieldsOptionsField: diff --git a/test/task_assignments.py b/test/task_assignments.py new file mode 100644 index 0000000..64f0f45 --- /dev/null +++ b/test/task_assignments.py @@ -0,0 +1,85 @@ +from box_sdk_gen.utils import to_string + +import pytest + +from box_sdk_gen.client import BoxClient + +from box_sdk_gen.schemas import FileFull + +from box_sdk_gen.schemas import Task + +from box_sdk_gen.managers.tasks import CreateTaskItem + +from box_sdk_gen.managers.tasks import CreateTaskItemTypeField + +from box_sdk_gen.managers.tasks import CreateTaskAction + +from box_sdk_gen.managers.tasks import CreateTaskCompletionRule + +from box_sdk_gen.schemas import UserFull + +from box_sdk_gen.schemas import TaskAssignment + +from box_sdk_gen.managers.task_assignments import CreateTaskAssignmentTask + +from box_sdk_gen.managers.task_assignments import CreateTaskAssignmentTaskTypeField + +from box_sdk_gen.managers.task_assignments import CreateTaskAssignmentAssignTo + +from box_sdk_gen.schemas import TaskAssignments + +from box_sdk_gen.managers.task_assignments import ( + UpdateTaskAssignmentByIdResolutionState, +) + +from test.commons import upload_new_file + +from test.commons import get_default_client + +client: BoxClient = get_default_client() + + +def testCreateUpdateGetDeleteTaskAssignment(): + file: FileFull = upload_new_file() + task: Task = client.tasks.create_task( + item=CreateTaskItem(type=CreateTaskItemTypeField.FILE.value, id=file.id), + action=CreateTaskAction.REVIEW.value, + message='test message', + due_at='2035-01-01T00:00:00Z', + completion_rule=CreateTaskCompletionRule.ALL_ASSIGNEES.value, + ) + assert task.message == 'test message' + assert task.item.id == file.id + current_user: UserFull = client.users.get_user_me() + task_assignment: TaskAssignment = client.task_assignments.create_task_assignment( + task=CreateTaskAssignmentTask( + type=CreateTaskAssignmentTaskTypeField.TASK.value, id=task.id + ), + assign_to=CreateTaskAssignmentAssignTo(id=current_user.id), + ) + assert task_assignment.item.id == file.id + assert task_assignment.assigned_to.id == current_user.id + task_assignment_by_id: TaskAssignment = ( + client.task_assignments.get_task_assignment_by_id( + task_assignment_id=task_assignment.id + ) + ) + assert task_assignment_by_id.id == task_assignment.id + task_assignments_on_task: TaskAssignments = ( + client.task_assignments.get_task_assignments(task_id=task.id) + ) + assert task_assignments_on_task.total_count == 1 + updated_task_assignment: TaskAssignment = ( + client.task_assignments.update_task_assignment_by_id( + task_assignment_id=task_assignment.id, + message='updated message', + resolution_state=UpdateTaskAssignmentByIdResolutionState.APPROVED.value, + ) + ) + assert updated_task_assignment.message == 'updated message' + assert to_string(updated_task_assignment.resolution_state) == 'approved' + with pytest.raises(Exception): + client.task_assignments.delete_task_assignment_by_id( + task_assignment_id=task_assignment.id + ) + client.files.delete_file_by_id(file_id=file.id) diff --git a/test/terms_of_services.py b/test/terms_of_services.py new file mode 100644 index 0000000..4c66b98 --- /dev/null +++ b/test/terms_of_services.py @@ -0,0 +1,39 @@ +from box_sdk_gen.utils import to_string + +from box_sdk_gen.client import BoxClient + +from box_sdk_gen.schemas import TermsOfService + +from box_sdk_gen.managers.terms_of_services import UpdateTermsOfServiceByIdStatus + +from box_sdk_gen.schemas import TermsOfServices + +from test.commons import get_default_client + +from test.commons import get_or_create_terms_of_services + +client: BoxClient = get_default_client() + + +def testGetTermsOfServices(): + tos: TermsOfService = get_or_create_terms_of_services() + updated_tos_1: TermsOfService = ( + client.terms_of_services.update_terms_of_service_by_id( + terms_of_service_id=tos.id, + status=UpdateTermsOfServiceByIdStatus.ENABLED.value, + text='Enabled TOS', + ) + ) + assert to_string(updated_tos_1.status) == 'enabled' + assert updated_tos_1.text == 'Enabled TOS' + updated_tos_2: TermsOfService = ( + client.terms_of_services.update_terms_of_service_by_id( + terms_of_service_id=tos.id, + status=UpdateTermsOfServiceByIdStatus.DISABLED.value, + text='Disabled TOS', + ) + ) + assert to_string(updated_tos_2.status) == 'disabled' + assert updated_tos_2.text == 'Disabled TOS' + list_tos: TermsOfServices = client.terms_of_services.get_terms_of_service() + assert list_tos.total_count > 0