Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Update .codegen.json with commit hash of codegen and openapi spec #118

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codegen.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "engineHash": "ce205f7", "specHash": "1698c95", "version": "0.6.4" }
{ "engineHash": "81071a6", "specHash": "1698c95", "version": "0.6.4" }
98 changes: 96 additions & 2 deletions box_sdk_gen/box/developer_token_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from typing import Optional

from typing import List

from box_sdk_gen.schemas import PostOAuth2TokenGrantTypeField

from box_sdk_gen.schemas import PostOAuth2TokenSubjectTokenTypeField

from box_sdk_gen.schemas import AccessToken

from box_sdk_gen.networking.auth import Authentication
Expand All @@ -8,11 +14,37 @@

from box_sdk_gen.box.errors import BoxSDKError

from box_sdk_gen.box.token_storage import TokenStorage

from box_sdk_gen.box.token_storage import InMemoryTokenStorage

from box_sdk_gen.managers.authorization import AuthorizationManager

from box_sdk_gen.schemas import PostOAuth2Token

from box_sdk_gen.schemas import PostOAuth2Revoke


class DeveloperTokenConfig:
def __init__(
self, *, client_id: Optional[str] = None, client_secret: Optional[str] = None
):
self.client_id = client_id
self.client_secret = client_secret


class BoxDeveloperTokenAuth(Authentication):
def __init__(self, token: str, **kwargs):
def __init__(self, token: str, *, config: DeveloperTokenConfig = None, **kwargs):
"""
:param config: Configuration object of DeveloperTokenAuth., defaults to None
:type config: DeveloperTokenConfig, optional
"""
super().__init__(**kwargs)
self.token = token
self.config = config
self.token_storage = InMemoryTokenStorage(
token=AccessToken(access_token=self.token)
)

