diff --git a/docs/api_reference.rst b/docs/api_reference.rst index b9181186..55cfc47b 100644 --- a/docs/api_reference.rst +++ b/docs/api_reference.rst @@ -13,6 +13,7 @@ API Reference api_reference/dataframe api_reference/spec api_reference/file + api_reference/auth Indices and tables ------------------ diff --git a/docs/api_reference/auth.rst b/docs/api_reference/auth.rst new file mode 100644 index 00000000..7d857f8a --- /dev/null +++ b/docs/api_reference/auth.rst @@ -0,0 +1,15 @@ +.. _api_tag_page: + +nisystemlink.clients.auth +====================== + +.. autoclass:: nisystemlink.clients.auth.AuthClient + :exclude-members: __init__ + + .. automethod:: __init__ + .. automethod:: authenticate + + +.. automodule:: nisystemlink.clients.auth.models + :members: + :imported-members: \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 0efe9358..1e052a98 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -182,5 +182,31 @@ Examples Get the metadata of a File using its Id and download it. .. literalinclude:: ../examples/file/download_file.py + :language: python + :linenos: + + +Auth API +------- + +Overview +~~~~~~~~ + +The :class:`.AuthClient` class is the primary entry point of the Auth API. + +When constructing a :class:`.AuthClient`, you can pass an +:class:`.HttpConfiguration` (like one retrieved from the +:class:`.HttpConfigurationManager`), or let :class:`.AuthClient` use the +default connection. The default connection depends on your environment. + +With a :class:`.AuthClient` object, you can: + +* Get the information about the caller. + +Examples +~~~~~~~~ + +Get the workspace id for the workspace name. +.. literalinclude:: ../examples/auth/get_workspace_id.py :language: python :linenos: \ No newline at end of file diff --git a/examples/auth/get_workspace_id.py b/examples/auth/get_workspace_id.py new file mode 100644 index 00000000..3e6ef70d --- /dev/null +++ b/examples/auth/get_workspace_id.py @@ -0,0 +1,40 @@ +"""Example of getting workspace ID.""" + +from nisystemlink.clients.auth import AuthClient +from nisystemlink.clients.auth.utilities._get_workspace_info import ( + get_workspace_by_name, +) +from nisystemlink.clients.core import ApiException, HttpConfiguration + +server_url = "" # SystemLink API URL +server_api_key = "" # SystemLink API key +workspace_name = "" # Systemlink workspace name + +# Provide the valid API key and API URL for client initialization. +auth_client = AuthClient( + HttpConfiguration(server_uri=server_url, api_key=server_api_key) +) + +# Getting workspace ID. +try: + # Get the caller details for workspaces information. + caller_info = auth_client.get_auth_info() + workspaces = caller_info.workspaces if caller_info.workspaces else None + workspace_id = None + + # Get the required workspace information for getting ID. + if workspaces: + workspace_info = get_workspace_by_name( + workspaces=workspaces, + name=workspace_name, + ) + workspace_id = workspace_info.id if workspace_info else None + + if workspace_id: + print(f"Workspace ID: {workspace_id}") + +except ApiException as exp: + print(exp) + +except Exception as exp: + print(exp) diff --git a/nisystemlink/clients/auth/__init__.py b/nisystemlink/clients/auth/__init__.py new file mode 100644 index 00000000..5d80edbd --- /dev/null +++ b/nisystemlink/clients/auth/__init__.py @@ -0,0 +1,3 @@ +from ._auth_client import AuthClient + +# flake8: noqa diff --git a/nisystemlink/clients/auth/_auth_client.py b/nisystemlink/clients/auth/_auth_client.py new file mode 100644 index 00000000..2ecaccf2 --- /dev/null +++ b/nisystemlink/clients/auth/_auth_client.py @@ -0,0 +1,40 @@ +"""Implementation of AuthClient.""" + +from typing import Optional + +from nisystemlink.clients import core +from nisystemlink.clients.core._uplink._base_client import BaseClient +from nisystemlink.clients.core._uplink._methods import get + +from . import models + + +class AuthClient(BaseClient): + def __init__(self, configuration: Optional[core.HttpConfiguration] = None): + """Initialize an instance. + + Args: + configuration: Defines the web server to connect to and information about + how to connect. If not provided, an instance of + :class:`JupyterHttpConfiguration ` # noqa: W505 + is used. + + Raises: + ApiException: if unable to communicate with the Auth Service. + """ + if configuration is None: + configuration = core.HttpConfigurationManager.get_configuration() + + super().__init__(configuration, base_path="/niauth/v1/") + + @get("auth") + def get_auth_info(self) -> models.AuthInfo: + """Gets information about the authenticated API Key. + + Returns: + models.AuthInfo: Information about the caller. + + Raises: + ApiException: if unable to communicate with the Auth Service. + """ + ... diff --git a/nisystemlink/clients/auth/models/__init__.py b/nisystemlink/clients/auth/models/__init__.py new file mode 100644 index 00000000..ea571818 --- /dev/null +++ b/nisystemlink/clients/auth/models/__init__.py @@ -0,0 +1,4 @@ +from ._auth_info import AuthInfo +from ._workspace import Workspace + +# flake8: noqa diff --git a/nisystemlink/clients/auth/models/_auth_info.py b/nisystemlink/clients/auth/models/_auth_info.py new file mode 100644 index 00000000..c63525e8 --- /dev/null +++ b/nisystemlink/clients/auth/models/_auth_info.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + +from ._auth_policy import AuthPolicy +from ._user import Org, User +from ._workspace import Workspace + + +class AuthInfo(JsonModel): + """Information about the authenticated caller.""" + + user: Optional[User] = None + """Details of authenticated caller.""" + org: Optional[Org] = None + """Organization of authenticated caller.""" + workspaces: Optional[List[Workspace]] = None + """List of workspaces the authenticated caller has access.""" + policies: Optional[List[AuthPolicy]] = None + """List of policies for the authenticated caller.""" + properties: Optional[Dict[str, str]] = None + """A map of key value properties.""" diff --git a/nisystemlink/clients/auth/models/_auth_policy.py b/nisystemlink/clients/auth/models/_auth_policy.py new file mode 100644 index 00000000..51495362 --- /dev/null +++ b/nisystemlink/clients/auth/models/_auth_policy.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from typing import List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class AuthStatement(JsonModel): + """Auth Statement information.""" + + actions: Optional[List[str]] = None + """A list of actions the user is allowed to perform. + + example: notebookexecution:Query + """ + resource: Optional[List[str]] = None + """A list of resources the user is allowed to access. + + example: Notebook + """ + workspace: Optional[str] = None + """The workspace the user is allowed to access. + + example: 5afb2ce3741fe11d88838cc9 + """ + + +class Statement(AuthStatement): + """Statement information.""" + + description: Optional[str] = None + """A description for this statement.""" + + +class AuthPolicy(JsonModel): + """Auth Policy information.""" + + statements: Optional[List[AuthStatement]] = None + """A list of statements defining the actions the user can perform on a resource in a workspace. + """ diff --git a/nisystemlink/clients/auth/models/_user.py b/nisystemlink/clients/auth/models/_user.py new file mode 100644 index 00000000..83171436 --- /dev/null +++ b/nisystemlink/clients/auth/models/_user.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any, Dict, List, Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class Status(Enum): + """Enumeration to represent different status of user's registration.""" + + PENDING = "pending" + ACTIVE = "active" + + +class Org(JsonModel): + """User's Organization information.""" + + id: Optional[str] = None + """The unique id.""" + name: Optional[str] = None + """The name of the organization.""" + owner_id: Optional[str] = None + """The userId of the organization owner.""" + + +class User(JsonModel): + """User information.""" + + id: Optional[str] = None + """The unique id. + + example: "47d-47c7-8dd1-70f63de3583f" + """ + + first_name: Optional[str] = None + """The user's first name.""" + + last_name: Optional[str] = None + """The user's last name.""" + + email: Optional[str] = None + """The user's email. + + example: example@email.com + """ + + phone: Optional[str] = None + """The user's contact phone number. + + example: 555-555-5555 + """ + + niua_id: Optional[str] = None + """The external id (niuaId, SID, login name). + + example: example@email.com + """ + + login: Optional[str] = None + """ + The login name of the user. This the "username" or equivalent entered when + the user authenticates with the identity provider. + """ + + accepted_to_s: Optional[bool] = None + """(deprecated) Whether the user accepted the terms of service.""" + + properties: Optional[Dict[str, str]] = None + """A map of key value properties. + + example: { "key1": "value1" } + """ + + keywords: Optional[List[str]] = None + """A list of keywords associated with the user.""" + + created: Optional[datetime] = None + """The created timestamp. + + example: 2019-12-02T15:31:45.379Z + """ + + updated: Optional[datetime] = None + """The last updated timestamp. + + example: 2019-12-02T15:31:45.379Z + """ + + org_id: Optional[str] = None + """The id of the organization. + + example: "47d-47c7-8dd1-70f63de3435f" + """ + + policies: Optional[List[str]] = None + """A list of policy ids to reference existing policies.""" + + status: Optional[Status] = None + """The status of the users' registration.""" + + entitlements: Optional[Any] = None + """(deprecated) Features to which the user is entitled within the application.""" diff --git a/nisystemlink/clients/auth/models/_workspace.py b/nisystemlink/clients/auth/models/_workspace.py new file mode 100644 index 00000000..2927322d --- /dev/null +++ b/nisystemlink/clients/auth/models/_workspace.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import Optional + +from nisystemlink.clients.core._uplink._json_model import JsonModel + + +class Workspace(JsonModel): + """Workspace information.""" + + id: Optional[str] = None + """The unique id.""" + name: Optional[str] = None + """The workspace name.""" + enabled: Optional[bool] = None + """Whether the workspace is enabled or not.""" + default: Optional[bool] = None + """ + Whether the workspace is the default. The default workspace is used when callers omit a \ +workspace id. + """ diff --git a/nisystemlink/clients/auth/utilities/__init__.py b/nisystemlink/clients/auth/utilities/__init__.py new file mode 100644 index 00000000..7fa9b593 --- /dev/null +++ b/nisystemlink/clients/auth/utilities/__init__.py @@ -0,0 +1,5 @@ +from nisystemlink.clients.auth.utilities._get_workspace_info import ( + get_workspace_by_name, +) + +# flake8: noqa diff --git a/nisystemlink/clients/auth/utilities/_get_workspace_info.py b/nisystemlink/clients/auth/utilities/_get_workspace_info.py new file mode 100644 index 00000000..827a4824 --- /dev/null +++ b/nisystemlink/clients/auth/utilities/_get_workspace_info.py @@ -0,0 +1,24 @@ +"""Get workspace information.""" + +from typing import List, Optional + +from nisystemlink.clients.auth.models import Workspace + + +def get_workspace_by_name( + workspaces: List[Workspace], + name: str, +) -> Optional[Workspace]: + """Get workspace information from the list of workspace using `name`. + + Args: + workspaces (List[Workspace]): List of workspace. + name (str): Workspace name. + + Returns: + Optional[Workspace]: Workspace information. + """ + for workspace in workspaces: + if workspace.name == name and workspace.id: + return workspace + return None diff --git a/tests/integration/auth/test_auth_client.py b/tests/integration/auth/test_auth_client.py new file mode 100644 index 00000000..c3b02183 --- /dev/null +++ b/tests/integration/auth/test_auth_client.py @@ -0,0 +1,19 @@ +"""Integration tests for AuthClient.""" + +import pytest +from nisystemlink.clients.auth import AuthClient + + +@pytest.fixture(scope="class") +def client(enterprise_config) -> AuthClient: + """Fixture to create a AuthClient instance.""" + return AuthClient(enterprise_config) + + +@pytest.mark.enterprise +@pytest.mark.integration +class TestAuthClient: + def test__get_auth_info__succeeds(self, client: AuthClient): + """Test the case of getting caller information with SystemLink Credentials.""" + response = client.get_auth_info() + assert response is not None