예제 #1
0
    def test_to_settings(self):
        # Arrange
        option = ConfigurationOption("key1", "value1")
        expected_result = {"key": "key1", "label": "value1"}

        # Act
        result = option.to_settings()

        # Assert
        assert result == expected_result
예제 #2
0
class ConfigurationWithBooleanProperty(ConfigurationGrouping):
    boolean_setting = ConfigurationMetadata(
        key="boolean_setting",
        label="Boolean Setting",
        description="Boolean Setting",
        type=ConfigurationAttributeType.SELECT,
        required=True,
        default="true",
        options=[
            ConfigurationOption("true", "True"),
            ConfigurationOption("false", "False"),
        ],
    )
예제 #3
0
    def test_from_enum(self):
        # Arrange
        class TestEnum(Enum):
            LABEL1 = "KEY1"
            LABEL2 = "KEY2"

        expected_result = [
            ConfigurationOption("KEY1", "LABEL1"),
            ConfigurationOption("KEY2", "LABEL2"),
        ]

        # Act
        result = ConfigurationOption.from_enum(TestEnum)

        # Assert
        assert result == expected_result
예제 #4
0
    def __get__(self, instance, owner):
        """Return a SETTINGS-compatible dictionary.

        :return: SETTINGS-compatible dictionary
        :rtype: Dict
        """
        with self._mutex:
            if not SAMLConfiguration.federated_identity_provider_entity_ids.options:
                try:
                    from api.app import app

                    # 1. Load all InCommon IdPs from the database
                    incommon_federated_identity_providers = (
                        app._db.query(
                            SAMLFederatedIdentityProvider.entity_id,
                            SAMLFederatedIdentityProvider.display_name,
                        )
                        .join(SAMLFederation)
                        .filter(SAMLFederation.type == incommon.FEDERATION_TYPE)
                        .order_by(SAMLFederatedIdentityProvider.display_name)
                    ).all()

                    # 2. Convert SAMLFederatedIdentityProvider objects to ConfigurationOption objects
                    configuration_options = []
                    for (
                        incommon_federated_identity_provider
                    ) in incommon_federated_identity_providers:
                        configuration_options.append(
                            ConfigurationOption(
                                key=incommon_federated_identity_provider[0],
                                label=incommon_federated_identity_provider[1],
                            )
                        )

                    # 3. Update SAMLConfiguration.federated_identity_provider_entity_ids.options
                    SAMLConfiguration.federated_identity_provider_entity_ids = ConfigurationMetadata(
                        SAMLConfiguration.federated_identity_provider_entity_ids.key,
                        SAMLConfiguration.federated_identity_provider_entity_ids.label,
                        SAMLConfiguration.federated_identity_provider_entity_ids.description,
                        SAMLConfiguration.federated_identity_provider_entity_ids.type,
                        SAMLConfiguration.federated_identity_provider_entity_ids.required,
                        SAMLConfiguration.federated_identity_provider_entity_ids.default,
                        configuration_options,
                        SAMLConfiguration.federated_identity_provider_entity_ids.category,
                        SAMLConfiguration.federated_identity_provider_entity_ids.format,
                        SAMLConfiguration.federated_identity_provider_entity_ids.index,
                    )
                except:
                    pass

            # 4. Return updated settings
            return SAMLConfiguration.to_settings()
