Skip to content

Commit

Permalink
Start to refactor generic for settings
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathangreen committed Oct 16, 2023
1 parent 80ed941 commit 139ec87
Show file tree
Hide file tree
Showing 38 changed files with 271 additions and 173 deletions.
2 changes: 1 addition & 1 deletion api/admin/controller/integration_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from core.util.log import LoggerMixin
from core.util.problem_detail import ProblemError

T = TypeVar("T", bound=HasIntegrationConfiguration)
T = TypeVar("T", bound=HasIntegrationConfiguration[BaseSettings])


class UpdatedLibrarySettingsTuple(NamedTuple):
Expand Down
6 changes: 3 additions & 3 deletions api/admin/controller/patron_auth_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
)
from api.admin.form_data import ProcessFormData
from api.admin.problem_details import *
from api.authentication.base import AuthenticationProvider
from api.authentication.base import AuthenticationProviderType
from api.authentication.basic import BasicAuthenticationProvider
from api.integration.registry.patron_auth import PatronAuthRegistry
from core.integration.goals import Goals
Expand All @@ -25,10 +25,10 @@


class PatronAuthServicesController(
IntegrationSettingsController[AuthenticationProvider],
IntegrationSettingsController[AuthenticationProviderType],
AdminPermissionsControllerMixin,
):
def default_registry(self) -> IntegrationRegistry[AuthenticationProvider]:
def default_registry(self) -> IntegrationRegistry[AuthenticationProviderType]:
return PatronAuthRegistry()

