Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
8 changes: 8 additions & 0 deletions changelog.d/20260625_000000_andrei_sdk_persistent_auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Added

- \[SDK\] Persistent authentication support with saved profiles:
`make_client_from_profile`, `make_client_from_cli`, and
`resolve_server_host` helpers let SDK users save server + credential profiles
to disk and instantiate a `Client` from a profile name without re-entering
credentials
(<https://github.com/cvat-ai/cvat/pull/10824>)
113 changes: 14 additions & 99 deletions cvat-cli/src/cvat_cli/_internal/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,23 @@
# SPDX-License-Identifier: MIT

import argparse
import getpass
import importlib
import importlib.util
import logging
import os
import sys
import textwrap
from collections.abc import Callable
from http.client import HTTPConnection
from pathlib import Path
from typing import Any

import attrs
import cvat_sdk.auto_annotation as cvataa
from cvat_sdk.core.client import (
AccessTokenCredentials,
Client,
Config,
Credentials,
PasswordCredentials,
from cvat_sdk.core.auth import (
ClientAuthParameters,
configure_client_auth_arguments,
make_client_from_cli,
)
from cvat_sdk.core.client import Client
from cvat_sdk.core.exceptions import AuthStoreError

from ..version import VERSION
from .parsers import BuildDictAction, parse_function_parameter
Expand All @@ -35,82 +31,9 @@ class CriticalError(Exception):
pass


CVAT_ACCESS_TOKEN_ENV_VAR = "CVAT_ACCESS_TOKEN" # nosec - a variable name declaration


def default_auth_factory() -> Callable[[str], Credentials]:
"""
Try to read the CVAT_ACCESS_TOKEN environment variable for a Personal Access Token (PAT).
If there is no value, try using the current user and asking for the password.
"""

token = os.getenv(CVAT_ACCESS_TOKEN_ENV_VAR)
if token is not None:
return lambda _: AccessTokenCredentials(token)

return get_auth_factory(getpass.getuser())


def get_auth_factory(s: str) -> Callable[[str], Credentials]:
"""
Parse a USER[:PASS] string and return a callable that takes the server URL
and returns auth credentials for that URL.
The callable will prompt the user for the password if none was initially supplied in the
input string and in the PASS env variable.
"""

user, _, password = s.partition(":")
if not password:
password = os.environ.get("PASS")

if password:
return lambda _: PasswordCredentials(user, password)
else:
return lambda url: PasswordCredentials(
user, getpass.getpass(f"Password for {user} at {url}: ")
)


def configure_common_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--version", action="version", version=VERSION)
parser.add_argument(
"--insecure",
action="store_true",
help="Allows to disable SSL certificate check",
)

parser.add_argument(
"--auth",
type=get_auth_factory,
metavar="USER[:PASS]",
default=default_auth_factory(),
help=textwrap.dedent("""\
User and password to use for authentication;
defaults to the current user and supports the PASS
environment variable or password prompt.
A Personal Access Token (PAT) can be generated on the server
and specified in the {} environment variable instead.
(default user: {}).
""").format(CVAT_ACCESS_TOKEN_ENV_VAR, getpass.getuser()),
)
parser.add_argument(
"--server-host", type=str, default="http://localhost", help="host (default: %(default)s)"
)
parser.add_argument(
"--server-port",
type=int,
default=None,
help="port (default: 80 for http and 443 for https connections)",
)
parser.add_argument(
"--organization",
"--org",
metavar="SLUG",
help="""short name (slug) of the organization
to use when listing or creating resources;
set to blank string to use the personal workspace
(default: list all accessible objects, create in personal workspace)""",
)
configure_client_auth_arguments(parser)
parser.add_argument(
"--debug",
action="store_const",
Expand All @@ -135,24 +58,16 @@ def configure_logger(logger: logging.Logger, parsed_args: argparse.Namespace) ->


def build_client(parsed_args: argparse.Namespace, logger: logging.Logger) -> Client:
config = Config(verify_ssl=not popattr(parsed_args, "insecure"))

url = popattr(parsed_args, "server_host")
if server_port := popattr(parsed_args, "server_port"):
url += f":{server_port}"
auth_args = ClientAuthParameters.from_namespace(parsed_args)
for field in attrs.fields(ClientAuthParameters):
popattr(parsed_args, field.name)

client = Client(
url=url,
logger=logger,
config=config,
check_server_version=False, # version is checked after auth to support versions < 2.3
)
try:
client = make_client_from_cli(auth_args, logger=logger)
except AuthStoreError as e:
raise CriticalError(str(e)) from e

client.login(popattr(parsed_args, "auth")(client.api_client.configuration.host))
client.check_server_version(fail_if_unsupported=False)

client.organization_slug = popattr(parsed_args, "organization")

return client


Expand Down
9 changes: 9 additions & 0 deletions cvat-sdk/cvat_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@
#
# SPDX-License-Identifier: MIT

from cvat_sdk.core.auth import (
AuthStore,
ClientAuthParameters,
ProfileEntry,
configure_client_auth_arguments,
get_auth_store_path,
make_client_from_cli,
make_client_from_profile,
)
from cvat_sdk.core.client import Client, Config, make_client
from cvat_sdk.version import VERSION as __version__
9 changes: 9 additions & 0 deletions cvat-sdk/cvat_sdk/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,14 @@
#
# SPDX-License-Identifier: MIT

from cvat_sdk.core.auth import (
AuthStore,
ClientAuthParameters,
ProfileEntry,
configure_client_auth_arguments,
get_auth_store_path,
make_client_from_cli,
make_client_from_profile,
)
from cvat_sdk.core.client import Client, Config, make_client
from cvat_sdk.version import VERSION as __version__
Loading
Loading