Skip to content

Commit

Permalink
generated with codegen at box/box-codegen@2dac8b6 and spec at box/box…
Browse files Browse the repository at this point in the history
  • Loading branch information
box-sdk-build committed Jan 8, 2024
1 parent 213de0b commit 9214637
Show file tree
Hide file tree
Showing 9 changed files with 769 additions and 236 deletions.
577 changes: 360 additions & 217 deletions box_sdk_gen/jwt_auth.py

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions box_sdk_gen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
)
63 changes: 58 additions & 5 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -112,17 +119,17 @@ 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
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`
Expand Down Expand Up @@ -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:

<!-- sample post_oauth2_revoke -->

```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.

<!-- sample post_oauth2_token downscope_token -->

```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
Expand Down
30 changes: 25 additions & 5 deletions docs/task_assignments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_
<!-- sample get_tasks_id_assignments -->

```python
client.task_assignments.get_task_assignments(task_id=task.id)
```

### Arguments

Expand Down Expand Up @@ -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_
<!-- sample post_task_assignments -->

```python
client.task_assignments.create_task_assignment(task=CreateTaskAssignmentTask(type=CreateTaskAssignmentTaskTypeField.TASK.value, id=task.id), assign_to=CreateTaskAssignmentAssignTo(id=current_user.id))
```

### Arguments

Expand All @@ -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_
<!-- sample get_task_assignments_id -->

```python
client.task_assignments.get_task_assignment_by_id(task_assignment_id=task_assignment.id)
```

### Arguments

Expand All @@ -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_
<!-- sample put_task_assignments_id -->

```python
client.task_assignments.update_task_assignment_by_id(task_assignment_id=task_assignment.id, message='updated message', resolution_state=UpdateTaskAssignmentByIdResolutionState.APPROVED.value)
```

### Arguments

Expand Down Expand Up @@ -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_
<!-- sample delete_task_assignments_id -->

```python
client.task_assignments.delete_task_assignment_by_id(task_assignment_id=task_assignment.id)
```

### Arguments

Expand Down
18 changes: 15 additions & 3 deletions docs/terms_of_services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_
<!-- sample get_terms_of_services -->

```python
client.terms_of_services.get_terms_of_service()
```

### Arguments

Expand All @@ -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_
<!-- sample post_terms_of_services -->

```python
client.terms_of_services.create_terms_of_service(status=CreateTermsOfServiceStatus.ENABLED.value, tos_type=CreateTermsOfServiceTosType.MANAGED.value, text='Test TOS')
```

### Arguments

Expand Down Expand Up @@ -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_
<!-- sample put_terms_of_services_id -->

```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

Expand Down
60 changes: 56 additions & 4 deletions test/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'
Expand Down
Loading

0 comments on commit 9214637

Please sign in to comment.