@property
Expand Down
11 changes: 9 additions & 2 deletions api/admin/controller/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,21 @@ def _get_integration_protocols(
protocols = []
_db = self._db
for api in provider_apis:
is_integration = issubclass(api, HasIntegrationConfiguration)
protocol = dict()
name = getattr(api, protocol_name_attr)
name = (
getattr(api, protocol_name_attr) if not is_integration else api.label()
)
protocol["name"] = name

label = getattr(api, "NAME", name)
protocol["label"] = label

description = getattr(api, "DESCRIPTION", None)
description = (
getattr(api, "DESCRIPTION", None)
if not is_integration
else api.description()
)
if description != None:
protocol["description"] = description

Expand Down
34 changes: 21 additions & 13 deletions api/authentication/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TypeVar

from money import Money
from sqlalchemy.orm import Session
Expand All @@ -27,9 +28,15 @@ class AuthProviderLibrarySettings(BaseSettings):
...


SettingsType = TypeVar("SettingsType", bound=AuthProviderSettings, covariant=True)
LibrarySettingsType = TypeVar(
"LibrarySettingsType", bound=AuthProviderLibrarySettings, covariant=True
)


class AuthenticationProvider(
OPDSAuthenticationFlow,
HasLibraryIntegrationConfiguration,
HasLibraryIntegrationConfiguration[SettingsType, LibrarySettingsType],
HasSelfTestsIntegrationConfiguration,
LoggerMixin,
ABC,
Expand All @@ -40,34 +47,30 @@ def __init__(
self,
library_id: int,
integration_id: int,
settings: AuthProviderSettings,
library_settings: AuthProviderLibrarySettings,
settings: SettingsType,
library_settings: LibrarySettingsType,
analytics: Analytics | None = None,
):
self.library_id = library_id
self.integration_id = integration_id
self.analytics = analytics
self._settings = settings
self._library_settings = library_settings

def library(self, _db: Session) -> Library | None:
return Library.by_id(_db, self.library_id)

@property
def settings(self) -> SettingsType:
return self._settings

def integration(self, _db: Session) -> IntegrationConfiguration | None:
return (
_db.query(IntegrationConfiguration)
.filter(IntegrationConfiguration.id == self.integration_id)
.one_or_none()
)

@classmethod
def settings_class(cls) -> type[AuthProviderSettings]:
return AuthProviderSettings

@classmethod
def library_settings_class(
cls,
) -> type[AuthProviderLibrarySettings]:
return AuthProviderLibrarySettings

@property
@abstractmethod
def identifies_individuals(self):
Expand Down Expand Up @@ -111,6 +114,11 @@ def get_credential_from_header(self, auth: Authorization) -> str | None:
...


AuthenticationProviderType = AuthenticationProvider[
AuthProviderSettings, AuthProviderLibrarySettings
]


class CannotCreateLocalPatron(Exception):
"""A remote system provided information about a patron, but we could
not put it into our database schema.
Expand Down
26 changes: 12 additions & 14 deletions api/authentication/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, Dict, Generator, List, Optional, Pattern
from typing import Any, Dict, Generator, List, Optional, Pattern, TypeVar

from flask import url_for
from pydantic import PositiveInt, validator
Expand Down Expand Up @@ -259,7 +259,15 @@ def validate_restriction_criteria(
return v


class BasicAuthenticationProvider(AuthenticationProvider, ABC):
SettingsType = TypeVar("SettingsType", bound=BasicAuthProviderSettings, covariant=True)
LibrarySettingsType = TypeVar(
"LibrarySettingsType", bound=BasicAuthProviderLibrarySettings, covariant=True
)


class BasicAuthenticationProvider(
AuthenticationProvider[SettingsType, LibrarySettingsType], ABC
):
"""Verify a username/password, obtained through HTTP Basic Auth, with
a remote source of truth.
"""
Expand All @@ -268,8 +276,8 @@ def __init__(
self,
library_id: int,
integration_id: int,
settings: BasicAuthProviderSettings,
library_settings: BasicAuthProviderLibrarySettings,
settings: SettingsType,
library_settings: LibrarySettingsType,
analytics: Analytics | None = None,
):
"""Create a BasicAuthenticationProvider."""
Expand Down Expand Up @@ -337,16 +345,6 @@ def authentication_realm(self) -> str:
def flow_type(self) -> str:
return "http://opds-spec.org/auth/basic"

@classmethod
def settings_class(cls) -> type[BasicAuthProviderSettings]:
return BasicAuthProviderSettings

@classmethod
def library_settings_class(
cls,
) -> type[BasicAuthProviderLibrarySettings]:
return BasicAuthProviderLibrarySettings

@abstractmethod
def remote_patron_lookup(
self, patron_or_patrondata: PatronData | Patron
Expand Down
21 changes: 18 additions & 3 deletions api/authentication/basic_token.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Generator, cast
from typing import TYPE_CHECKING, Generator, Type, cast

from flask import url_for
from sqlalchemy.orm import Session
from werkzeug.datastructures import Authorization

from api.authentication.access_token import AccessTokenProvider
from api.authentication.base import AuthenticationProvider
from api.authentication.base import (
AuthenticationProvider,
AuthProviderLibrarySettings,
AuthProviderSettings,
)
from api.authentication.basic import BasicAuthenticationProvider
from api.problem_details import PATRON_AUTH_ACCESS_TOKEN_INVALID
from core.integration.base import LibrarySettingsType, SettingsType
from core.model import Patron, Session, get_one
from core.selftest import SelfTestResult
from core.util.problem_detail import ProblemDetail, ProblemError
Expand All @@ -18,11 +23,21 @@
from core.model import Library


class BasicTokenAuthenticationProvider(AuthenticationProvider):
class BasicTokenAuthenticationProvider(
AuthenticationProvider[AuthProviderSettings, AuthProviderLibrarySettings]
):
"""Patron Authentication based on a CM generated Access Token
It is a companion to the basic authentication, and has no meaning without it.
"""

@classmethod
def library_settings_class(cls) -> Type[LibrarySettingsType]:
raise NotImplementedError()

@classmethod
def settings_class(cls) -> Type[SettingsType]:
raise NotImplementedError()

FLOW_TYPE = "http://thepalaceproject.org/authtype/basic-token"

def __init__(
Expand Down
10 changes: 8 additions & 2 deletions api/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
from api.adobe_vendor_id import AuthdataUtility
from api.annotations import AnnotationWriter
from api.authentication.access_token import AccessTokenProvider
from api.authentication.base import AuthenticationProvider
from api.authentication.base import (
AuthenticationProvider,
LibrarySettingsType,
SettingsType,
)
from api.authentication.basic import BasicAuthenticationProvider
from api.authentication.basic_token import BasicTokenAuthenticationProvider
from api.config import CannotLoadConfiguration, Configuration
Expand Down Expand Up @@ -842,7 +846,9 @@ def bearer_token_signing_secret(cls, db):
return ConfigurationSetting.sitewide_secret(db, cls.BEARER_TOKEN_SIGNING_SECRET)


class BaseSAMLAuthenticationProvider(AuthenticationProvider, BearerTokenSigner, ABC):
class BaseSAMLAuthenticationProvider(
AuthenticationProvider[SettingsType, LibrarySettingsType], BearerTokenSigner, ABC
):
"""
Base class for SAML authentication providers
"""
Expand Down
21 changes: 10 additions & 11 deletions api/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,8 @@ class Axis360API(
CirculationInternalFormatsMixin,
Axis360APIConstants,
):
NAME = ExternalIntegration.AXIS_360

SET_DELIVERY_MECHANISM_AT = BaseCirculationAPI.BORROW_STEP

SERVICE_NAME = "Axis 360"
DATE_FORMAT = "%m-%d-%Y %H:%M:%S"

access_token_endpoint = "accesstoken"
Expand Down Expand Up @@ -208,7 +205,7 @@ def library_settings_class(cls) -> Type[Axis360LibrarySettings]:

@classmethod
def label(cls) -> str:
return cls.NAME
return ExternalIntegration.AXIS_360

@classmethod
def description(cls) -> str:
Expand All @@ -223,12 +220,12 @@ def __init__(self, _db: Session, collection: Collection) -> None:

super().__init__(_db, collection)
self.library_id = collection.external_account_id or ""
config = self.configuration()
self.username = config.username
self.password = config.password
settings = self.settings
self.username = settings.username
self.password = settings.password

# Convert the nickname for a server into an actual URL.
base_url = config.url or self.PRODUCTION_BASE_URL
base_url = settings.url or self.PRODUCTION_BASE_URL
if base_url in self.SERVER_NICKNAMES:
base_url = self.SERVER_NICKNAMES[base_url]
if not base_url.endswith("/"):
Expand All @@ -240,7 +237,9 @@ def __init__(self, _db: Session, collection: Collection) -> None:

self.token: Optional[str] = None
self.verify_certificate: bool = (
config.verify_certificate if config.verify_certificate is not None else True
settings.verify_certificate
if settings.verify_certificate is not None
else True
)

@property
Expand Down Expand Up @@ -404,7 +403,7 @@ def checkin(self, patron: Patron, pin: str, licensepool: LicensePool) -> None:
response.content
)
except etree.XMLSyntaxError as e:
raise RemoteInitiatedServerError(response.content, self.SERVICE_NAME)
raise RemoteInitiatedServerError(response.content, self.label())

def _checkin(
self, title_id: Optional[str], patron_id: Optional[str]
Expand Down Expand Up @@ -448,7 +447,7 @@ def checkout(
raise CannotLoan()
return loan_info
except etree.XMLSyntaxError as e:
raise RemoteInitiatedServerError(response.content, self.SERVICE_NAME)
raise RemoteInitiatedServerError(response.content, self.label())

def _checkout(
self, title_id: Optional[str], patron_id: Optional[str], internal_format: str
Expand Down
15 changes: 8 additions & 7 deletions api/bibliotheca.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ class BibliothecaAPI(
PatronActivityCirculationAPI[BibliothecaSettings, BibliothecaLibrarySettings],
HasCollectionSelfTests,
):
NAME = ExternalIntegration.BIBLIOTHECA
AUTH_TIME_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
ARGUMENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
AUTHORIZATION_FORMAT = "3MCLAUTH %s:%s"
Expand All @@ -148,10 +147,12 @@ def settings_class(cls):
def library_settings_class(cls):
return BibliothecaLibrarySettings

def label(self):
return self.NAME
@classmethod
def label(cls):
return ExternalIntegration.BIBLIOTHECA

def description(self):
@classmethod
def description(cls):
return ""

def __init__(self, _db, collection):
Expand All @@ -164,10 +165,10 @@ def __init__(self, _db, collection):
super().__init__(_db, collection)

self._db = _db
config = self.configuration()
settings = self.settings
self.version = self.DEFAULT_VERSION
self.account_id = config.username
self.account_key = config.password
self.account_id = settings.username
self.account_key = settings.password
self.library_id = collection.external_account_id
self.base_url = self.DEFAULT_BASE_URL

Expand Down
Loading

0 comments on commit 139ec87

Please sign in to comment.