예제 #5
0
class ProQuestOPDS2ImporterConfiguration(ConfigurationGrouping):
    """Contains configuration settings of ProQuestOPDS2Importer."""

    DEFAULT_TOKEN_EXPIRATION_TIMEOUT_SECONDS = 60 * 60
    TEST_AFFILIATION_ID = 1
    DEFAULT_AFFILIATION_ATTRIBUTES = [
        SAMLAttributeType.eduPersonPrincipalName.name,
        SAMLAttributeType.eduPersonScopedAffiliation.name,
    ]

    data_source_name = ConfigurationMetadata(
        key=Collection.DATA_SOURCE_NAME_SETTING,
        label=_("Data source name"),
        description=_(
            "Name of the data source associated with this collection."),
        type=ConfigurationAttributeType.TEXT,
        required=True,
        default="ProQuest",
    )

    token_expiration_timeout = ConfigurationMetadata(
        key="token_expiration_timeout",
        label=_("ProQuest JWT token's expiration timeout"),
        description=_(
            "Determines how long in seconds can a ProQuest JWT token be valid."
        ),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=DEFAULT_TOKEN_EXPIRATION_TIMEOUT_SECONDS,
    )

    affiliation_attributes = ConfigurationMetadata(
        key="affiliation_attributes",
        label=_("List of SAML attributes containing an affiliation ID"),
        description=
        _("ProQuest integration assumes that the SAML provider is used for authentication. "
          "ProQuest JWT bearer tokens required by the most ProQuest API services "
          "are created based on the affiliation ID - SAML attribute uniquely identifying the patron."
          "This setting determines what attributes the ProQuest integration will use to look for affiliation IDs. "
          "The ProQuest integration will investigate the specified attributes sequentially "
          "and will take the first non-empty value."),
        type=ConfigurationAttributeType.MENU,
        required=False,
        default=list(DEFAULT_AFFILIATION_ATTRIBUTES),
        options=[
            ConfigurationOption(attribute.name, attribute.name)
            for attribute in SAMLAttributeType
        ],
        format="narrow",
    )

    test_affiliation_id = ConfigurationMetadata(
        key="test_affiliation_id",
        label=_("Test SAML affiliation ID"),
        description=_(
            "Test SAML affiliation ID used for testing ProQuest API. "
            "Please contact ProQuest before using it."),
        type=ConfigurationAttributeType.TEXT,
        required=False,
    )

    default_audience = ConfigurationMetadata(
        key=Collection.DEFAULT_AUDIENCE_KEY,
        label=_("Default audience"),
        description=_(
            "If ProQuest does not specify the target audience for their books, "
            "assume the books have this target audience."),
        type=ConfigurationAttributeType.SELECT,
        required=False,
        default=OPDSImporter.NO_DEFAULT_AUDIENCE,
        options=[
            ConfigurationOption(key=OPDSImporter.NO_DEFAULT_AUDIENCE,
                                label=_("No default audience"))
        ] + [
            ConfigurationOption(key=audience, label=audience)
            for audience in sorted(Classifier.AUDIENCES)
        ],
        format="narrow",
    )
예제 #6
0
class LCPServerConfiguration(ConfigurationGrouping):
    """Contains LCP License Server's settings"""

    DEFAULT_PAGE_SIZE = 100
    DEFAULT_PASSPHRASE_HINT = 'If you do not remember your passphrase, please contact your administrator'
    DEFAULT_ENCRYPTION_ALGORITHM = HashingAlgorithm.SHA256.value

    lcpserver_url = ConfigurationMetadata(
        key='lcpserver_url',
        label=_('LCP License Server\'s URL'),
        description=_('URL of the LCP License Server'),
        type=ConfigurationAttributeType.TEXT,
        required=True
    )

    lcpserver_user = ConfigurationMetadata(
        key='lcpserver_user',
        label=_('LCP License Server\'s user'),
        description=_('Name of the user used to connect to the LCP License Server'),
        type=ConfigurationAttributeType.TEXT,
        required=True
    )

    lcpserver_password = ConfigurationMetadata(
        key='lcpserver_password',
        label=_('LCP License Server\'s password'),
        description=_('Password of the user used to connect to the LCP License Server'),
        type=ConfigurationAttributeType.TEXT,
        required=True
    )

    lcpserver_input_directory = ConfigurationMetadata(
        key='lcpserver_input_directory',
        label=_('LCP License Server\'s input directory'),
        description=_(
            'Full path to the directory containing encrypted books. '
            'This directory should be the same as lcpencrypt\'s output directory'
        ),
        type=ConfigurationAttributeType.TEXT,
        required=True
    )

    lcpserver_page_size = ConfigurationMetadata(
        key='lcpserver_page_size',
        label=_('LCP License Server\'s page size'),
        description=_('Number of licences returned by the server'),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=DEFAULT_PAGE_SIZE
    )

    provider_name = ConfigurationMetadata(
        key='provider_name',
        label=_('LCP service provider\'s identifier'),
        description=_(
            'URI that identifies the provider in an unambiguous way'
        ),
        type=ConfigurationAttributeType.TEXT,
        required=True
    )

    passphrase_hint = ConfigurationMetadata(
        key='passphrase_hint',
        label=_('Passphrase hint'),
        description=_('Hint proposed to the user for selecting their passphrase'),
        type=ConfigurationAttributeType.TEXT,
        required=False,
        default=DEFAULT_PASSPHRASE_HINT
    )

    encryption_algorithm = ConfigurationMetadata(
        key='encryption_algorithm',
        label=_('Passphrase encryption algorithm'),
        description=_('Algorithm used for encrypting the passphrase'),
        type=ConfigurationAttributeType.SELECT,
        required=False,
        default=DEFAULT_ENCRYPTION_ALGORITHM,
        options=ConfigurationOption.from_enum(HashingAlgorithm)
    )

    max_printable_pages = ConfigurationMetadata(
        key='max_printable_pages',
        label=_('Maximum number or printable pages'),
        description=_('Maximum number of pages that can be printed over the lifetime of the license'),
        type=ConfigurationAttributeType.NUMBER,
        required=False
    )

    max_copiable_pages = ConfigurationMetadata(
        key='max_copiable_pages',
        label=_('Maximum number or copiable characters'),
        description=_('Maximum number of characters that can be copied to the clipboard'),
        type=ConfigurationAttributeType.NUMBER,
        required=False
    )
