Skip to content

Commit

Permalink
v0.4.0
Browse files Browse the repository at this point in the history
- Reorganize authentification methods

See merge request cdos-pub/dinamis-sdk!35
  • Loading branch information
pboizeau authored and Cresson Remi committed Jan 15, 2025
1 parent 8e19367 commit fd46e07
Show file tree
Hide file tree
Showing 20 changed files with 444 additions and 365 deletions.
11 changes: 6 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
*.egg-info
*/**/__pycache__
.idea
*.egg-info/
__pycache__/
.idea/
dinamis_sdk/test
build
dist
build/
dist/
*venv/
.vscode/
20 changes: 11 additions & 9 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,24 +129,26 @@ OAuth2 Tests:
- python tests/test_spot-6-7-drs.py
- python tests/test_super-s2.py
- python tests/test_push.py
- python tests/test_headers.py authorization

API key Tests:
extends: .tests_base
script:
- dinamis_cli register
- mv /root/.config/dinamis_sdk_auth/.token /root/.config/dinamis_sdk_auth/.token_
- python tests/test_headers.py access-key
# ensure that we une only API key from now
- mv /root/.config/dinamis_sdk_auth/.jwt /root/.config/dinamis_sdk_auth/.jwt_
- python tests/test_spot-6-7-drs.py
- python tests/test_super-s2.py
- python tests/test_push.py
- mv /root/.config/dinamis_sdk_auth/.token_ /root/.config/dinamis_sdk_auth/.token
- dinamis_cli delete
- toto=$(dinamis_cli create 2>&1)
- mv /root/.config/dinamis_sdk_auth/.token /root/.config/dinamis_sdk_auth/.token_
- export DINAMIS_SDK_ACCESS_KEY=$(echo $toto | cut -d"'" -f4)
- export DINAMIS_SDK_SECRET_KEY=$(echo $toto | cut -d"'" -f8)
# Test API key from environment variables
- export DINAMIS_SDK_ACCESS_KEY=$(cat /root/.config/dinamis_sdk_auth/.apikey | cut -d'"' -f4)
- export DINAMIS_SDK_SECRET_KEY=$(cat /root/.config/dinamis_sdk_auth/.apikey | cut -d'"' -f8)
- rm /root/.config/dinamis_sdk_auth/.apikey # ensure that we use env. vars.
- python tests/test_spot-6-7-drs.py
- mv /root/.config/dinamis_sdk_auth/.token_ /root/.config/dinamis_sdk_auth/.token
- dinamis_cli revoke $DINAMIS_SDK_ACCESS_KEY
# bring back oauth2 credentials so we can revoke the API key
- mv /root/.config/dinamis_sdk_auth/.jwt_ /root/.config/dinamis_sdk_auth/.jwt
- dinamis_cli revoke ${DINAMIS_SDK_ACCESS_KEY}

# --------------------------------- Ship --------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

