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
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"), ], )
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
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()
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", )
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 )
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"), ]
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, )
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