예제 #7
0
SETTING1_KEY = "setting1"
SETTING1_LABEL = "Setting 1's label"
SETTING1_DESCRIPTION = "Setting 1's description"
SETTING1_TYPE = ConfigurationAttributeType.TEXT
SETTING1_REQUIRED = False
SETTING1_DEFAULT = "12345"
SETTING1_CATEGORY = "Settings"

SETTING2_KEY = "setting2"
SETTING2_LABEL = "Setting 2's label"
SETTING2_DESCRIPTION = "Setting 2's description"
SETTING2_TYPE = ConfigurationAttributeType.SELECT
SETTING2_REQUIRED = False
SETTING2_DEFAULT = "value1"
SETTING2_OPTIONS = [
    ConfigurationOption("key1", "value1"),
    ConfigurationOption("key2", "value2"),
    ConfigurationOption("key3", "value3"),
]
SETTING2_CATEGORY = "Settings"

SETTING3_KEY = "setting3"
SETTING3_LABEL = "Setting 3's label"
SETTING3_DESCRIPTION = "Setting 3's description"
SETTING3_TYPE = ConfigurationAttributeType.MENU
SETTING3_REQUIRED = False
SETTING3_OPTIONS = [
    ConfigurationOption("key1", "value1"),
    ConfigurationOption("key2", "value2"),
    ConfigurationOption("key3", "value3"),
]
예제 #8
0
class LCPServerConfiguration(ConfigurationGrouping):
    """Contains LCP License Server's settings"""

    DEFAULT_PAGE_SIZE = 100
    DEFAULT_PASSPHRASE_HINT = (
        "If you do not remember your passphrase, please contact your administrator"
    )
    DEFAULT_ENCRYPTION_ALGORITHM = HashingAlgorithm.SHA256.value

    lcpserver_url = ConfigurationMetadata(
        key="lcpserver_url",
        label=_("LCP License Server's URL"),
        description=_("URL of the LCP License Server"),
        type=ConfigurationAttributeType.TEXT,
        required=True,
    )

    lcpserver_user = ConfigurationMetadata(
        key="lcpserver_user",
        label=_("LCP License Server's user"),
        description=_(
            "Name of the user used to connect to the LCP License Server"),
        type=ConfigurationAttributeType.TEXT,
        required=True,
    )

    lcpserver_password = ConfigurationMetadata(
        key="lcpserver_password",
        label=_("LCP License Server's password"),
        description=_(
            "Password of the user used to connect to the LCP License Server"),
        type=ConfigurationAttributeType.TEXT,
        required=True,
    )

    lcpserver_input_directory = ConfigurationMetadata(
        key="lcpserver_input_directory",
        label=_("LCP License Server's input directory"),
        description=_(
            "Full path to the directory containing encrypted books. "
            "This directory should be the same as lcpencrypt's output directory"
        ),
        type=ConfigurationAttributeType.TEXT,
        required=True,
    )

    lcpserver_page_size = ConfigurationMetadata(
        key="lcpserver_page_size",
        label=_("LCP License Server's page size"),
        description=_("Number of licences returned by the server"),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=DEFAULT_PAGE_SIZE,
    )

    provider_name = ConfigurationMetadata(
        key="provider_name",
        label=_("LCP service provider's identifier"),
        description=_(
            "URI that identifies the provider in an unambiguous way"),
        type=ConfigurationAttributeType.TEXT,
        required=True,
    )

    passphrase_hint = ConfigurationMetadata(
        key="passphrase_hint",
        label=_("Passphrase hint"),
        description=_(
            "Hint proposed to the user for selecting their passphrase"),
        type=ConfigurationAttributeType.TEXT,
        required=False,
        default=DEFAULT_PASSPHRASE_HINT,
    )

    encryption_algorithm = ConfigurationMetadata(
        key="encryption_algorithm",
        label=_("Passphrase encryption algorithm"),
        description=_("Algorithm used for encrypting the passphrase"),
        type=ConfigurationAttributeType.SELECT,
        required=False,
        default=DEFAULT_ENCRYPTION_ALGORITHM,
        options=ConfigurationOption.from_enum(HashingAlgorithm),
    )

    max_printable_pages = ConfigurationMetadata(
        key="max_printable_pages",
        label=_("Maximum number or printable pages"),
        description=
        _("Maximum number of pages that can be printed over the lifetime of the license"
          ),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
    )

    max_copiable_pages = ConfigurationMetadata(
        key="max_copiable_pages",
        label=_("Maximum number or copiable characters"),
        description=_(
            "Maximum number of characters that can be copied to the clipboard"
        ),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
    )