Largely inspired from the *Microsoft Planetary Computer SDK*, **Dinamis-SDK** is
built on the STAC ecosystem to provide easy access to remote sensing imagery
and thematic products of the [THEIA-MTP geospatial data center](https://home-cdos.apps.okd.crocc.meso.umontpellier.fr/).
and thematic products of the [THEIA-MTD geospatial data center](https://home-cdos.apps.okd.crocc.meso.umontpellier.fr/).

```python
import dinamis_sdk
Expand Down
10 changes: 5 additions & 5 deletions dinamis_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Dinamis SDK module."""

# flake8: noqa
import pkg_resources

__version__ = "0.3.8"
from dinamis_sdk.s3 import (
__version__ = "0.4.0"
from dinamis_sdk.signing import (
sign,
sign_inplace,
sign_urls,
Expand All @@ -13,5 +12,6 @@
sign_item_collection,
sign_url_put,
) # noqa
from dinamis_sdk import auth # noqa
from dinamis_sdk.upload import push
from .oauth2 import OAuth2Session # noqa
from .upload import push
from .http import get_headers
39 changes: 14 additions & 25 deletions dinamis_sdk/cli.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
"""Dinamis Command Line Interface."""

import json
import os
from typing import Dict, List

import click

from .auth import get_access_token
from .utils import (
APIKEY_FILE,
S3_SIGNING_ENDPOINT,
create_session,
log,
)
from .model import ApiKey
from .http import OAuth2ConnectionMethod
from .utils import get_logger_for, create_session

log = get_logger_for(__name__)
conn = OAuth2ConnectionMethod()


@click.group(help="Dinamis CLI")
Expand All @@ -24,9 +21,9 @@ def http(route: str):
"""Perform an HTTP request."""
session = create_session()
ret = session.get(
f"{S3_SIGNING_ENDPOINT}{route}",
f"{conn.endpoint}{route}",
timeout=5,
headers={"authorization": f"bearer {get_access_token()}"},
headers=conn.get_headers(),
)
ret.raise_for_status()
return ret
Expand Down Expand Up @@ -80,22 +77,14 @@ def revoke(access_key: str):
@app.command(help="Get and store an API key")
def register():
"""Get and store an API key."""
with open(APIKEY_FILE, "w", encoding="utf-8") as f:
json.dump(create_key(), f)
log.info(f"API key successfully created and stored in {APIKEY_FILE}")
ApiKey.from_dict(create_key()).to_config_dir()
log.info("API key successfully created and stored")


@app.command(help="Delete the stored API key")
@click.option("--dont-revoke", default=False)
def delete(dont_revoke):
def delete(dont_revoke: bool):
"""Delete the stored API key."""
if os.path.isfile(APIKEY_FILE):
if not dont_revoke:
with open(APIKEY_FILE, encoding="UTF-8") as json_file:
api_key = json.load(json_file)
if "access-key" in api_key:
revoke_key(api_key["access-key"])
os.remove(APIKEY_FILE)
log.info(f"File {APIKEY_FILE} deleted!")
else:
log.info("No API key stored!")
if not dont_revoke:
revoke_key(ApiKey.from_config_dir().access_key)
ApiKey.delete_from_config_dir()
115 changes: 115 additions & 0 deletions dinamis_sdk/http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""HTTP connections with various methods."""

from typing import Dict
from ast import literal_eval
from pydantic import BaseModel, ConfigDict
from .utils import get_logger_for, create_session
from .oauth2 import OAuth2Session
from .model import ApiKey
from .settings import ENV, SIGNING_ENDPOINT


log = get_logger_for(__name__)


class BareConnectionMethod(BaseModel):
"""Bare connection method, no extra headers."""

model_config = ConfigDict(arbitrary_types_allowed=True)
endpoint: str = SIGNING_ENDPOINT

def get_headers(self) -> Dict[str, str]:
"""Get the headers."""
return {}

def model_post_init(self, __context):
"""Post initialization."""
if not self.endpoint.lower().startswith(("http://", "https://")):
raise ValueError(f"{self.endpoint} must start with http[s]://")
if not self.endpoint.endswith("/"):
self.endpoint += "/"
return self.endpoint


class OAuth2ConnectionMethod(BareConnectionMethod):
"""OAuth2 connection method."""

oauth2_session: OAuth2Session = OAuth2Session()

def get_headers(self):
"""Return the headers."""
return {"authorization": f"bearer {self.oauth2_session.get_access_token()}"}


class ApiKeyConnectionMethod(BareConnectionMethod):
"""API key connection method."""

api_key: ApiKey

def get_headers(self):
"""Return the headers."""
return self.api_key.to_dict()


class HTTPSession:
"""HTTP session class."""

def __init__(self, timeout=10):
"""Initialize the HTTP session."""
self.session = create_session(
retry_total=ENV.dinamis_sdk_retry_total,
retry_backoff_factor=ENV.dinamis_sdk_retry_backoff_factor,
)
self.timeout = timeout
self.headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
self._method = None

def get_method(self):
"""Get method."""
log.debug("Get method")
if not self._method:
# Lazy instantiation
self.prepare_connection_method()
return self._method

def prepare_connection_method(self):
"""Set the connection method."""
# Custom server without authentication method
if ENV.dinamis_sdk_bypass_auth_api:
self._method = BareConnectionMethod(
endpoint=ENV.dinamis_sdk_bypass_auth_api
)

# API key method
elif api_key := ApiKey.grab():
self._method = ApiKeyConnectionMethod(api_key=api_key)

# OAuth2 method
else:
self._method = OAuth2ConnectionMethod()

def post(self, route: str, params: Dict):
"""Perform a POST request."""
method = self.get_method()
url = f"{method.endpoint}{route}"
headers = {**self.headers, **method.get_headers()}
log.debug("POST to %s", url)
response = self.session.post(url, params=params, headers=headers, timeout=10)
try:
response.raise_for_status()
except Exception as e:
log.error(literal_eval(response.text))
raise e

return response


session = HTTPSession()


def get_headers():
"""Return the headers."""
return session.get_method().get_headers()
118 changes: 118 additions & 0 deletions dinamis_sdk/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Models."""

import os
import json
from typing import Dict
from pydantic import BaseModel, Field, ConfigDict # pylint: disable = no-name-in-module
from .utils import get_logger_for
from .settings import ENV, get_config_path

log = get_logger_for(__name__)


class Serializable(BaseModel): # pylint: disable = R0903
"""Base class for serializable pyantic models."""

model_config = ConfigDict(
populate_by_name=True,
)

@classmethod
def get_cfg_file_name(cls) -> str | None:
"""Get the config file name (without full path)."""
name = f".{cls.__name__.lower()}"
log.debug("Looking for config file for %s", name)
cfg_pth = get_config_path()
cfg_file = os.path.join(cfg_pth, name) if cfg_pth else None
log.debug("Config file %sfound %s", "" if cfg_file else "not ", cfg_file or "")
return cfg_file

@classmethod
def from_config_dir(cls):
"""Try to load from config directory."""
cfg_file = cls.get_cfg_file_name()
return cls.from_file(cfg_file) if cfg_file else None

def to_config_dir(self):
"""Try to save to config files."""
cfg_file = self.get_cfg_file_name()
if cfg_file:
self.to_file(cfg_file)

@classmethod
def from_dict(cls, dict: Dict):
"""Get the object from dict."""
return cls(**dict)

def to_dict(self) -> Dict[str, str]:
"""To dict."""
return self.model_dump(by_alias=True)

@classmethod
def from_file(cls, file_path: str):
"""Load object from a file."""
try:
log.debug("Reading JSON file %s", file_path)
with open(file_path, "r", encoding="utf-8") as file_handler:
return cls(**json.load(file_handler))
except (FileNotFoundError, IOError, json.decoder.JSONDecodeError) as err:
log.debug("Cannot read object from config directory (%s).", err)

return None

def to_file(self, file_path: str):
"""Save the object to file."""
try:
log.debug("Writing JSON file %s", file_path)
with open(file_path, "w", encoding="utf-8") as file_handler:
json.dump(self.to_dict(), file_handler)
except IOError as io_err:
log.warning("Unable to save file %s (%s)", file_path, io_err)

@classmethod
def delete_from_config_dir(cls):
"""Delete the config file, if there."""
cfg_file = cls.get_cfg_file_name()
if cfg_file:
os.remove(cfg_file)


class JWT(Serializable):
"""JWT model."""

access_token: str
expires_in: int
refresh_token: str
refresh_expires_in: int
token_type: str


class DeviceGrantResponse(BaseModel): # pylint: disable = R0903
"""Device grant login response model."""

verification_uri_complete: str
device_code: str
expires_in: int
interval: int


class ApiKey(Serializable):
"""API key class."""

access_key: str = Field(alias="access-key")
secret_key: str = Field(alias="secret-key")

@classmethod
def from_env(cls):
"""Try to load from env."""
if ENV.dinamis_sdk_access_key and ENV.dinamis_sdk_secret_key:
return cls(
access_key=ENV.dinamis_sdk_access_key,
secret_key=ENV.dinamis_sdk_secret_key,
)
return None

@classmethod
def grab(cls):
"""Try to load an API key from env. or file."""
return cls.from_env() or cls.from_config_dir()
Loading

0 comments on commit fd46e07

Please sign in to comment.