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