def test_sitewide_settings_get(self): with self.request_context_with_admin("/"): response = self.manager.admin_sitewide_configuration_settings_controller.process_get() settings = response.get("settings") all_settings = response.get("all_settings") eq_([], settings) keys = [s.get("key") for s in all_settings] assert AcquisitionFeed.GROUPED_MAX_AGE_POLICY in keys assert AcquisitionFeed.NONGROUPED_MAX_AGE_POLICY in keys assert Configuration.SECRET_KEY in keys ConfigurationSetting.sitewide(self._db, AcquisitionFeed.GROUPED_MAX_AGE_POLICY).value = 0 ConfigurationSetting.sitewide(self._db, Configuration.SECRET_KEY).value = "secret" self._db.flush() with self.request_context_with_admin("/"): response = self.manager.admin_sitewide_configuration_settings_controller.process_get() settings = response.get("settings") all_settings = response.get("all_settings") eq_(2, len(settings)) settings_by_key = { s.get("key") : s.get("value") for s in settings } eq_("0", settings_by_key.get(AcquisitionFeed.GROUPED_MAX_AGE_POLICY)) eq_("secret", settings_by_key.get(Configuration.SECRET_KEY)) keys = [s.get("key") for s in all_settings] assert AcquisitionFeed.GROUPED_MAX_AGE_POLICY in keys assert AcquisitionFeed.NONGROUPED_MAX_AGE_POLICY in keys assert Configuration.SECRET_KEY in keys self.admin.remove_role(AdminRole.SYSTEM_ADMIN) self._db.flush() assert_raises(AdminNotAuthorized, self.manager.admin_sitewide_configuration_settings_controller.process_get)
def test_catalog_services_post_create(self): ME = MARCExporter s3, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.S3, goal=ExternalIntegration.STORAGE_GOAL, ) s3.setting(S3Uploader.MARC_BUCKET_KEY).value = "marc-files" with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "exporter name"), ("protocol", ME.NAME), (ME.STORAGE_PROTOCOL, ExternalIntegration.S3), ("libraries", json.dumps([{ "short_name": self._default_library.short_name, ME.INCLUDE_SUMMARY: "false", ME.INCLUDE_SIMPLIFIED_GENRES: "true", }])), ]) response = self.manager.admin_catalog_services_controller.process_catalog_services() eq_(response.status_code, 201) service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.CATALOG_GOAL) eq_(service.id, int(response.response[0])) eq_(ME.NAME, service.protocol) eq_("exporter name", service.name) eq_(ExternalIntegration.S3, service.setting(ME.STORAGE_PROTOCOL).value) eq_([self._default_library], service.libraries) eq_("false", ConfigurationSetting.for_library_and_externalintegration( self._db, ME.INCLUDE_SUMMARY, self._default_library, service).value) eq_("true", ConfigurationSetting.for_library_and_externalintegration( self._db, ME.INCLUDE_SIMPLIFIED_GENRES, self._default_library, service).value)
def test_domains(self): super(TestGoogleOAuthAdminAuthenticationProvider, self).setup() auth_integration, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.GOOGLE_OAUTH, goal=ExternalIntegration.ADMIN_AUTH_GOAL ) auth_integration.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_integration ).value = json.dumps(["nypl.org"]) google = GoogleOAuthAdminAuthenticationProvider(auth_integration, "", test_mode=True) eq_(["nypl.org"], google.domains.keys()) eq_([self._default_library], google.domains["nypl.org"]) l2 = self._library() auth_integration.libraries += [l2] ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", l2, auth_integration ).value = json.dumps(["nypl.org", "l2.org"]) eq_(set([self._default_library, l2]), set(google.domains["nypl.org"])) eq_([l2], google.domains["l2.org"])
def test_catalog_services_get_with_marc_exporter(self): integration, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.MARC_EXPORT, goal=ExternalIntegration.CATALOG_GOAL, name="name", ) integration.setting(MARCExporter.STORAGE_PROTOCOL).value = ExternalIntegration.S3 integration.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, MARCExporter.MARC_ORGANIZATION_CODE, self._default_library, integration).value = "US-MaBoDPL" ConfigurationSetting.for_library_and_externalintegration( self._db, MARCExporter.INCLUDE_SUMMARY, self._default_library, integration).value = "false" ConfigurationSetting.for_library_and_externalintegration( self._db, MARCExporter.INCLUDE_SIMPLIFIED_GENRES, self._default_library, integration).value = "true" with self.request_context_with_admin("/"): response = self.manager.admin_catalog_services_controller.process_catalog_services() [service] = response.get("catalog_services") eq_(integration.id, service.get("id")) eq_(integration.name, service.get("name")) eq_(integration.protocol, service.get("protocol")) eq_(ExternalIntegration.S3, service.get("settings").get(MARCExporter.STORAGE_PROTOCOL)) [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_("US-MaBoDPL", library.get(MARCExporter.MARC_ORGANIZATION_CODE)) eq_("false", library.get(MARCExporter.INCLUDE_SUMMARY)) eq_("true", library.get(MARCExporter.INCLUDE_SIMPLIFIED_GENRES))
def process_post(self): self.require_system_admin() setting = ConfigurationSetting.sitewide(self._db, flask.request.form.get("key")) error = self.validate_form_fields(setting, flask.request.form.keys()) if error: return error setting = ConfigurationSetting.sitewide(self._db, flask.request.form.get("key")) setting.value = flask.request.form.get("value") return Response(unicode(setting.key), 200)
def __init__(self, integration, library=None): _db = Session.object_session(integration) if not library: raise CannotLoadConfiguration("Google Analytics can't be configured without a library.") url_setting = ConfigurationSetting.for_externalintegration(ExternalIntegration.URL, integration) self.url = url_setting.value or self.DEFAULT_URL self.tracking_id = ConfigurationSetting.for_library_and_externalintegration( _db, self.TRACKING_ID, library, integration, ).value if not self.tracking_id: raise CannotLoadConfiguration("Missing tracking id for library %s" % library.short_name)
def setup(self): super(TestLaneScript, self).setup() base_url_setting = ConfigurationSetting.sitewide( self._db, Configuration.BASE_URL_KEY) base_url_setting.value = u'http://test-circulation-manager/' for k, v in [ (Configuration.LARGE_COLLECTION_LANGUAGES, []), (Configuration.SMALL_COLLECTION_LANGUAGES, []), (Configuration.TINY_COLLECTION_LANGUAGES, ['eng', 'fre']) ]: ConfigurationSetting.for_library( k, self._default_library).value = json.dumps(v)
def test_callback(self): super(TestGoogleOAuthAdminAuthenticationProvider, self).setup() auth_integration, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.GOOGLE_OAUTH, goal=ExternalIntegration.ADMIN_AUTH_GOAL ) self.google = GoogleOAuthAdminAuthenticationProvider(auth_integration, "", test_mode=True) auth_integration.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_integration ).value = json.dumps(["nypl.org"]) # Returns a problem detail when Google returns an error. error_response, redirect = self.google.callback(self._db, {'error' : 'access_denied'}) eq_(True, isinstance(error_response, ProblemDetail)) eq_(400, error_response.status_code) eq_(True, error_response.detail.endswith('access_denied')) eq_(None, redirect) # Successful case creates a dict of admin details success, redirect = self.google.callback(self._db, {'code' : 'abc'}) eq_('*****@*****.**', success['email']) default_credentials = json.dumps({"id_token": {"email": "*****@*****.**", "hd": "nypl.org"}}) eq_(default_credentials, success['credentials']) eq_(GoogleOAuthAdminAuthenticationProvider.NAME, success["type"]) [role] = success.get("roles") eq_(AdminRole.LIBRARIAN, role.get("role")) eq_(self._default_library.short_name, role.get("library")) # If domains are set, the admin's domain must match one of the domains. setting = ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_integration) setting.value = json.dumps(["otherlibrary.org"]) failure, ignore = self.google.callback(self._db, {'code' : 'abc'}) eq_(INVALID_ADMIN_CREDENTIALS, failure) setting.value = json.dumps(["nypl.org"]) # Returns a problem detail when the oauth client library # raises an exception. class ExceptionRaisingClient(DummyGoogleClient): def step2_exchange(self, auth_code): raise GoogleClient.FlowExchangeError("mock error") self.google.dummy_client = ExceptionRaisingClient() error_response, redirect = self.google.callback(self._db, {'code' : 'abc'}) eq_(True, isinstance(error_response, ProblemDetail)) eq_(400, error_response.status_code) eq_(True, error_response.detail.endswith('mock error')) eq_(None, redirect)
def check_identifier_restriction(self, library, auth_service): """Check whether the library's identifier restriction regular expression is set and is supposed to be a regular expression; if so, check that it's valid.""" identifier_restriction_type = ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE, library, auth_service).value identifier_restriction = ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION, library, auth_service).value if identifier_restriction and identifier_restriction_type == AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE_REGEX: try: re.compile(identifier_restriction) except Exception, e: return INVALID_LIBRARY_IDENTIFIER_RESTRICTION_REGULAR_EXPRESSION
def test_discovery_service_library_registrations_get(self): discovery_service, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.OPDS_REGISTRATION, goal=ExternalIntegration.DISCOVERY_GOAL, ) succeeded, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) ConfigurationSetting.for_library_and_externalintegration( self._db, "library-registration-status", succeeded, discovery_service, ).value = "success" ConfigurationSetting.for_library_and_externalintegration( self._db, "library-registration-stage", succeeded, discovery_service, ).value = "production" failed, ignore = create( self._db, Library, name="Library 2", short_name="L2", ) ConfigurationSetting.for_library_and_externalintegration( self._db, "library-registration-status", failed, discovery_service, ).value = "failure" ConfigurationSetting.for_library_and_externalintegration( self._db, "library-registration-stage", failed, discovery_service, ).value = "testing" unregistered, ignore = create( self._db, Library, name="Library 3", short_name="L3", ) discovery_service.libraries = [succeeded, failed] controller = self.manager.admin_discovery_service_library_registrations_controller with self.request_context_with_admin("/", method="GET"): response = controller.process_discovery_service_library_registrations() serviceInfo = response.get("library_registrations") eq_(1, len(serviceInfo)) eq_(discovery_service.id, serviceInfo[0].get("id")) libraryInfo = serviceInfo[0].get("libraries") expected = [ dict(short_name=succeeded.short_name, status="success", stage="production"), dict(short_name=failed.short_name, status="failure", stage="testing"), ] eq_(expected, libraryInfo) self.admin.remove_role(AdminRole.SYSTEM_ADMIN) self._db.flush() assert_raises(AdminNotAuthorized, controller.process_discovery_service_library_registrations)
def test_libraries_get_with_multiple_libraries(self): # Delete any existing library created by the controller test setup. library = get_one(self._db, Library) if library: self._db.delete(library) l1 = self._library("Library 1", "L1") l2 = self._library("Library 2", "L2") l3 = self._library("Library 3", "L3") # L2 has some additional library-wide settings. ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, l2).value = 5 ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2 ).value = FacetConstants.ORDER_RANDOM ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, l2 ).value = json.dumps([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]) ConfigurationSetting.for_library( Configuration.LARGE_COLLECTION_LANGUAGES, l2 ).value = json.dumps(["French"]) # The admin only has access to L1 and L2. self.admin.remove_role(AdminRole.SYSTEM_ADMIN) self.admin.add_role(AdminRole.LIBRARIAN, l1) self.admin.add_role(AdminRole.LIBRARY_MANAGER, l2) with self.request_context_with_admin("/"): response = self.manager.admin_library_settings_controller.process_get() libraries = response.get("libraries") eq_(2, len(libraries)) eq_(l1.uuid, libraries[0].get("uuid")) eq_(l2.uuid, libraries[1].get("uuid")) eq_(l1.name, libraries[0].get("name")) eq_(l2.name, libraries[1].get("name")) eq_(l1.short_name, libraries[0].get("short_name")) eq_(l2.short_name, libraries[1].get("short_name")) eq_({}, libraries[0].get("settings")) eq_(4, len(libraries[1].get("settings").keys())) settings = libraries[1].get("settings") eq_("5", settings.get(Configuration.FEATURED_LANE_SIZE)) eq_(FacetConstants.ORDER_RANDOM, settings.get(Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) eq_([FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM], settings.get(Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) eq_(["French"], settings.get(Configuration.LARGE_COLLECTION_LANGUAGES))
def max_outstanding_fines(cls, library): max_fines = ConfigurationSetting.for_library( cls.MAX_OUTSTANDING_FINES, library ) if max_fines.value is None: return None return MoneyUtility.parse(max_fines.value)
def check_client_authorization(self, collection, client): """Verify that an IntegrationClient is whitelisted for access to the collection.""" external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration ).json_value if client.url not in [IntegrationClient.normalize_url(url) for url in external_library_urls]: raise AuthorizationFailedException()
def test_admin_auth_services_get_with_google_oauth_service(self): auth_service, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.GOOGLE_OAUTH, goal=ExternalIntegration.ADMIN_AUTH_GOAL ) auth_service.url = "http://oauth.test" auth_service.username = "******" auth_service.password = "******" auth_service.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_service ).value = json.dumps(["nypl.org"]) with self.request_context_with_admin("/"): response = self.manager.admin_auth_services_controller.process_admin_auth_services() [service] = response.get("admin_auth_services") eq_(auth_service.id, service.get("id")) eq_(auth_service.name, service.get("name")) eq_(auth_service.protocol, service.get("protocol")) eq_(auth_service.url, service.get("settings").get("url")) eq_(auth_service.username, service.get("settings").get("username")) eq_(auth_service.password, service.get("settings").get("password")) [library_info] = service.get("libraries") eq_(self._default_library.short_name, library_info.get("short_name")) eq_(["nypl.org"], library_info.get("domains"))
def test_admin_auth_services_post_google_oauth_edit(self): # The auth service exists. auth_service, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.GOOGLE_OAUTH, goal=ExternalIntegration.ADMIN_AUTH_GOAL ) auth_service.url = "url" auth_service.username = "******" auth_service.password = "******" auth_service.libraries += [self._default_library] setting = ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_service) setting.value = json.dumps(["library1.org"]) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "oauth"), ("protocol", "Google OAuth"), ("url", "http://url2"), ("username", "user2"), ("password", "pass2"), ("libraries", json.dumps([{ "short_name": self._default_library.short_name, "domains": ["library2.org"] }])), ]) response = self.manager.admin_auth_services_controller.process_admin_auth_services() eq_(response.status_code, 200) eq_(auth_service.protocol, response.response[0]) eq_("oauth", auth_service.name) eq_("http://url2", auth_service.url) eq_("user2", auth_service.username) eq_("domains", setting.key) eq_(["library2.org"], json.loads(setting.value))
def test_admin_auth_services_post_create(self): with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "oauth"), ("protocol", "Google OAuth"), ("url", "http://url2"), ("username", "username"), ("password", "password"), ("libraries", json.dumps([{ "short_name": self._default_library.short_name, "domains": ["nypl.org", "gmail.com"] }])), ]) response = self.manager.admin_auth_services_controller.process_admin_auth_services() eq_(response.status_code, 201) # The auth service was created and configured properly. auth_service = ExternalIntegration.admin_authentication(self._db) eq_(auth_service.protocol, response.response[0]) eq_("oauth", auth_service.name) eq_("http://url2", auth_service.url) eq_("username", auth_service.username) eq_("password", auth_service.password) eq_([self._default_library], auth_service.libraries) setting = ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_service ) eq_("domains", setting.key) eq_(["nypl.org", "gmail.com"], json.loads(setting.value))
def do_run(self, cmd_args=None, in_unit_test=False): parser = self.arg_parser(self._db) parsed = self.parse_command_line(self._db, cmd_args) url = parsed.registry_url registry = RemoteRegistry.for_protocol_goal_and_url( self._db, self.PROTOCOL, self.GOAL, url ) stage = parsed.stage # Set up an application context so we have access to url_for. from api.app import app app.manager = CirculationManager(self._db, testing=in_unit_test) base_url = ConfigurationSetting.sitewide( self._db, Configuration.BASE_URL_KEY ).value ctx = app.test_request_context(base_url=base_url) ctx.push() for library in parsed.libraries: registration = Registration(registry, library) library_stage = stage or registration.stage_field.value self.process_library( registration, library_stage, app.manager.url_for ) ctx.pop() # For testing purposes, return the application object that was # created. return app
def test_do_run(self): # Normally, do_run is only called by run() if the database has # not yet meen initialized. But we can test it by calling it # directly. timestamp = get_one( self._db, Timestamp, service=u"Database Migration", service_type=Timestamp.SCRIPT_TYPE ) eq_(None, timestamp) # Remove all secret keys, should they exist, before running the # script. secret_keys = self._db.query(ConfigurationSetting).filter( ConfigurationSetting.key==Configuration.SECRET_KEY) [self._db.delete(secret_key) for secret_key in secret_keys] script = InstanceInitializationScript(_db=self._db) script.do_run(ignore_search=True) # It initializes the database. timestamp = get_one( self._db, Timestamp, service=u"Database Migration", service_type=Timestamp.SCRIPT_TYPE ) assert timestamp # It creates a secret key. eq_(1, secret_keys.count()) eq_( secret_keys.one().value, ConfigurationSetting.sitewide_secret(self._db, Configuration.SECRET_KEY) )
def test_staff_email(self): super(TestGoogleOAuthAdminAuthenticationProvider, self).setup() auth_integration, ignore = create( self._db, ExternalIntegration, protocol=ExternalIntegration.GOOGLE_OAUTH, goal=ExternalIntegration.ADMIN_AUTH_GOAL ) nypl_admin = create(self._db, Admin, email="*****@*****.**") bpl_admin = create(self._db, Admin, email="*****@*****.**") # If no domains are set, the admin must already exist in the db # to be considered library staff. google = GoogleOAuthAdminAuthenticationProvider(auth_integration, "", test_mode=True) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(False, google.staff_email(self._db, "*****@*****.**")) # If domains are set, the admin's domain can match one of the domains # if the admin doesn't exist yet. auth_integration.libraries += [self._default_library] setting = ConfigurationSetting.for_library_and_externalintegration( self._db, "domains", self._default_library, auth_integration) setting.value = json.dumps(["nypl.org"]) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(False, google.staff_email(self._db, "*****@*****.**")) setting.value = json.dumps(["nypl.org", "bklynlibrary.org"]) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**")) eq_(True, google.staff_email(self._db, "*****@*****.**"))
def test_patron_auth_services_get_with_millenium_auth_service(self): auth_service, ignore = create( self._db, ExternalIntegration, protocol=MilleniumPatronAPI.__module__, goal=ExternalIntegration.PATRON_AUTH_GOAL ) auth_service.setting(BasicAuthenticationProvider.TEST_IDENTIFIER).value = "user" auth_service.setting(BasicAuthenticationProvider.TEST_PASSWORD).value = "pass" auth_service.setting(BasicAuthenticationProvider.IDENTIFIER_REGULAR_EXPRESSION).value = "u*" auth_service.setting(BasicAuthenticationProvider.PASSWORD_REGULAR_EXPRESSION).value = "p*" auth_service.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, self._default_library, auth_service, ).value = "^(u)" with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services() [service] = response.get("patron_auth_services") eq_(auth_service.id, service.get("id")) eq_(MilleniumPatronAPI.__module__, service.get("protocol")) eq_("user", service.get("settings").get(BasicAuthenticationProvider.TEST_IDENTIFIER)) eq_("pass", service.get("settings").get(BasicAuthenticationProvider.TEST_PASSWORD)) eq_("u*", service.get("settings").get(BasicAuthenticationProvider.IDENTIFIER_REGULAR_EXPRESSION)) eq_("p*", service.get("settings").get(BasicAuthenticationProvider.PASSWORD_REGULAR_EXPRESSION)) [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_("^(u)", library.get(AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION))
def test_default_notification_email_address(self): """Test the ability of the Overdrive API to detect an email address previously given by the patron to Overdrive for the purpose of notifications. """ ignore, patron_with_email = self.sample_json( "patron_info.json" ) self.api.queue_response(200, content=patron_with_email) patron = self._patron() # If the patron has used a particular email address to put # books on hold, use that email address, not the site default. ConfigurationSetting.for_library( Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, self._default_library).value = "*****@*****.**" eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If the patron has never before put an Overdrive book on # hold, their JSON object has no `lastHoldEmail` key. In this # case we use the site default. patron_with_no_email = dict(patron_with_email) del patron_with_no_email['lastHoldEmail'] self.api.queue_response(200, content=patron_with_no_email) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin')) # If there's an error getting the information, use the # site default. self.api.queue_response(404) eq_("*****@*****.**", self.api.default_notification_email_address(patron, 'pin'))
def test_patron_auth_services_get_with_firstbook_auth_service(self): auth_service, ignore = create( self._db, ExternalIntegration, protocol=FirstBookAuthenticationAPI.__module__, goal=ExternalIntegration.PATRON_AUTH_GOAL ) auth_service.url = "url" auth_service.password = "******" auth_service.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, self._default_library, auth_service, ).value = "^(u)" with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services() [service] = response.get("patron_auth_services") eq_(auth_service.id, service.get("id")) eq_(FirstBookAuthenticationAPI.__module__, service.get("protocol")) eq_("url", service.get("settings").get(ExternalIntegration.URL)) eq_("pass", service.get("settings").get(ExternalIntegration.PASSWORD)) [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_("^(u)", library.get(AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION))
def test_analytics_services_post_edit(self): l1, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) l2, ignore = create( self._db, Library, name="Library 2", short_name="L2", ) ga_service, ignore = create( self._db, ExternalIntegration, protocol=GoogleAnalyticsProvider.__module__, goal=ExternalIntegration.ANALYTICS_GOAL, ) ga_service.url = "oldurl" ga_service.libraries = [l1] with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", ga_service.id), ("name", "some other analytics name"), ("protocol", GoogleAnalyticsProvider.__module__), (ExternalIntegration.URL, "http://test"), ("libraries", json.dumps([{"short_name": "L2", "tracking_id": "l2id"}])), ]) response = self.manager.admin_analytics_services_controller.process_analytics_services() eq_(response.status_code, 200) eq_(ga_service.id, int(response.response[0])) eq_(GoogleAnalyticsProvider.__module__, ga_service.protocol) eq_("http://test", ga_service.url) eq_([l2], ga_service.libraries) eq_("l2id", ConfigurationSetting.for_library_and_externalintegration( self._db, GoogleAnalyticsProvider.TRACKING_ID, l2, ga_service).value)
def test_borrow_with_outstanding_fines(self): # This checkout would succeed... now = datetime.now() loaninfo = LoanInfo( self.pool.collection, self.pool.data_source, self.pool.identifier.type, self.pool.identifier.identifier, now, now + timedelta(seconds=3600), ) self.remote.queue_checkout(loaninfo) # ...except the patron has too many fines. old_fines = self.patron.fines self.patron.fines = 1000 setting = ConfigurationSetting.for_library( Configuration.MAX_OUTSTANDING_FINES, self._default_library ) setting.value = "$0.50" assert_raises(OutstandingFines, self.borrow) # Test the case where any amount of fines are too much. setting.value = "$0" assert_raises(OutstandingFines, self.borrow) # Remove the fine policy, and borrow succeeds. setting.value = None loan, i1, i2 = self.borrow() assert isinstance(loan, Loan) self.patron.fines = old_fines
def test_patron_auth_services_get_with_sip2_auth_service(self): auth_service, ignore = create( self._db, ExternalIntegration, protocol=SIP2AuthenticationProvider.__module__, goal=ExternalIntegration.PATRON_AUTH_GOAL ) auth_service.url = "url" auth_service.setting(SIP2AuthenticationProvider.PORT).value = "1234" auth_service.username = "******" auth_service.password = "******" auth_service.setting(SIP2AuthenticationProvider.LOCATION_CODE).value = "5" auth_service.setting(SIP2AuthenticationProvider.FIELD_SEPARATOR).value = "," auth_service.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, self._default_library, auth_service, ).value = "^(u)" with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services() [service] = response.get("patron_auth_services") eq_(auth_service.id, service.get("id")) eq_(SIP2AuthenticationProvider.__module__, service.get("protocol")) eq_("url", service.get("settings").get(ExternalIntegration.URL)) eq_("1234", service.get("settings").get(SIP2AuthenticationProvider.PORT)) eq_("user", service.get("settings").get(ExternalIntegration.USERNAME)) eq_("pass", service.get("settings").get(ExternalIntegration.PASSWORD)) eq_("5", service.get("settings").get(SIP2AuthenticationProvider.LOCATION_CODE)) eq_(",", service.get("settings").get(SIP2AuthenticationProvider.FIELD_SEPARATOR)) [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_("^(u)", library.get(AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION))
def process_get(self): libraries = [] for library in self._db.query(Library).order_by(Library.name): # Only include libraries this admin has librarian access to. if not flask.request.admin or not flask.request.admin.is_librarian(library): continue settings = dict() for setting in Configuration.LIBRARY_SETTINGS: if setting.get("type") == "list": value = ConfigurationSetting.for_library(setting.get("key"), library).json_value if value and setting.get("format") == "geographic": value = self.get_extra_geographic_information(value) else: value = self.current_value(setting, library) if value: settings[setting.get("key")] = value libraries += [dict( uuid=library.uuid, name=library.name, short_name=library.short_name, settings=settings, )] return dict(libraries=libraries, settings=Configuration.LIBRARY_SETTINGS)
def test_lane_loading(self): # The default setup loads lane IDs properly. gate = COPPAGate(self._default_library, self.integration) eq_(self.lane1.id, gate.yes_lane_id) eq_(self.lane2.id, gate.no_lane_id) # If a lane isn't associated with the right library, the # COPPAGate is misconfigured and cannot be instantiated. library = self._library() self.lane1.library = library self._db.commit() assert_raises_regexp( CannotLoadConfiguration, "Lane .* is for the wrong library", COPPAGate, self._default_library, self.integration ) self.lane1.library_id = self._default_library.id # If the lane ID doesn't correspond to a real lane, the # COPPAGate cannot be instantiated. ConfigurationSetting.for_library_and_externalintegration( self._db, COPPAGate.REQUIREMENT_MET_LANE, self._default_library, self.integration ).value = -100 assert_raises_regexp( CannotLoadConfiguration, "No lane with ID: -100", COPPAGate, self._default_library, self.integration )
def test_analytics_services_post_create(self): library, ignore = create( self._db, Library, name="Library", short_name="L", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "Google analytics name"), ("protocol", GoogleAnalyticsProvider.__module__), (ExternalIntegration.URL, "http://test"), ("libraries", json.dumps([{"short_name": "L", "tracking_id": "trackingid"}])), ]) response = self.manager.admin_analytics_services_controller.process_analytics_services() eq_(response.status_code, 201) service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.ANALYTICS_GOAL) eq_(service.id, int(response.response[0])) eq_(GoogleAnalyticsProvider.__module__, service.protocol) eq_("http://test", service.url) eq_([library], service.libraries) eq_("trackingid", ConfigurationSetting.for_library_and_externalintegration( self._db, GoogleAnalyticsProvider.TRACKING_ID, library, service).value) # Creating a local analytics service doesn't require a URL. with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "local analytics name"), ("protocol", LocalAnalyticsProvider.__module__), ("libraries", json.dumps([{"short_name": "L", "tracking_id": "trackingid"}])), ]) response = self.manager.admin_analytics_services_controller.process_analytics_services() eq_(response.status_code, 201)
def enki_library_id(self, library): """Find the Enki library ID for the given library.""" _db = Session.object_session(library) return ConfigurationSetting.for_library_and_externalintegration( _db, self.ENKI_LIBRARY_ID_KEY, library, self.external_integration(_db) ).value
def opds_catalog(self): url = ConfigurationSetting.sitewide(self._db, Configuration.BASE_URL_KEY).value_or_default(request.url) catalog = dict( id=url, title='Library Simplified Metadata Wrangler', ) catalog['links'] = [ { "rel": "register", "href": "/register", "type": "application/opds+json;profile=https://librarysimplified.org/rel/profile/metadata-service", "title": "Register your OPDS server with this metadata service" }, { "rel": "http://librarysimplified.org/rel/metadata/lookup", "href": "/lookup{?data_source,urn*}", "type": "application/atom+xml;profile=opds-catalog", "title": "Look up metadata about one or more specific items", "templated": "true" }, { "rel": "http://opds-spec.org/sort/new", "href": "/{collection_metadata_identifier}/updates{?data_source}", "type": "application/atom+xml;profile=opds-catalog", "title": "Recent changes to one of your tracked collections.", "templated": "true" }, { "rel": "http://librarysimplified.org/rel/metadata/collection-add", "href": "/{collection_metadata_identifier}/add{?data_source,urn*}", "title": "Add items to one of your tracked collections.", "templated": "true" }, { "rel": "http://librarysimplified.org/rel/metadata/collection-remove", "href": "/{collection_metadata_identifier}/remove{?data_source,urn*}", "title": "Remove items from one of your tracked collections.", "templated": "true" }, { "rel": "http://librarysimplified.org/rel/metadata/collection-metadata-needed", "href": "/{collection_metadata_identifier}/metadata_needed", "title": "Get items in your collection for which the metadata wrangler needs more information to process.", "templated": "true" }, { "rel": "http://librarysimplified.org/rel/metadata/resolve-name", "href": "/canonical-author-name{?urn,display_name}", "type": "text/plain", "title": "Look up an author's canonical sort name", "templated": "true" } ] return make_response( json.dumps(catalog), HTTP_OK, {'Content-Type' : OPDS_2_MEDIA_TYPE } )
def test_collections_post_edit_library_specific_configuration(self): # The collection exists. collection = self._collection(name="Collection 1", protocol=ExternalIntegration.RB_DIGITAL) l1, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", collection.id), ("name", "Collection 1"), ("protocol", ExternalIntegration.RB_DIGITAL), ("external_account_id", "1234"), ("username", "user2"), ("password", "password"), ("url", "http://rb/"), ("libraries", json.dumps([{ "short_name": "L1", "ebook_loan_duration": "14", "audio_loan_duration": "12" }])), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 200) # Additional settings were set on the collection+library. eq_( "14", ConfigurationSetting.for_library_and_externalintegration( self._db, "ebook_loan_duration", l1, collection.external_integration).value) eq_( "12", ConfigurationSetting.for_library_and_externalintegration( self._db, "audio_loan_duration", l1, collection.external_integration).value) # Remove the connection between collection and library. with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", collection.id), ("name", "Collection 1"), ("protocol", ExternalIntegration.RB_DIGITAL), ("external_account_id", "1234"), ("username", "user2"), ("password", "password"), ("url", "http://rb/"), ("libraries", json.dumps([])), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 200) eq_(collection.id, int(response.response[0])) # The settings associated with the collection+library were removed # when the connection between collection and library was deleted. eq_( None, ConfigurationSetting.for_library_and_externalintegration( self._db, "ebook_loan_duration", l1, collection.external_integration).value) eq_( None, ConfigurationSetting.for_library_and_externalintegration( self._db, "audio_loan_duration", l1, collection.external_integration).value) eq_([], collection.libraries)
from controller import setup_admin_controllers from templates import ( admin_sign_in_again as sign_in_again_template, ) from api.routes import ( has_library, library_route, ) import csv, codecs, cStringIO from StringIO import StringIO import urllib from datetime import timedelta # The secret key is used for signing cookies for admin login app.secret_key = ConfigurationSetting.sitewide_secret(_db, Configuration.SECRET_KEY) # An admin's session will expire after this amount of time and # the admin will have to log in again. app.permanent_session_lifetime = timedelta(hours=9) @app.before_first_request def setup_admin(): if getattr(app, 'manager', None) is not None: setup_admin_controllers(app.manager) def allows_admin_auth_setup(f): @wraps(f) def decorated(*args, **kwargs):
def push(self, stage, url_for, catalog_url=None, do_get=HTTP.debuggable_get, do_post=HTTP.debuggable_post): """Attempt to register a library with a RemoteRegistry. NOTE: This method is designed to be used in a controller. Other callers may use this method, but they must be able to render a ProblemDetail when there's a failure. NOTE: The application server must be running when this method is called, because part of the OPDS Directory Registration Protocol is the remote server retrieving the library's Authentication For OPDS document. :param stage: Either TESTING_STAGE or PRODUCTION_STAGE :param url_for: Flask url_for() or equivalent, used to generate URLs for the application server. :param do_get: Mockable method to make a GET request. :param do_post: Mockable method to make a POST request. :return: A ProblemDetail if there was a problem; otherwise True. """ # Assume that the registration will fail. # # TODO: If a registration has previously succeeded, failure to # re-register probably means a maintenance of the status quo, # not a change of success to failure. But we don't have any way # of being sure. self.status_field.value = self.FAILURE_STATUS if stage not in self.VALID_REGISTRATION_STAGES: return INVALID_INPUT.detailed( _("%r is not a valid registration stage") % stage) # Verify that a public/private key pair exists for this library. # This key pair is created during initialization of the # LibraryAuthenticator, so this should always be present. # # We can't just create the key pair here because the process # of pushing a registration involves the other site making a # request to the circulation manager. This means the key pair # needs to be committed to the database _before_ the push # attempt starts. key_pair = ConfigurationSetting.for_library(Configuration.KEY_PAIR, self.library).json_value if not key_pair: # TODO: We could create the key pair _here_. The database # session will be committed at the end of this request, # so the push attempt would succeed if repeated. return SHARED_SECRET_DECRYPTION_ERROR.detailed( _("Library %(library)s has no key pair set.", library=self.library.short_name)) public_key, private_key = key_pair cipher = Configuration.cipher(private_key) # Before we can start the registration protocol, we must fetch # the remote catalog's URL and extract the link to the # registration resource that kicks off the protocol. result = self.registry.fetch_catalog(catalog_url, do_get) if isinstance(result, ProblemDetail): return result register_url, vendor_id = result # Store the vendor id as a ConfigurationSetting on the integration # -- it'll be the same value for all libraries. if vendor_id: ConfigurationSetting.for_externalintegration( AuthdataUtility.VENDOR_ID_KEY, self.integration).value = vendor_id # Build the document we'll be sending to the registration URL. payload = self._create_registration_payload(url_for, stage) if isinstance(payload, ProblemDetail): return payload headers = self._create_registration_headers() if isinstance(headers, ProblemDetail): return headers # Send the document. response = self._send_registration_request(register_url, headers, payload, do_post) if isinstance(response, ProblemDetail): return response catalog = json.loads(response.content) # Process the result. return self._process_registration_result(catalog, cipher, stage)
def from_config(cls, library, _db=None): """Initialize an AuthdataUtility from site configuration. :return: An AuthdataUtility if one is configured; otherwise None. :raise CannotLoadConfiguration: If an AuthdataUtility is incompletely configured. """ _db = _db or Session.object_session(library) if not _db: raise ValueError( "No database connection provided and could not derive one from Library object!" ) # Use a version of the library library = _db.merge(library, load=False) # Try to find an external integration with a configured Vendor ID. integrations = _db.query( ExternalIntegration ).outerjoin( ExternalIntegration.libraries ).filter( ExternalIntegration.protocol==ExternalIntegration.OPDS_REGISTRATION, ExternalIntegration.goal==ExternalIntegration.DISCOVERY_GOAL, Library.id==library.id ) integration = None for possible_integration in integrations: vendor_id = ConfigurationSetting.for_externalintegration( cls.VENDOR_ID_KEY, possible_integration).value if vendor_id: integration = possible_integration break library_uri = ConfigurationSetting.for_library( Configuration.WEBSITE_URL, library).value if not integration: return None vendor_id = integration.setting(cls.VENDOR_ID_KEY).value library_short_name = ConfigurationSetting.for_library_and_externalintegration( _db, ExternalIntegration.USERNAME, library, integration ).value secret = ConfigurationSetting.for_library_and_externalintegration( _db, ExternalIntegration.PASSWORD, library, integration ).value other_libraries = None adobe_integration = ExternalIntegration.lookup( _db, ExternalIntegration.ADOBE_VENDOR_ID, ExternalIntegration.DRM_GOAL, library=library ) if adobe_integration: other_libraries = adobe_integration.setting(cls.OTHER_LIBRARIES_KEY).json_value other_libraries = other_libraries or dict() if (not vendor_id or not library_uri or not library_short_name or not secret ): raise CannotLoadConfiguration( "Short Client Token configuration is incomplete. " "vendor_id, username, password and " "Library website_url must all be defined.") if '|' in library_short_name: raise CannotLoadConfiguration( "Library short name cannot contain the pipe character." ) return cls(vendor_id, library_uri, library_short_name, secret, other_libraries)
def test_from_config(self): library = self._default_library library2 = self._library() self.initialize_adobe(library, [library2]) library_url = library.setting(Configuration.WEBSITE_URL).value library2_url = library2.setting(Configuration.WEBSITE_URL).value utility = AuthdataUtility.from_config(library) registry = ExternalIntegration.lookup( self._db, ExternalIntegration.OPDS_REGISTRATION, ExternalIntegration.DISCOVERY_GOAL, library=library) eq_( library.short_name + "token", ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.USERNAME, library, registry).value) eq_( library.short_name + " token secret", ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.PASSWORD, library, registry).value) eq_(self.TEST_VENDOR_ID, utility.vendor_id) eq_(library_url, utility.library_uri) eq_( { library2_url: "%s token secret" % library2.short_name, library_url: "%s token secret" % library.short_name }, utility.secrets_by_library_uri) eq_( { "%sTOKEN" % library.short_name.upper(): library_url, "%sTOKEN" % library2.short_name.upper(): library2_url }, utility.library_uris_by_short_name) # If the Library object is disconnected from its database # session, as may happen in production... self._db.expunge(library) # Then an attempt to use it to get an AuthdataUtility # will fail... assert_raises_regexp( ValueError, "No database connection provided and could not derive one from Library object!", AuthdataUtility.from_config, library) # ...unless a database session is provided in the constructor. authdata = AuthdataUtility.from_config(library, self._db) eq_( { "%sTOKEN" % library.short_name.upper(): library_url, "%sTOKEN" % library2.short_name.upper(): library2_url }, authdata.library_uris_by_short_name) library = self._db.merge(library) self._db.commit() # If an integration is set up but incomplete, from_config # raises CannotLoadConfiguration. setting = ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.USERNAME, library, registry) old_short_name = setting.value setting.value = None assert_raises(CannotLoadConfiguration, AuthdataUtility.from_config, library) setting.value = old_short_name setting = library.setting(Configuration.WEBSITE_URL) old_value = setting.value setting.value = None assert_raises(CannotLoadConfiguration, AuthdataUtility.from_config, library) setting.value = old_value setting = ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.PASSWORD, library, registry) old_secret = setting.value setting.value = None assert_raises(CannotLoadConfiguration, AuthdataUtility.from_config, library) setting.value = old_secret # If other libraries are not configured, that's fine. We'll # only have a configuration for ourselves. self.adobe_vendor_id.set_setting(AuthdataUtility.OTHER_LIBRARIES_KEY, None) authdata = AuthdataUtility.from_config(library) eq_({library_url: "%s token secret" % library.short_name}, authdata.secrets_by_library_uri) eq_({"%sTOKEN" % library.short_name.upper(): library_url}, authdata.library_uris_by_short_name) # Short library names are case-insensitive. If the # configuration has the same library short name twice, you # can't create an AuthdataUtility. self.adobe_vendor_id.set_setting( AuthdataUtility.OTHER_LIBRARIES_KEY, json.dumps({ "http://a/": ("a", "secret1"), "http://b/": ("A", "secret2"), })) assert_raises(ValueError, AuthdataUtility.from_config, library) # If there is no Adobe Vendor ID integration set up, # from_config() returns None. self._db.delete(registry) eq_(None, AuthdataUtility.from_config(library))
def value(self, key, integration): _db = Session.object_session(integration) return ConfigurationSetting.for_library_and_externalintegration( _db, key, self.library, integration).value
def __init__(self, library): super(LibraryAnnotator, self).__init__() self.library = library _db = Session.object_session(library) self.base_url = ConfigurationSetting.sitewide( _db, Configuration.BASE_URL_KEY).value
def process_delete(self, key): self.require_system_admin() setting = ConfigurationSetting.sitewide(self._db, key) setting.value = None return Response(str(_("Deleted")), 200)
@returns_problem_detail def remove(collection_metadata_identifier): return CatalogController(Conf.db).remove_items( collection_details=collection_metadata_identifier ) @app.route('/client/update_url', methods=['POST']) @requires_auth @returns_problem_detail def update_url(): return CatalogController(Conf.db).update_client_url() if __name__ == '__main__': debug = True url = ConfigurationSetting.sitewide(Conf.db, Configuration.BASE_URL_KEY).value scheme, netloc, path, parameters, query, fragment = urlparse.urlparse(url) if ':' in netloc: host, port = netloc.split(':') port = int(port) else: host = netloc port = 80 # Workaround for a "Resource temporarily unavailable" error when # running in debug mode with the global socket timeout set by isbnlib if debug: import socket socket.setdefaulttimeout(None) Conf.log.info("Starting app on %s:%s", host, port)
def val(x): return ConfigurationSetting.for_library(x, library).value
def test_register(self): # An auth document URL is required to register. assert_raises(InvalidInputException, self.shared_collection.register, self.collection, None) # If the url doesn't return a valid auth document, there's an exception. auth_response = "not json" def do_get(*args, **kwargs): return MockRequestsResponse(200, content=auth_response) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # The auth document also must have a link to the library's catalog. auth_response = json.dumps({"links": []}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If no external library URLs are configured, no one can register. auth_response = json.dumps( {"links": [{ "href": "http://library.org", "rel": "start" }]}) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = None assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # If the library's URL isn't in the configuration, it can't register. auth_response = json.dumps({ "links": [{ "href": "http://differentlibrary.org", "rel": "start" }] }) ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, self.collection.external_integration).value = json.dumps( ["http://library.org"]) assert_raises(AuthorizationFailedException, self.shared_collection.register, self.collection, "http://differentlibrary.org/auth", do_get=do_get) # Or if the public key is missing from the auth document. auth_response = json.dumps( {"links": [{ "href": "http://library.org", "rel": "start" }]}) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({ "public_key": { "type": "not RSA", "value": "123" }, "links": [{ "href": "http://library.org", "rel": "start" }] }) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) auth_response = json.dumps({ "public_key": { "type": "RSA" }, "links": [{ "href": "http://library.org", "rel": "start" }] }) assert_raises(RemoteInitiatedServerError, self.shared_collection.register, self.collection, "http://library.org/auth", do_get=do_get) # Here's an auth document with a valid key. key = RSA.generate(2048) public_key = key.publickey().exportKey() encryptor = PKCS1_OAEP.new(key) auth_response = json.dumps({ "public_key": { "type": "RSA", "value": public_key }, "links": [{ "href": "http://library.org", "rel": "start" }] }) response = self.shared_collection.register(self.collection, "http://library.org/auth", do_get=do_get) # An IntegrationClient has been created. client = get_one( self._db, IntegrationClient, url=IntegrationClient.normalize_url("http://library.org/")) decrypted_secret = encryptor.decrypt( base64.b64decode( response.get("metadata", {}).get("shared_secret"))) eq_(client.shared_secret, decrypted_secret)
def register(self, collection, auth_document_url, do_get=HTTP.get_with_timeout): """Register a library on an external circulation manager for access to this collection. The library's auth document url must be whitelisted in the collection's settings.""" if not auth_document_url: raise InvalidInputException( _("An authentication document URL is required to register a library.") ) auth_response = do_get(auth_document_url, allowed_response_codes=["2xx", "3xx"]) try: auth_document = json.loads(auth_response.content) except ValueError as e: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s was not valid JSON.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) links = auth_document.get("links") start_url = None for link in links: if link.get("rel") == "start": start_url = link.get("href") break if not start_url: raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain a start link.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration, ).json_value if not external_library_urls or start_url not in external_library_urls: raise AuthorizationFailedException( _( "Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url, ) ) public_key = auth_document.get("public_key") if ( not public_key or not public_key.get("type") == "RSA" or not public_key.get("value") ): raise RemoteInitiatedServerError( _( "Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url, ), _("Remote authentication document"), ) public_key = public_key.get("value") encryptor = Configuration.cipher(public_key) normalized_url = IntegrationClient.normalize_url(start_url) client = get_one(self._db, IntegrationClient, url=normalized_url) if not client: client, ignore = IntegrationClient.register(self._db, start_url) shared_secret = client.shared_secret.encode("utf-8") encrypted_secret = encryptor.encrypt(shared_secret) return dict(metadata=dict(shared_secret=base64.b64encode(encrypted_secret)))
def initialize_adobe(self, vendor_id_library, short_token_libraries=[]): """Initialize an Adobe Vendor ID integration and a Short Client Token integration with a number of libraries. :param vendor_id_library: The Library that should have an Adobe Vendor ID integration. :param short_token_libraries: The Libraries that should have a Short Client Token integration. """ short_token_libraries = list(short_token_libraries) if not vendor_id_library in short_token_libraries: short_token_libraries.append(vendor_id_library) # The first library acts as an Adobe Vendor ID server. self.adobe_vendor_id = self._external_integration( ExternalIntegration.ADOBE_VENDOR_ID, ExternalIntegration.DRM_GOAL, username=self.TEST_VENDOR_ID, libraries=[vendor_id_library], ) # The other libraries will share a registry integration. self.registry = self._external_integration( ExternalIntegration.OPDS_REGISTRATION, ExternalIntegration.DISCOVERY_GOAL, libraries=short_token_libraries, ) # The integration knows which Adobe Vendor ID server it # gets its Adobe IDs from. self.registry.set_setting(AuthdataUtility.VENDOR_ID_KEY, self.adobe_vendor_id.username) # As we give libraries their Short Client Token settings, # we build the 'other_libraries' setting we'll apply to the # Adobe Vendor ID integration. other_libraries = dict() # Every library in the system can generate Short Client # Tokens. for library in short_token_libraries: # Each library will get a slightly different short # name and secret for generating Short Client Tokens. library_uri = self._url short_name = library.short_name + "token" secret = library.short_name + " token secret" ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.USERNAME, library, self.registry).value = short_name ConfigurationSetting.for_library_and_externalintegration( self._db, ExternalIntegration.PASSWORD, library, self.registry).value = secret ConfigurationSetting.for_library_and_externalintegration( self._db, RegistrationConstants.LIBRARY_REGISTRATION_STATUS, library, self.registry, ).value = RegistrationConstants.SUCCESS_STATUS library.setting(Configuration.WEBSITE_URL).value = library_uri # Each library's Short Client Token configuration will be registered # with that Adobe Vendor ID server. if library != vendor_id_library: other_libraries[library_uri] = (short_name, secret) # Tell the Adobe Vendor ID server about the other libraries. other_libraries = json.dumps(other_libraries) self.adobe_vendor_id.set_setting(AuthdataUtility.OTHER_LIBRARIES_KEY, other_libraries)
def test_collections_post_create(self): l1, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) l2, ignore = create( self._db, Library, name="Library 2", short_name="L2", ) l3, ignore = create( self._db, Library, name="Library 3", short_name="L3", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "New Collection"), ("protocol", "Overdrive"), ("libraries", json.dumps([{ "short_name": "L1", "ils_name": "l1_ils" }, { "short_name": "L2", "ils_name": "l2_ils" }])), ("external_account_id", "acctid"), ("username", "username"), ("password", "password"), ("website_id", "1234"), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 201) # The collection was created and configured properly. collection = get_one(self._db, Collection, name="New Collection") eq_(collection.id, int(response.response[0])) eq_("New Collection", collection.name) eq_("acctid", collection.external_account_id) eq_("username", collection.external_integration.username) eq_("password", collection.external_integration.password) # Two libraries now have access to the collection. eq_([collection], l1.collections) eq_([collection], l2.collections) eq_([], l3.collections) # Additional settings were set on the collection. setting = collection.external_integration.setting("website_id") eq_("website_id", setting.key) eq_("1234", setting.value) eq_( "l1_ils", ConfigurationSetting.for_library_and_externalintegration( self._db, "ils_name", l1, collection.external_integration).value) eq_( "l2_ils", ConfigurationSetting.for_library_and_externalintegration( self._db, "ils_name", l2, collection.external_integration).value) # This collection will be a child of the first collection. with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "Child Collection"), ("protocol", "Overdrive"), ("parent_id", collection.id), ("libraries", json.dumps([{ "short_name": "L3", "ils_name": "l3_ils" }])), ("external_account_id", "child-acctid"), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 201) # The collection was created and configured properly. child = get_one(self._db, Collection, name="Child Collection") eq_(child.id, int(response.response[0])) eq_("Child Collection", child.name) eq_("child-acctid", child.external_account_id) # The settings that are inherited from the parent weren't set. eq_(None, child.external_integration.username) eq_(None, child.external_integration.password) setting = child.external_integration.setting("website_id") eq_(None, setting.value) # One library has access to the collection. eq_([child], l3.collections) eq_( "l3_ils", ConfigurationSetting.for_library_and_externalintegration( self._db, "ils_name", l3, child.external_integration).value)
def decorated(*args, **kwargs): patron = app.manager.index_controller.authenticated_patron_from_request( ) if isinstance(patron, ProblemDetail): return patron.response elif isinstance(patron, Response): return patron else: return f(*args, **kwargs) return decorated patron_web_url = None if (hasattr(app, 'manager') and hasattr(app.manager, '_db')): patron_web_url = ConfigurationSetting.sitewide( app.manager._db, Configuration.PATRON_WEB_CLIENT_URL).value if patron_web_url: # The allows_patron_web decorator will add Cross-Origin Resource Sharing # (CORS) headers to routes that will be used by the patron web interface. # This is necessary for a JS app on a different domain to make requests. # # The partial function sets the arguments to the cross_origin decorator, # since they're the same for all routes that use it. allows_patron_web = partial( cross_origin, origins=[patron_web_url], supports_credentials=True, ) else: # If the patron web client isn't configured, the decorator will do nothing.
Configuration.load() if not Configuration.instance: # No need to import configuration if there isn't any. sys.exit() _db = production_session() try: integrations = [] auth_conf = Configuration.policy('authentication') if not auth_conf: sys.exit() bearer_token_signing_secret = auth_conf.get('bearer_token_signing_secret') secret_setting = ConfigurationSetting.sitewide( _db, OAuthAuthenticationProvider.BEARER_TOKEN_SIGNING_SECRET) if bearer_token_signing_secret: secret_setting.value = bearer_token_signing_secret for provider in auth_conf.get('providers'): integration = make_patron_auth_integration(_db, provider) module = provider.get('module') if module == 'api.millenium_patron': convert_millenium(_db, integration, provider) elif module == 'api.firstbook': convert_firstbook(_db, integration, provider) elif module == 'api.clever': convert_clever(_db, integration, provider) elif module == 'api.sip': convert_sip(_db, integration, provider) else:
from core.model import ( Library, ExternalIntegration, ConfigurationSetting, production_session, ) from api.authenticator import AuthenticationProvider try: _db = production_session() for library in _db.query(Library): for integration in library.integrations: if integration.goal == ExternalIntegration.PATRON_AUTH_GOAL: # Get old patron restriction. patron_restriction = ConfigurationSetting.for_library_and_externalintegration( _db, 'patron_identifier_restriction', library, integration) # Get new settings. library_identifier_field = ConfigurationSetting.for_library_and_externalintegration( _db, AuthenticationProvider.LIBRARY_IDENTIFIER_FIELD, library, integration) library_identifier_restriction_type = ConfigurationSetting.for_library_and_externalintegration( _db, AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE, library, integration) library_identifier_restriction = ConfigurationSetting.for_library_and_externalintegration( _db, AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION, library, integration) # Set new settings. if not patron_restriction.value:
def test_analytics_services_get_with_one_service(self): # Delete the local analytics service that gets created by default. local_analytics_default = get_one( self._db, ExternalIntegration, protocol=LocalAnalyticsProvider.__module__) self._db.delete(local_analytics_default) ga_service, ignore = create( self._db, ExternalIntegration, protocol=GoogleAnalyticsProvider.__module__, goal=ExternalIntegration.ANALYTICS_GOAL, ) ga_service.url = self._str with self.request_context_with_admin("/"): response = (self.manager.admin_analytics_services_controller. process_analytics_services()) [service] = response.get("analytics_services") assert ga_service.id == service.get("id") assert ga_service.protocol == service.get("protocol") assert ga_service.url == service.get("settings").get( ExternalIntegration.URL) ga_service.libraries += [self._default_library] ConfigurationSetting.for_library_and_externalintegration( self._db, GoogleAnalyticsProvider.TRACKING_ID, self._default_library, ga_service, ).value = "trackingid" with self.request_context_with_admin("/"): response = (self.manager.admin_analytics_services_controller. process_analytics_services()) [service] = response.get("analytics_services") [library] = service.get("libraries") assert self._default_library.short_name == library.get( "short_name") assert "trackingid" == library.get( GoogleAnalyticsProvider.TRACKING_ID) self._db.delete(ga_service) local_service, ignore = create( self._db, ExternalIntegration, protocol=LocalAnalyticsProvider.__module__, goal=ExternalIntegration.ANALYTICS_GOAL, ) local_service.libraries += [self._default_library] with self.request_context_with_admin("/"): response = (self.manager.admin_analytics_services_controller. process_analytics_services()) [local_analytics] = response.get("analytics_services") assert local_service.id == local_analytics.get("id") assert local_service.protocol == local_analytics.get("protocol") assert local_analytics.get( "protocol") == LocalAnalyticsProvider.__module__ [library] = local_analytics.get("libraries") assert self._default_library.short_name == library.get( "short_name")
def test_analytics_services_post_create(self): library, ignore = create( self._db, Library, name="Library", short_name="L", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "Google analytics name"), ("protocol", GoogleAnalyticsProvider.__module__), (ExternalIntegration.URL, "http://test"), ( "libraries", json.dumps([{ "short_name": "L", "tracking_id": "trackingid" }]), ), ]) response = (self.manager.admin_analytics_services_controller. process_analytics_services()) assert response.status_code == 201 service = get_one( self._db, ExternalIntegration, goal=ExternalIntegration.ANALYTICS_GOAL, protocol=GoogleAnalyticsProvider.__module__, ) assert service.id == int(response.get_data()) assert GoogleAnalyticsProvider.__module__ == service.protocol assert "http://test" == service.url assert [library] == service.libraries assert ("trackingid" == ConfigurationSetting.for_library_and_externalintegration( self._db, GoogleAnalyticsProvider.TRACKING_ID, library, service).value) local_analytics_default = get_one( self._db, ExternalIntegration, goal=ExternalIntegration.ANALYTICS_GOAL, protocol=LocalAnalyticsProvider.__module__, ) self._db.delete(local_analytics_default) # Creating a local analytics service doesn't require a URL. with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "local analytics name"), ("protocol", LocalAnalyticsProvider.__module__), ( "libraries", json.dumps([{ "short_name": "L", "tracking_id": "trackingid" }]), ), ]) response = (self.manager.admin_analytics_services_controller. process_analytics_services()) assert response.status_code == 201
def current_value(self, setting, library): return ConfigurationSetting.for_library(setting['key'], library).value
links = auth_document.get("links") start_url = None for link in links: if link.get("rel") == "start": start_url = link.get("href") break if not start_url: raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain a start link.", auth_document_url=auth_document_url), _("Remote authentication document")) external_library_urls = ConfigurationSetting.for_externalintegration( BaseSharedCollectionAPI.EXTERNAL_LIBRARY_URLS, collection.external_integration).json_value if not external_library_urls or start_url not in external_library_urls: raise AuthorizationFailedException( _("Your library's URL is not one of the allowed URLs for this collection. Ask the collection administrator to add %(library_url)s to the list of allowed URLs.", library_url=start_url)) public_key = auth_document.get("public_key") if not public_key or not public_key.get( "type") == "RSA" or not public_key.get("value"): raise RemoteInitiatedServerError( _("Authentication document at %(auth_document_url)s did not contain an RSA public key.", auth_document_url=auth_document_url), _("Remote authentication document"))
def test_patron_auth_services_post_create(self): mock_controller = self._get_mock() library, ignore = create( self._db, Library, name="Library", short_name="L", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("protocol", SimpleAuthenticationProvider.__module__), ( "libraries", json.dumps([{ "short_name": library.short_name, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION: "^(.)", AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE: AuthenticationProvider. LIBRARY_IDENTIFIER_RESTRICTION_TYPE_REGEX, AuthenticationProvider.LIBRARY_IDENTIFIER_FIELD: AuthenticationProvider. LIBRARY_IDENTIFIER_RESTRICTION_BARCODE, AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION: "^1234", }]), ), ] + self._common_basic_auth_arguments()) response = mock_controller.process_patron_auth_services() assert response.status_code == 201 assert mock_controller.validate_formats_call_count == 1 auth_service = get_one(self._db, ExternalIntegration, goal=ExternalIntegration.PATRON_AUTH_GOAL) assert auth_service.id == int(response.response[0]) assert SimpleAuthenticationProvider.__module__ == auth_service.protocol assert ("user" == auth_service.setting( BasicAuthenticationProvider.TEST_IDENTIFIER).value) assert ("pass" == auth_service.setting( BasicAuthenticationProvider.TEST_PASSWORD).value) assert [library] == auth_service.libraries assert ( "^(.)" == ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, library, auth_service, ).value) common_args = self._common_basic_auth_arguments() with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("protocol", MilleniumPatronAPI.__module__), (ExternalIntegration.URL, "url"), (MilleniumPatronAPI.VERIFY_CERTIFICATE, "true"), ( MilleniumPatronAPI.AUTHENTICATION_MODE, MilleniumPatronAPI.PIN_AUTHENTICATION_MODE, ), ] + common_args) response = mock_controller.process_patron_auth_services() assert response.status_code == 201 assert mock_controller.validate_formats_call_count == 2 auth_service2 = get_one( self._db, ExternalIntegration, goal=ExternalIntegration.PATRON_AUTH_GOAL, protocol=MilleniumPatronAPI.__module__, ) assert auth_service2 != auth_service assert auth_service2.id == int(response.response[0]) assert "url" == auth_service2.url assert ("user" == auth_service2.setting( BasicAuthenticationProvider.TEST_IDENTIFIER).value) assert ("pass" == auth_service2.setting( BasicAuthenticationProvider.TEST_PASSWORD).value) assert ("true" == auth_service2.setting( MilleniumPatronAPI.VERIFY_CERTIFICATE).value) assert (MilleniumPatronAPI.PIN_AUTHENTICATION_MODE == auth_service2.setting( MilleniumPatronAPI.AUTHENTICATION_MODE).value) assert None == auth_service2.setting( MilleniumPatronAPI.BLOCK_TYPES).value assert [] == auth_service2.libraries
def test_collections_post_edit(self): # The collection exists. collection = self._collection(name="Collection 1", protocol=ExternalIntegration.OVERDRIVE) l1, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", collection.id), ("name", "Collection 1"), ("protocol", ExternalIntegration.OVERDRIVE), ("external_account_id", "1234"), ("username", "user2"), ("password", "password"), ("website_id", "1234"), ("libraries", json.dumps([{ "short_name": "L1", "ils_name": "the_ils" }])), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 200) eq_(collection.id, int(response.response[0])) # The collection has been changed. eq_("user2", collection.external_integration.username) # A library now has access to the collection. eq_([collection], l1.collections) # Additional settings were set on the collection. setting = collection.external_integration.setting("website_id") eq_("website_id", setting.key) eq_("1234", setting.value) eq_( "the_ils", ConfigurationSetting.for_library_and_externalintegration( self._db, "ils_name", l1, collection.external_integration).value) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", collection.id), ("name", "Collection 1"), ("protocol", ExternalIntegration.OVERDRIVE), ("external_account_id", "1234"), ("username", "user2"), ("password", "password"), ("website_id", "1234"), ("libraries", json.dumps([])), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 200) eq_(collection.id, int(response.response[0])) # The collection is the same. eq_("user2", collection.external_integration.username) eq_(ExternalIntegration.OVERDRIVE, collection.protocol) # But the library has been removed. eq_([], l1.collections) eq_( None, ConfigurationSetting.for_library_and_externalintegration( self._db, "ils_name", l1, collection.external_integration).value) parent = self._collection(name="Parent", protocol=ExternalIntegration.OVERDRIVE) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", collection.id), ("name", "Collection 1"), ("protocol", ExternalIntegration.OVERDRIVE), ("parent_id", parent.id), ("external_account_id", "1234"), ("libraries", json.dumps([])), ]) response = self.manager.admin_collection_settings_controller.process_collections( ) eq_(response.status_code, 200) eq_(collection.id, int(response.response[0])) # The collection now has a parent. eq_(parent, collection.parent)
def test_patron_auth_services_post_edit(self): mock_controller = self._get_mock() l1, ignore = create( self._db, Library, name="Library 1", short_name="L1", ) l2, ignore = create( self._db, Library, name="Library 2", short_name="L2", ) auth_service, ignore = create( self._db, ExternalIntegration, protocol=SimpleAuthenticationProvider.__module__, goal=ExternalIntegration.PATRON_AUTH_GOAL, ) auth_service.setting( BasicAuthenticationProvider.TEST_IDENTIFIER).value = "old_user" auth_service.setting( BasicAuthenticationProvider.TEST_PASSWORD).value = "old_password" auth_service.libraries = [l1] with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("id", auth_service.id), ("protocol", SimpleAuthenticationProvider.__module__), ( "libraries", json.dumps([{ "short_name": l2.short_name, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION: "^(.)", AuthenticationProvider.LIBRARY_IDENTIFIER_RESTRICTION_TYPE: AuthenticationProvider. LIBRARY_IDENTIFIER_RESTRICTION_TYPE_NONE, AuthenticationProvider.LIBRARY_IDENTIFIER_FIELD: AuthenticationProvider. LIBRARY_IDENTIFIER_RESTRICTION_BARCODE, }]), ), ] + self._common_basic_auth_arguments()) response = (self.manager.admin_patron_auth_services_controller. process_patron_auth_services()) assert response.status_code == 200 assert mock_controller.validate_formats_call_count == 1 assert auth_service.id == int(response.response[0]) assert SimpleAuthenticationProvider.__module__ == auth_service.protocol assert ("user" == auth_service.setting( BasicAuthenticationProvider.TEST_IDENTIFIER).value) assert ("pass" == auth_service.setting( BasicAuthenticationProvider.TEST_PASSWORD).value) assert [l2] == auth_service.libraries assert ( "^(.)" == ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, l2, auth_service, ).value)
def max_outstanding_fines(cls, library): max_fines = ConfigurationSetting.for_library( cls.MAX_OUTSTANDING_FINES, library ).value return MoneyUtility.parse(max_fines)
def test_libraries_post_create(self): class TestFileUpload(StringIO): headers = {"Content-Type": "image/png"} image_data = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x06PLTE\xffM\x00\x01\x01\x01\x8e\x1e\xe5\x1b\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82' original_validate = GeographicValidator().validate_geographic_areas class MockValidator(GeographicValidator): def __init__(self): self.was_called = False def validate_geographic_areas(self, values, db): self.was_called = True return original_validate(values, db) with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("name", "The New York Public Library"), ("short_name", "nypl"), ("library_description", "Short description of library"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.TINY_COLLECTION_LANGUAGES, ['ger']), (Configuration.LIBRARY_SERVICE_AREA, ['06759', 'everywhere', 'MD', 'Boston, MA']), (Configuration.LIBRARY_FOCUS_AREA, ['Manitoba', 'Broward County, FL', 'QC']), (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**"), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.FEATURED_LANE_SIZE, "5"), (Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_RANDOM), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_TITLE, ''), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, ''), ]) flask.request.files = MultiDict([ (Configuration.LOGO, TestFileUpload(image_data)), ]) validator = MockValidator() response = self.manager.admin_library_settings_controller.process_post( validator) eq_(response.status_code, 201) library = get_one(self._db, Library, short_name="nypl") eq_(library.uuid, response.response[0]) eq_(library.name, "The New York Public Library") eq_(library.short_name, "nypl") eq_( "5", ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, library).value) eq_( FacetConstants.ORDER_RANDOM, ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_( json.dumps( [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]), ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value) eq_( "data:image/png;base64,%s" % base64.b64encode(image_data), ConfigurationSetting.for_library(Configuration.LOGO, library).value) eq_(validator.was_called, True) eq_( '{"CA": [], "US": ["06759", "everywhere", "MD", "Boston, MA"]}', ConfigurationSetting.for_library( Configuration.LIBRARY_SERVICE_AREA, library).value) eq_( '{"CA": ["Manitoba", "Quebec"], "US": ["Broward County, FL"]}', ConfigurationSetting.for_library(Configuration.LIBRARY_FOCUS_AREA, library).value) # When the library was created, default lanes were also created # according to its language setup. This library has one tiny # collection (not a good choice for a real library), so only # two lanes were created: "Other Languages" and then "German" # underneath it. [german, other_languages] = sorted(library.lanes, key=lambda x: x.display_name) eq_(None, other_languages.parent) eq_(['ger'], other_languages.languages) eq_(other_languages, german.parent) eq_(['ger'], german.languages)
def test_collections_get_collections_with_multiple_collections(self): old_prior_test_results = HasSelfTests.prior_test_results HasSelfTests.prior_test_results = self.mock_prior_test_results [c1] = self._default_library.collections c2 = self._collection( name="Collection 2", protocol=ExternalIntegration.OVERDRIVE, ) c2_storage = self._external_integration( protocol=ExternalIntegration.S3, goal=ExternalIntegration.STORAGE_GOAL ) c2_external_integration_link = self._external_integration_link( integration=c2.external_integration, other_integration=c2_storage, purpose=ExternalIntegrationLink.COVERS, ) c2.external_account_id = "1234" c2.external_integration.password = "******" c2.external_integration.username = "******" c2.external_integration.setting("website_id").value = "100" c3 = self._collection( name="Collection 3", protocol=ExternalIntegration.OVERDRIVE, ) c3.external_account_id = "5678" c3.parent = c2 l1 = self._library(short_name="L1") c3.libraries += [l1, self._default_library] c3.external_integration.libraries += [l1] ConfigurationSetting.for_library_and_externalintegration( self._db, "ebook_loan_duration", l1, c3.external_integration ).value = "14" l1_librarian, ignore = create(self._db, Admin, email="*****@*****.**") l1_librarian.add_role(AdminRole.LIBRARIAN, l1) with self.request_context_with_admin("/"): controller = self.manager.admin_collection_settings_controller response = controller.process_collections() # The system admin can see all collections. coll2, coll3, coll1 = sorted( response.get("collections"), key=lambda c: c.get("name") ) assert c1.id == coll1.get("id") assert c2.id == coll2.get("id") assert c3.id == coll3.get("id") assert c1.name == coll1.get("name") assert c2.name == coll2.get("name") assert c3.name == coll3.get("name") assert c1.protocol == coll1.get("protocol") assert c2.protocol == coll2.get("protocol") assert c3.protocol == coll3.get("protocol") assert self.self_test_results == coll1.get("self_test_results") assert self.self_test_results == coll2.get("self_test_results") assert self.self_test_results == coll3.get("self_test_results") settings1 = coll1.get("settings", {}) settings2 = coll2.get("settings", {}) settings3 = coll3.get("settings", {}) assert controller.NO_MIRROR_INTEGRATION == settings1.get( "covers_mirror_integration_id" ) assert controller.NO_MIRROR_INTEGRATION == settings1.get( "books_mirror_integration_id" ) # Only added an integration for S3 storage for covers. assert str(c2_storage.id) == settings2.get("covers_mirror_integration_id") assert controller.NO_MIRROR_INTEGRATION == settings2.get( "books_mirror_integration_id" ) assert controller.NO_MIRROR_INTEGRATION == settings3.get( "covers_mirror_integration_id" ) assert controller.NO_MIRROR_INTEGRATION == settings3.get( "books_mirror_integration_id" ) assert c1.external_account_id == settings1.get("external_account_id") assert c2.external_account_id == settings2.get("external_account_id") assert c3.external_account_id == settings3.get("external_account_id") assert c1.external_integration.password == settings1.get("password") assert c2.external_integration.password == settings2.get("password") assert c2.id == coll3.get("parent_id") coll3_libraries = coll3.get("libraries") assert 2 == len(coll3_libraries) coll3_l1, coll3_default = sorted( coll3_libraries, key=lambda x: x.get("short_name") ) assert "L1" == coll3_l1.get("short_name") assert "14" == coll3_l1.get("ebook_loan_duration") assert self._default_library.short_name == coll3_default.get("short_name") with self.request_context_with_admin("/", admin=l1_librarian): # A librarian only sees collections associated with their library. response = controller.process_collections() [coll3] = response.get("collections") assert c3.id == coll3.get("id") coll3_libraries = coll3.get("libraries") assert 1 == len(coll3_libraries) assert "L1" == coll3_libraries[0].get("short_name") assert "14" == coll3_libraries[0].get("ebook_loan_duration") HasSelfTests.prior_test_results = old_prior_test_results
def test_libraries_post_edit(self): # A library already exists. library = self._library("New York Public Library", "nypl") ConfigurationSetting.for_library(Configuration.FEATURED_LANE_SIZE, library).value = 5 ConfigurationSetting.for_library( Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value = FacetConstants.ORDER_RANDOM ConfigurationSetting.for_library( Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, library).value = json.dumps( [FacetConstants.ORDER_TITLE, FacetConstants.ORDER_RANDOM]) ConfigurationSetting.for_library(Configuration.LOGO, library).value = "A tiny image" with self.request_context_with_admin("/", method="POST"): flask.request.form = MultiDict([ ("uuid", library.uuid), ("name", "The New York Public Library"), ("short_name", "nypl"), (Configuration.FEATURED_LANE_SIZE, "20"), (Configuration.MINIMUM_FEATURED_QUALITY, "0.9"), (Configuration.WEBSITE_URL, "https://library.library/"), (Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS, "*****@*****.**"), (Configuration.HELP_EMAIL, "*****@*****.**"), (Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME, FacetConstants.ORDER_AUTHOR), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_AUTHOR, ''), (Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME + "_" + FacetConstants.ORDER_RANDOM, ''), ]) flask.request.files = MultiDict([]) response = self.manager.admin_library_settings_controller.process_post( ) eq_(response.status_code, 200) library = get_one(self._db, Library, uuid=library.uuid) eq_(library.uuid, response.response[0]) eq_(library.name, "The New York Public Library") eq_(library.short_name, "nypl") # The library-wide settings were updated. def val(x): return ConfigurationSetting.for_library(x, library).value eq_("https://library.library/", val(Configuration.WEBSITE_URL)) eq_("*****@*****.**", val(Configuration.DEFAULT_NOTIFICATION_EMAIL_ADDRESS)) eq_("*****@*****.**", val(Configuration.HELP_EMAIL)) eq_("20", val(Configuration.FEATURED_LANE_SIZE)) eq_("0.9", val(Configuration.MINIMUM_FEATURED_QUALITY)) eq_( FacetConstants.ORDER_AUTHOR, val(Configuration.DEFAULT_FACET_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) eq_( json.dumps( [FacetConstants.ORDER_AUTHOR, FacetConstants.ORDER_RANDOM]), val(Configuration.ENABLED_FACETS_KEY_PREFIX + FacetConstants.ORDER_FACET_GROUP_NAME)) # The library-wide logo was not updated and has been left alone. eq_( "A tiny image", ConfigurationSetting.for_library(Configuration.LOGO, library).value)
def test_patron_auth_services_get_with_simple_auth_service(self): auth_service, ignore = create( self._db, ExternalIntegration, protocol=SimpleAuthenticationProvider.__module__, goal=ExternalIntegration.PATRON_AUTH_GOAL, name="name", ) auth_service.setting( BasicAuthenticationProvider.TEST_IDENTIFIER).value = "user" auth_service.setting( BasicAuthenticationProvider.TEST_PASSWORD).value = "pass" with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services( ) [service] = response.get("patron_auth_services") eq_(auth_service.id, service.get("id")) eq_(auth_service.name, service.get("name")) eq_(SimpleAuthenticationProvider.__module__, service.get("protocol")) eq_( "user", service.get("settings").get( BasicAuthenticationProvider.TEST_IDENTIFIER)) eq_( "pass", service.get("settings").get( BasicAuthenticationProvider.TEST_PASSWORD)) eq_([], service.get("libraries")) auth_service.libraries += [self._default_library] with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services( ) [service] = response.get("patron_auth_services") eq_( "user", service.get("settings").get( BasicAuthenticationProvider.TEST_IDENTIFIER)) [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_( None, library.get( AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION)) ConfigurationSetting.for_library_and_externalintegration( self._db, AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION, self._default_library, auth_service, ).value = "^(u)" with self.request_context_with_admin("/"): response = self.manager.admin_patron_auth_services_controller.process_patron_auth_services( ) [service] = response.get("patron_auth_services") [library] = service.get("libraries") eq_(self._default_library.short_name, library.get("short_name")) eq_( "^(u)", library.get( AuthenticationProvider.EXTERNAL_TYPE_REGULAR_EXPRESSION))
def current_value(self, setting, library): """Retrieve the current value of the given setting from the database.""" return ConfigurationSetting.for_library(setting["key"], library).value