예제 #9
0
class SAMLConfiguration(ConfigurationGrouping):
    """Contains SP and IdP settings."""

    service_provider_xml_metadata = ConfigurationMetadata(
        key="sp_xml_metadata",
        label=_("Service Provider's XML Metadata"),
        description=_(
            "SAML metadata of the Circulation Manager's Service Provider in an XML format. "
            "MUST contain exactly one SPSSODescriptor tag with at least one "
            "AssertionConsumerService tag with Binding attribute set to "
            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST."
        ),
        type=ConfigurationAttributeType.TEXTAREA,
        required=True,
    )

    service_provider_private_key = ConfigurationMetadata(
        key="sp_private_key",
        label=_("Service Provider's Private Key"),
        description=_("Private key used for encrypting SAML requests."),
        type=ConfigurationAttributeType.TEXTAREA,
        required=False,
    )

    federated_identity_provider_entity_ids = ConfigurationMetadata(
        key="saml_federated_idp_entity_ids",
        label=_("List of Federated IdPs"),
        description=_(
            "List of federated (for example, from InCommon Federation) IdPs supported by this authentication provider. "
            "Try to type the name of the IdP to find it in the list."
        ),
        type=ConfigurationAttributeType.MENU,
        required=False,
        options=[],
        default=[],
        format="narrow",
    )

    patron_id_use_name_id = ConfigurationMetadata(
        key="saml_patron_id_use_name_id",
        label=_("Patron ID: SAML NameID"),
        description=_(
            "Configuration setting indicating whether SAML NameID should be searched for a unique patron ID. "
            "If NameID found, it will supersede any SAML attributes selected in the next section."
        ),
        type=ConfigurationAttributeType.SELECT,
        required=False,
        default="true",
        options=[
            ConfigurationOption("true", "Use SAML NameID"),
            ConfigurationOption("false", "Do NOT use SAML NameID"),
        ],
    )

    patron_id_attributes = ConfigurationMetadata(
        key="saml_patron_id_attributes",
        label=_("Patron ID: SAML Attributes"),
        description=_(
            "List of SAML attributes that MAY contain a unique patron ID. "
            "The attributes will be scanned sequentially in the order you chose them, "
            "and the first existing attribute will be used to extract a unique patron ID."
            "<br>"
            "NOTE: If a SAML attribute contains several values, only the first will be used."
        ),
        type=ConfigurationAttributeType.MENU,
        required=False,
        options=[
            ConfigurationOption(attribute.name, attribute.name)
            for attribute in SAMLAttributeType
        ],
        default=[
            SAMLAttributeType.eduPersonUniqueId.name,
            SAMLAttributeType.eduPersonTargetedID.name,
            SAMLAttributeType.eduPersonPrincipalName.name,
            SAMLAttributeType.uid.name,
        ],
        format="narrow",
    )

    patron_id_regular_expression = ConfigurationMetadata(
        key="saml_patron_id_regular_expression",
        label=_("Patron ID: Regular expression"),
        description=_(
            "Regular expression used to extract a unique patron ID from the attributes "
            "specified in <b>Patron ID: SAML Attributes</b> and/or NameID "
            "(if it's enabled in <b>Patron ID: SAML NameID</b>). "
            "<br>"
            "The expression MUST contain a named group <b>patron_id</b> used to match the patron ID. "
            "For example:"
            "<br>"
            "<pre>"
            "{the_regex_pattern}"
            "</pre>"
            "The expression will extract the <b>patron_id</b> from the first SAML attribute that matches "
            "or NameID if it matches the expression."
        ).format(the_regex_pattern=cgi.escape(r"(?P<patron_id>.+)@university\.org")),
        type=ConfigurationAttributeType.TEXT,
        required=False,
    )

    non_federated_identity_provider_xml_metadata = ConfigurationMetadata(
        key="idp_xml_metadata",
        label=_("Identity Provider's XML metadata"),
        description=_(
            "SAML metadata of Identity Providers in an XML format. "
            "MAY contain multiple IDPSSODescriptor tags but each of them MUST contain "
            "at least one SingleSignOnService tag with Binding attribute set to "
            "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect."
        ),
        type=ConfigurationAttributeType.TEXTAREA,
        required=False,
    )

    session_lifetime = ConfigurationMetadata(
        key="saml_session_lifetime",
        label=_("Session Lifetime"),
        description=_(
            "This configuration setting determines how long "
            "a session created by the SAML authentication provider will live in days. "
            "By default it's empty meaning that the lifetime of the Circulation Manager's session "
            "is exactly the same as the lifetime of the IdP's session. "
            "Setting this value to a specific number will override this behaviour."
            "<br>"
            "NOTE: This setting affects the session's lifetime only Circulation Manager's side. "
            "Accessing content protected by SAML will still be governed by the IdP and patrons "
            "will have to reauthenticate each time the IdP's session expires."
        ),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=None,
    )

    filter_expression = ConfigurationMetadata(
        key="saml_filter_expression",
        label=_("Filter Expression"),
        description=_(
            "Python expression used for filtering out patrons by their SAML attributes."
            "<br>"
            "<br>"
            'For example, if you want to authenticate using SAML only patrons having "eresources" '
            'as the value of their "eduPersonEntitlement" then you need to use the following expression:'
            "<br>"
            "<pre>"
            """
"urn:mace:nyu.edu:entl:lib:eresources" == subject.attribute_statement.attributes["eduPersonEntitlement"].values[0]
"""
            "</pre>"
            "<br>"
            'If "eduPersonEntitlement" can have multiple values, you can use the following expression:'
            "<br>"
            "<pre>"
            """
"urn:mace:nyu.edu:entl:lib:eresources" in subject.attribute_statement.attributes["eduPersonEntitlement"].values
"""
            "</pre>"
        ),
        type=ConfigurationAttributeType.TEXTAREA,
        required=False,
    )

    service_provider_strict_mode = ConfigurationMetadata(
        key="strict",
        label=_("Service Provider's Strict Mode"),
        description=_(
            "If strict is 1, then the Python Toolkit will reject unsigned or unencrypted messages "
            "if it expects them to be signed or encrypted. Also, it will reject the messages "
            "if the SAML standard is not strictly followed."
        ),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=0,
    )

    service_provider_debug_mode = ConfigurationMetadata(
        key="debug",
        label=_("Service Provider's Debug Mode"),
        description=_("Enable debug mode (outputs errors)."),
        type=ConfigurationAttributeType.NUMBER,
        required=False,
        default=0,
    )

    IDP_DISPLAY_NAME_DEFAULT_TEMPLATE = "Identity Provider #{0}"

    def __init__(self, configuration_storage, db, metadata_parser):
        """Initializes a new instance of SAMLConfiguration class

        :param configuration_storage: SAML configuration storage
        :type configuration_storage: ConfigurationStorage

        :param metadata_parser: SAML metadata parser
        :type metadata_parser: SAMLMetadataParser
        """
        super(SAMLConfiguration, self).__init__(configuration_storage, db)

        self._metadata_parser = metadata_parser

        self._identity_providers = None
        self._service_provider = None

    def _get_federated_identity_providers(self, db):
        """Return a list of federated IdPs corresponding to the entity IDs selected by the admin.

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :return: List of SAMLFederatedIdP objects
        :rtype: List[api.saml.metadata.federations.model.SAMLFederatedIdP]
        """
        if not self.federated_identity_provider_entity_ids:
            return []

        return (
            db.query(SAMLFederatedIdentityProvider)
            .filter(
                SAMLFederatedIdentityProvider.entity_id.in_(
                    self.federated_identity_provider_entity_ids
                )
            )
            .all()
        )

    def _load_identity_providers(self, db):
        """Loads IdP settings from the library's configuration settings

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :return: List of IdentityProviderMetadata objects
        :rtype: List[IdentityProviderMetadata]

        :raise: SAMLParsingError
        """
        identity_providers = []

        if self.non_federated_identity_provider_xml_metadata:
            parsing_results = self._metadata_parser.parse(
                self.non_federated_identity_provider_xml_metadata
            )
            identity_providers = [
                parsing_result.provider for parsing_result in parsing_results
            ]

        if self.federated_identity_provider_entity_ids:
            for identity_provider_metadata in self._get_federated_identity_providers(
                db
            ):
                parsing_results = self._metadata_parser.parse(
                    identity_provider_metadata.xml_metadata
                )

                for parsing_result in parsing_results:
                    identity_providers.append(parsing_result.provider)

        return identity_providers

    def _load_service_provider(self, db):
        """Loads SP settings from the library's configuration settings

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :return: SAMLServiceProviderMetadata object
        :rtype: SAMLServiceProviderMetadata

        :raise: SAMLParsingError
        """
        parsing_results = self._metadata_parser.parse(
            self.service_provider_xml_metadata
        )

        if not isinstance(parsing_results, list) or len(parsing_results) != 1:
            raise SAMLConfigurationError(
                _("SAML Service Provider's configuration is not correct")
            )

        parsing_result = parsing_results[0]
        service_provider = parsing_result.provider

        if not isinstance(service_provider, SAMLServiceProviderMetadata):
            raise SAMLConfigurationError(
                _("SAML Service Provider's configuration is not correct")
            )

        service_provider.private_key = (
            self.service_provider_private_key
            if self.service_provider_private_key
            else ""
        )

        return service_provider

    def get_identity_providers(self, db):
        """Returns identity providers

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :return: List of IdentityProviderMetadata objects
        :rtype: List[IdentityProviderMetadata]

        :raise: ConfigurationError
        """
        if self._identity_providers is None:
            self._identity_providers = self._load_identity_providers(db)

        return self._identity_providers

    def get_service_provider(self, db):
        """Returns service provider

        :param db: Database session
        :type db: sqlalchemy.orm.session.Session

        :return: ServiceProviderMetadata object
        :rtype: ServiceProviderMetadata

        :raise: ConfigurationError
        """
        if self._service_provider is None:
            self._service_provider = self._load_service_provider(db)

        return self._service_provider