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

Give OPDS importers an API class (PP-501) #1442

Merged
merged 8 commits into from
Oct 18, 2023
Merged
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
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
28 changes: 15 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,8 +47,8 @@ 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
Expand All @@ -58,16 +65,6 @@ def integration(self, _db: Session) -> IntegrationConfiguration | None:
.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 +108,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()

Check warning on line 35 in api/authentication/basic_token.py

View check run for this annotation

Codecov / codecov/patch

api/authentication/basic_token.py#L35

Added line #L35 was not covered by tests

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

Check warning on line 39 in api/authentication/basic_token.py

View check run for this annotation

Codecov / codecov/patch

api/authentication/basic_token.py#L39

Added line #L39 was not covered by tests

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
24 changes: 12 additions & 12 deletions api/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
FulfillmentInfo,
HoldInfo,
LoanInfo,
PatronActivityCirculationAPI,
)
from api.circulation_exceptions import *
from api.selftest import HasCollectionSelfTests, SelfTestResult
Expand Down Expand Up @@ -158,16 +159,13 @@ class Axis360LibrarySettings(BaseCirculationLoanSettings):


class Axis360API(
BaseCirculationAPI[Axis360Settings, Axis360LibrarySettings],
PatronActivityCirculationAPI[Axis360Settings, Axis360LibrarySettings],
HasCollectionSelfTests,
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 @@ -207,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 @@ -222,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 @@ -239,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 @@ -403,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 @@ -447,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
18 changes: 10 additions & 8 deletions api/bibliotheca.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
FulfillmentInfo,
HoldInfo,
LoanInfo,
PatronActivityCirculationAPI,
)
from api.circulation_exceptions import *
from api.selftest import HasCollectionSelfTests, SelfTestResult
Expand Down Expand Up @@ -118,10 +119,9 @@ class BibliothecaLibrarySettings(BaseCirculationLoanSettings):


class BibliothecaAPI(
BaseCirculationAPI[BibliothecaSettings, BibliothecaLibrarySettings],
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 @@ -147,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 @@ -163,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
Loading