def retrieve_token(
self, *, network_session: Optional[NetworkSession] = None
Expand All @@ -22,7 +54,10 @@ def retrieve_token(
:param network_session: An object to keep network session state, defaults to None
:type network_session: Optional[NetworkSession], optional
"""
return AccessToken(access_token=self.token)
token: Optional[AccessToken] = self.token_storage.get()
if token == None:
raise BoxSDKError(message='No access token is available.')
return token

def refresh_token(
self, *, network_session: Optional[NetworkSession] = None
Expand All @@ -41,3 +76,62 @@ def retrieve_authorization_header(
) -> str:
token: AccessToken = self.retrieve_token(network_session=network_session)
return ''.join(['Bearer ', token.access_token])

def revoke_token(self, *, network_session: Optional[NetworkSession] = None) -> None:
"""
Revoke an active Access Token, effectively logging a user out that has been previously authenticated.
:param network_session: An object to keep network session state, defaults to None
:type network_session: Optional[NetworkSession], optional
"""
token: Optional[AccessToken] = self.token_storage.get()
if token == None:
return None
auth_manager: AuthorizationManager = AuthorizationManager(
network_session=(
network_session if not network_session == None else NetworkSession()
)
)
auth_manager.revoke_access_token(
client_id=self.config.client_id,
client_secret=self.config.client_secret,
token=token.access_token,
)
self.token_storage.clear()
return None

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. The resource should be a full URL to an item, e.g. https://api.box.com/2.0/files/123456., defaults to None
: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., defaults to None
:type shared_link: Optional[str], optional
:param network_session: An object to keep network session state, defaults to None
:type network_session: Optional[NetworkSession], optional
"""
token: Optional[AccessToken] = self.token_storage.get()
if token == None or token.access_token == None:
raise BoxSDKError(message='No access token is available.')
auth_manager: AuthorizationManager = AuthorizationManager(
network_session=(
network_session if not network_session == None else NetworkSession()
)
)
downscoped_token: AccessToken = auth_manager.request_access_token(
PostOAuth2TokenGrantTypeField.URN_IETF_PARAMS_OAUTH_GRANT_TYPE_TOKEN_EXCHANGE.value,
subject_token=token.access_token,
subject_token_type=PostOAuth2TokenSubjectTokenTypeField.URN_IETF_PARAMS_OAUTH_TOKEN_TYPE_ACCESS_TOKEN.value,
resource=resource,
scope=' '.join(scopes),
box_shared_link=shared_link,
)
return downscoped_token
10 changes: 5 additions & 5 deletions box_sdk_gen/box/token_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ def clear(self) -> None:


class InMemoryTokenStorage(TokenStorage):
def __init__(self):
self.token: Optional[AccessToken] = None
def __init__(self, token: Optional[AccessToken] = None):
self._token = token

def store(self, token: AccessToken) -> None:
self.token = token
self._token = token

def get(self) -> Optional[AccessToken]:
return self.token
return self._token

def clear(self) -> None:
self.token = None
self._token = None


class FileTokenStorage(TokenStorage):
Expand Down
53 changes: 30 additions & 23 deletions box_sdk_gen/internal/base_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def to_dict(self) -> dict:

@classmethod
def _deserialize(cls, key, value, annotation=None):
if annotation is None:
if annotation is None or value is None:
return value
if get_origin(annotation) == Optional:
return cls._deserialize(key, value, get_args(annotation))
Expand All @@ -69,13 +69,15 @@ def _deserialize(cls, key, value, annotation=None):
return cls._deserialize_datetime(key, value, annotation)
elif annotation == date:
return cls._deserialize_date(key, value, annotation)
else:
elif isinstance(annotation, type) and issubclass(annotation, BaseObject):
return cls._deserialize_nested_type(key, value, annotation)
else:
return value

@classmethod
def _deserialize_list(cls, key, value, annotation: list):
list_type = get_args(annotation)[0]
try:
list_type = get_args(annotation)[0]
return [
cls._deserialize(key, list_entry, list_type) for list_entry in value
]
Expand All @@ -84,27 +86,32 @@ def _deserialize_list(cls, key, value, annotation: list):

@classmethod
def _deserialize_union(cls, key, value, annotation):
possible_types = get_args(annotation)
if value is None:
if type(None) not in possible_types:
print('Value: ', value, 'should not be allowed in Union:', annotation)
try:
possible_types = get_args(annotation)
if value is None:
if type(None) not in possible_types:
print(
'Value: ', value, 'should not be allowed in Union:', annotation
)
return value

for possible_type in possible_types:
if (
isinstance(possible_type, type)
and issubclass(possible_type, BaseObject)
and value.get(possible_type._discriminator[0], None)
in possible_type._discriminator[1]
):
return cls._deserialize(key, value, possible_type)

for possible_type in possible_types:
try:
return cls._deserialize(key, value, possible_type)
except Exception:
continue
return value
except Exception:
return value

for possible_type in possible_types:
if (
issubclass(possible_type, BaseObject)
and value.get(possible_type._discriminator[0], None)
in possible_type._discriminator[1]
):
return cls._deserialize(key, value, possible_type)

for possible_type in possible_types:
try:
return cls._deserialize(key, value, possible_type)
except Exception:
continue

return value

@classmethod
def _deserialize_enum(cls, key, value, annotation):
Expand Down
17 changes: 17 additions & 0 deletions box_sdk_gen/networking/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from abc import abstractmethod

from typing import List

from box_sdk_gen.schemas import AccessToken

from box_sdk_gen.networking.network import NetworkSession
Expand All @@ -28,3 +30,18 @@ def retrieve_authorization_header(
self, *, network_session: Optional[NetworkSession] = None
) -> str:
pass

@abstractmethod
def revoke_token(self, *, network_session: Optional[NetworkSession] = None) -> None:
pass

@abstractmethod
def downscope_token(
self,
scopes: List[str],
*,
resource: Optional[str] = None,
shared_link: Optional[str] = None,
network_session: Optional[NetworkSession] = None
) -> AccessToken:
pass
12 changes: 7 additions & 5 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,11 @@ if __name__ == '__main__':
# Revoke token

Access tokens for a client can be revoked when needed. This call invalidates old token.
For CCGAuth and JWTAuth 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.
For OAuth it would be necessary to manually go through the authentication process again.
For BoxCCGAuth and BoxJWTAuth 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.
For BoxOAuth it would be necessary to manually go through the authentication process again.
For BoxDeveloperTokenAuth, it is necessary to provide a DeveloperTokenConfig during initialization,
containing the client ID and client secret.

To revoke current client's tokens in the storage use the following code:

Expand All @@ -342,7 +344,7 @@ If you want to learn more about available scopes please go [here](https://develo

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.
You can also initialize `BoxDeveloperTokenAuth` with the retrieved access token and use it to create a new Client.

<!-- sample post_oauth2_token downscope_token -->

Expand All @@ -354,7 +356,7 @@ downscoped_token: AccessToken = auth.downscope_token(
scopes=['item_preview'],
resource=resource,
)
downscoped_auth = BoxDeveloperTokenAuth(downscoped_token.access_token)
downscoped_auth = BoxDeveloperTokenAuth(token=downscoped_token.access_token)
client = BoxClient(auth=downscoped_auth)
```

Expand Down
2 changes: 1 addition & 1 deletion docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ See the endpoint docs at
<!-- sample post_tasks -->

```python
client.tasks.create_task(CreateTaskItem(type=CreateTaskItemTypeField.FILE.value, id=file.id), action=CreateTaskAction.REVIEW.value, message='test message', due_at=date, completion_rule=CreateTaskCompletionRule.ALL_ASSIGNEES.value)
client.tasks.create_task(CreateTaskItem(type=CreateTaskItemTypeField.FILE.value, id=file.id), action=CreateTaskAction.REVIEW.value, message='test message', due_at=date_time, completion_rule=CreateTaskCompletionRule.ALL_ASSIGNEES.value)
```

### Arguments
Expand Down
44 changes: 44 additions & 0 deletions test/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

from box_sdk_gen.box.developer_token_auth import BoxDeveloperTokenAuth

from box_sdk_gen.box.developer_token_auth import DeveloperTokenConfig

from box_sdk_gen.box.oauth import BoxOAuth

from box_sdk_gen.box.oauth import OAuthConfig
Expand Down Expand Up @@ -200,6 +202,48 @@ def get_access_token() -> AccessToken:
return auth_user.retrieve_token()


def test_developer_token_auth_revoke():
developer_token_config: DeveloperTokenConfig = DeveloperTokenConfig(
client_id=get_env_var('CLIENT_ID'), client_secret=get_env_var('CLIENT_SECRET')
)
token: AccessToken = get_access_token()
auth: BoxDeveloperTokenAuth = BoxDeveloperTokenAuth(
token=token.access_token, config=developer_token_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_developer_token_auth_downscope():
developer_token_config: DeveloperTokenConfig = DeveloperTokenConfig(
client_id=get_env_var('CLIENT_ID'), client_secret=get_env_var('CLIENT_SECRET')
)
token: AccessToken = get_access_token()
auth: BoxDeveloperTokenAuth = BoxDeveloperTokenAuth(
token=token.access_token, config=developer_token_config
)
parent_client: BoxClient = BoxClient(auth=auth)
folder: FolderFull = parent_client.folders.create_folder(
get_uuid(), CreateFolderParent(id='0')
)
resource_path: str = ''.join(['https://api.box.com/2.0/folders/', folder.id])
downscoped_token: AccessToken = auth.downscope_token(
['item_rename', 'item_preview'], resource=resource_path
)
assert not downscoped_token.access_token == None
downscoped_client: BoxClient = BoxClient(
auth=BoxDeveloperTokenAuth(token=downscoped_token.access_token)
)
downscoped_client.folders.update_folder_by_id(folder.id, name=get_uuid())
with pytest.raises(Exception):
downscoped_client.folders.delete_folder_by_id(folder.id)
parent_client.folders.delete_folder_by_id(folder.id)


def test_developer_token_auth():
user_id: str = get_env_var('USER_ID')
token: AccessToken = get_access_token()
Expand Down
Loading
Loading