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

feat: Add minimal client implementation for auth services #76

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ API Reference
api_reference/dataframe
api_reference/spec
api_reference/file
api_reference/auth

Indices and tables
------------------
Expand Down
15 changes: 15 additions & 0 deletions docs/api_reference/auth.rst
Original file line number Diff line number Diff line change
@@ -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:
26 changes: 26 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
40 changes: 40 additions & 0 deletions examples/auth/get_workspace_id.py
Original file line number Diff line number Diff line change
@@ -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)
3 changes: 3 additions & 0 deletions nisystemlink/clients/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._auth_client import AuthClient

# flake8: noqa
40 changes: 40 additions & 0 deletions nisystemlink/clients/auth/_auth_client.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming this AccessControlClient makes it intuitive and clubs the related feature across multiple services under the same client.
@mure, @spanglerco, @cameronwaterman - please share your thoughts

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 <nisystemlink.clients.core.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.
"""
...
4 changes: 4 additions & 0 deletions nisystemlink/clients/auth/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from ._auth_info import AuthInfo
from ._workspace import Workspace

# flake8: noqa
24 changes: 24 additions & 0 deletions nisystemlink/clients/auth/models/_auth_info.py
Original file line number Diff line number Diff line change
@@ -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."""
40 changes: 40 additions & 0 deletions nisystemlink/clients/auth/models/_auth_policy.py
Original file line number Diff line number Diff line change
@@ -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.
"""
104 changes: 104 additions & 0 deletions nisystemlink/clients/auth/models/_user.py
Original file line number Diff line number Diff line change
@@ -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: [email protected]
"""

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: [email protected]
"""

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."""
21 changes: 21 additions & 0 deletions nisystemlink/clients/auth/models/_workspace.py
Original file line number Diff line number Diff line change
@@ -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.
"""
5 changes: 5 additions & 0 deletions nisystemlink/clients/auth/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from nisystemlink.clients.auth.utilities._get_workspace_info import (
get_workspace_by_name,
)

# flake8: noqa
24 changes: 24 additions & 0 deletions nisystemlink/clients/auth/utilities/_get_workspace_info.py
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions tests/integration/auth/test_auth_client.py
Original file line number Diff line number Diff line change
@@ -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