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)
예제 #7
0
 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)
예제 #11
0
    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))
예제 #12
0
 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))
예제 #17
0
    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
예제 #18
0
    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))
예제 #21
0
    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)
예제 #29
0
 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 }
        )
예제 #31
0
    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)
예제 #32
0
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):
예제 #33
0
    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)
예제 #34
0
    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))
예제 #36
0
 def value(self, key, integration):
     _db = Session.object_session(integration)
     return ConfigurationSetting.for_library_and_externalintegration(
         _db, key, self.library, integration).value
예제 #37
0
 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
예제 #38
0
 def process_delete(self, key):
     self.require_system_admin()
     setting = ConfigurationSetting.sitewide(self._db, key)
     setting.value = None
     return Response(str(_("Deleted")), 200)
예제 #39
0
@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)
예제 #40
0
 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)
예제 #42
0
    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)))
예제 #43
0
    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)
예제 #44
0
    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)
예제 #45
0
    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
예제 #50
0
 def current_value(self, setting, library):
     return ConfigurationSetting.for_library(setting['key'], library).value
예제 #51
0
        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"))
예제 #52
0
    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
예제 #53
0
    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)
예제 #54
0
    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)
예제 #55
0
 def max_outstanding_fines(cls, library):
     max_fines = ConfigurationSetting.for_library(
         cls.MAX_OUTSTANDING_FINES, library
     ).value
     return MoneyUtility.parse(max_fines)
예제 #56
0
    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)
예제 #57
0
    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
예제 #58
0
    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)
예제 #59
0
    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))
예제 #60
0
 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