From 28f34c433e75fe61913984dec7fb07656b977e95 Mon Sep 17 00:00:00 2001 From: guerler Date: Fri, 5 Apr 2019 23:17:34 -0400 Subject: [PATCH] Add OpenID support --- config/galaxy.yml.sample | 64 ++++---- config/openid_conf.xml.sample | 5 + config/reports.yml.sample | 2 +- config/tool_shed.yml.sample | 4 +- doc/source/admin/galaxy_options.rst | 33 ++++ lib/galaxy/app.py | 5 + lib/galaxy/config.py | 3 + lib/galaxy/openid/__init__.py | 3 + lib/galaxy/openid/providers.py | 147 ++++++++++++++++++ lib/galaxy/web/framework/openid_manager.py | 75 +++++++++ lib/galaxy/webapps/galaxy/config_schema.yml | 23 +++ .../webapps/galaxy/controllers/openid.py | 69 ++++++++ 12 files changed, 404 insertions(+), 29 deletions(-) create mode 100644 config/openid_conf.xml.sample create mode 100644 lib/galaxy/openid/__init__.py create mode 100644 lib/galaxy/openid/providers.py create mode 100644 lib/galaxy/web/framework/openid_manager.py create mode 100644 lib/galaxy/webapps/galaxy/controllers/openid.py diff --git a/config/galaxy.yml.sample b/config/galaxy.yml.sample index 94fbc94855f..6ad9a7b34dd 100644 --- a/config/galaxy.yml.sample +++ b/config/galaxy.yml.sample @@ -110,7 +110,7 @@ galaxy: # string to specify an external database instead. This string takes # many options which are explained in detail in the config file # documentation. - #database_connection: 'sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE' + #database_connection: sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE # If the server logs errors about not having enough database pool # connections, you will want to increase these values, or consider @@ -163,7 +163,7 @@ galaxy: # instances with pretested installs. The following option can be used # to separate the tool shed install database (all other options listed # above but prefixed with install_ are also available). - #install_database_connection: 'sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE' + #install_database_connection: sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE # Setting the following option to true will cause Galaxy to # automatically migrate the database forward after updates. This is @@ -183,7 +183,7 @@ galaxy: # can be locally developed or installed from Galaxy tool sheds. # (config/tool_conf.xml.sample will be used if left unset and # config/tool_conf.xml does not exist). - #tool_config_file: 'config/tool_conf.xml,config/shed_tool_conf.xml' + #tool_config_file: config/tool_conf.xml,config/shed_tool_conf.xml # Enable / disable checking if any tools defined in the above non-shed # tool_config_files (i.e., tool_conf.xml) have been migrated from the @@ -245,7 +245,7 @@ galaxy: # conda channels to enable by default (https://conda.io/docs/user- # guide/tasks/manage-channels.html) - #conda_ensure_channels: 'iuc,conda-forge,bioconda,defaults' + #conda_ensure_channels: iuc,conda-forge,bioconda,defaults # Use locally-built conda packages. #conda_use_local: false @@ -350,7 +350,7 @@ galaxy: # Conda channels to use when building Docker or Singularity containers # using involucro. - #mulled_channels: 'conda-forge,bioconda' + #mulled_channels: conda-forge,bioconda # Enable automatic polling of relative tool sheds to see if any # updates are available for installed repositories. Ideally only one @@ -582,7 +582,7 @@ galaxy: # URL of the support resource for the galaxy instance. Used in # activation emails. - #instance_resource_url: 'https://galaxyproject.org/' + #instance_resource_url: https://galaxyproject.org/ # E-mail domains blacklist is used for filtering out users that are # using disposable email address during the registration. If their @@ -609,7 +609,7 @@ galaxy: # Shown in warning box to users that were not activated yet. In use # only if activation_grace_period is set. - #inactivity_box_content: 'Your account has not been activated yet. Feel free to browse around and see what''s available, but you won''t be able to upload data or run jobs until you have verified your email address.' + #inactivity_box_content: Your account has not been activated yet. Feel free to browse around and see what's available, but you won't be able to upload data or run jobs until you have verified your email address. # Password expiration period (in days). Users are required to change # their password every x days. Users will be redirected to the change @@ -640,7 +640,7 @@ galaxy: # Main, Test and Archaea browsers, but the default if left commented # is to not allow any display sites to bypass security (you must # uncomment the line below to allow them). - #display_servers: 'hgw1.cse.ucsc.edu,hgw2.cse.ucsc.edu,hgw3.cse.ucsc.edu,hgw4.cse.ucsc.edu,hgw5.cse.ucsc.edu,hgw6.cse.ucsc.edu,hgw7.cse.ucsc.edu,hgw8.cse.ucsc.edu,lowepub.cse.ucsc.edu' + #display_servers: hgw1.cse.ucsc.edu,hgw2.cse.ucsc.edu,hgw3.cse.ucsc.edu,hgw4.cse.ucsc.edu,hgw5.cse.ucsc.edu,hgw6.cse.ucsc.edu,hgw7.cse.ucsc.edu,hgw8.cse.ucsc.edu,lowepub.cse.ucsc.edu # To disable the old-style display applications that are hardcoded # into datatype classes, set enable_old_display_applications = False. @@ -694,7 +694,7 @@ galaxy: # your machine on the virtualbox network (vboxnet0) setup for the # Docker host VM. This can found by running ifconfig and using the IP # address of the network vboxnet0. - #galaxy_infrastructure_url: 'http://localhost:8080' + #galaxy_infrastructure_url: http://localhost:8080 # If the above URL cannot be determined ahead of time in dynamic # environments but the port which should be used to access Galaxy can @@ -716,26 +716,26 @@ galaxy: #helpsite_url: '' # The URL linked by the "Wiki" link in the "Help" menu. - #wiki_url: 'https://galaxyproject.org/' + #wiki_url: https://galaxyproject.org/ # The URL linked by the "Support" link in the "Help" menu. - #support_url: 'https://galaxyproject.org/support/' + #support_url: https://galaxyproject.org/support/ # The URL linked by the "How to Cite Galaxy" link in the "Help" menu. - #citation_url: 'https://galaxyproject.org/citing-galaxy' + #citation_url: https://galaxyproject.org/citing-galaxy # The URL linked by the "Search" link in the "Help" menu. - #search_url: 'https://galaxyproject.org/search/' + #search_url: https://galaxyproject.org/search/ # The URL linked by the "Mailing Lists" link in the "Help" menu. - #mailing_lists_url: 'https://galaxyproject.org/mailing-lists' + #mailing_lists_url: https://galaxyproject.org/mailing-lists # The URL linked by the "Videos" link in the "Help" menu. - #screencasts_url: 'https://vimeo.com/galaxyproject' + #screencasts_url: https://vimeo.com/galaxyproject # Points to the GenomeSpace UI service which will be used by the # GenomeSpace importer and exporter tools - #genomespace_ui_url: 'https://gsui.genomespace.org/jsui/' + #genomespace_ui_url: https://gsui.genomespace.org/jsui/ # The URL linked by the "Terms and Conditions" link in the "Help" # menu, as well as on the user registration and login forms and in the @@ -907,7 +907,7 @@ galaxy: # The golang proxy needs to know how to talk to your docker daemon. # Currently TLS is not supported, that will come in an update. - #dynamic_proxy_golang_docker_address: 'unix:///var/run/docker.sock' + #dynamic_proxy_golang_docker_address: unix:///var/run/docker.sock # The golang proxy uses a RESTful HTTP API for communication with # Galaxy instead of a JSON or SQLite file for IPC. If you do not @@ -1041,7 +1041,7 @@ galaxy: # Heartbeat log filename. Can accept the template variables # {server_name} and {pid} - #heartbeat_log: 'heartbeat_{server_name}.log' + #heartbeat_log: heartbeat_{server_name}.log # Log to Sentry Sentry is an open source logging and error aggregation # platform. Setting sentry_dsn will enable the Sentry middleware and @@ -1432,6 +1432,18 @@ galaxy: # to set this on public servers. #master_api_key: changethis + # Enable authentication via OpenID. Allows users to log in to their + # Galaxy account by authenticating with an OpenID provider. + #enable_openid: false + + # If OpenID is enabled, this configuration file specifies providers to + # use. Falls back to the .sample variant in config if default does not + # exist. + #openid_config_file: config/openid_conf.xml + + # If OpenID is enabled, consumer cache directory to use. + #openid_consumer_cache_path: database/openid_consumer_cache + # Enable tool tags (associating tools with tags). This has its own # option since its implementation has a few performance implications # on startup for large servers. @@ -1448,7 +1460,7 @@ galaxy: # The URL to the myExperiment instance being used (omit scheme but # include port) - #myexperiment_url: 'www.myexperiment.org:80' + #myexperiment_url: www.myexperiment.org:80 # Enable Galaxy's "Upload via FTP" interface. You'll need to install # and configure an FTP server (we've used ProFTPd since it can use @@ -1470,7 +1482,7 @@ galaxy: # Python string template used to determine an FTP upload directory for # a particular user. - #ftp_upload_dir_template: '${ftp_upload_dir}/${ftp_upload_dir_identifier}' + #ftp_upload_dir_template: ${ftp_upload_dir}/${ftp_upload_dir_identifier # This should be set to False to prevent Galaxy from deleting uploaded # FTP files as it imports them. @@ -1704,22 +1716,22 @@ galaxy: # Define toolbox filters (https://galaxyproject.org/user-defined- # toolbox-filters/) that users may use to restrict the tools to # display. - #user_tool_filters: 'examples:restrict_upload_to_admins, examples:restrict_encode' + #user_tool_filters: examples:restrict_upload_to_admins, examples:restrict_encode # Define toolbox filters (https://galaxyproject.org/user-defined- # toolbox-filters/) that users may use to restrict the tool sections # to display. - #user_tool_section_filters: 'examples:restrict_text' + #user_tool_section_filters: examples:restrict_text # Define toolbox filters (https://galaxyproject.org/user-defined- # toolbox-filters/) that users may use to restrict the tool labels to # display. - #user_tool_label_filters: 'examples:restrict_upload_to_admins, examples:restrict_encode' + #user_tool_label_filters: examples:restrict_upload_to_admins, examples:restrict_encode # The base module(s) that are searched for modules for toolbox # filtering (https://galaxyproject.org/user-defined-toolbox-filters/) # functions. - #toolbox_filter_base_modules: 'galaxy.tools.toolbox.filters,galaxy.tools.filters' + #toolbox_filter_base_modules: galaxy.tools.toolbox.filters,galaxy.tools.filters # Galaxy uses AMQP internally for communicating between processes. # For example, when reloading the toolbox or locking job execution, @@ -1731,13 +1743,13 @@ galaxy: # not specified either, Galaxy will automatically create and use a # separate sqlite database located in your /database folder # (indicated in the commented out line below). - #amqp_internal_connection: 'sqlalchemy+sqlite:///./database/control.sqlite?isolation_level=IMMEDIATE' + #amqp_internal_connection: sqlalchemy+sqlite:///./database/control.sqlite?isolation_level=IMMEDIATE # Galaxy real time communication server settings #enable_communication_server: false # Galaxy real time communication server settings - #communication_server_host: 'http://localhost' + #communication_server_host: http://localhost # Galaxy real time communication server settings #communication_server_port: 7070 diff --git a/config/openid_conf.xml.sample b/config/openid_conf.xml.sample new file mode 100644 index 00000000000..735c4ea8648 --- /dev/null +++ b/config/openid_conf.xml.sample @@ -0,0 +1,5 @@ + + + + + diff --git a/config/reports.yml.sample b/config/reports.yml.sample index 920082f5925..1855c8f427b 100644 --- a/config/reports.yml.sample +++ b/config/reports.yml.sample @@ -95,7 +95,7 @@ reports: # Galaxy instances, so sqlite (and the default value below) is not # supported. An SQLAlchemy connection string should be used specify an # external database. - #database_connection: 'sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE' + #database_connection: sqlite:///./database/universe.sqlite?isolation_level=IMMEDIATE # Where dataset files are stored. #file_path: database/files diff --git a/config/tool_shed.yml.sample b/config/tool_shed.yml.sample index ca7fe946182..939a88b2100 100644 --- a/config/tool_shed.yml.sample +++ b/config/tool_shed.yml.sample @@ -89,7 +89,7 @@ tool_shed: # string to specify an external database instead. This string takes # many options which are explained in detail in the config file # documentation. - #database_connection: 'sqlite:///./database/community.sqlite?isolation_level=IMMEDIATE' + #database_connection: sqlite:///./database/community.sqlite?isolation_level=IMMEDIATE # Where the hgweb.config file is stored. The default is the Galaxy # installation directory. @@ -253,7 +253,7 @@ tool_shed: #smtp_ssl: false # The URL linked by the "Support" link in the "Help" menu. - #support_url: 'https://galaxyproject.org/support/' + #support_url: https://galaxyproject.org/support/ # Address to join mailing list #mailing_join_addr: galaxy-announce-join@bx.psu.edu diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst index 409b4500de2..a6c7cdf1ae1 100644 --- a/doc/source/admin/galaxy_options.rst +++ b/doc/source/admin/galaxy_options.rst @@ -2943,6 +2943,39 @@ :Type: str +~~~~~~~~~~~~~~~~~ +``enable_openid`` +~~~~~~~~~~~~~~~~~ + +:Description: + Enable authentication via OpenID. Allows users to log in to their + Galaxy account by authenticating with an OpenID provider. +:Default: ``false`` +:Type: bool + + +~~~~~~~~~~~~~~~~~~~~~~ +``openid_config_file`` +~~~~~~~~~~~~~~~~~~~~~~ + +:Description: + If OpenID is enabled, this configuration file specifies providers + to use. Falls back to the .sample variant in config if default + does not exist. +:Default: ``config/openid_conf.xml`` +:Type: str + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``openid_consumer_cache_path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:Description: + If OpenID is enabled, consumer cache directory to use. +:Default: ``database/openid_consumer_cache`` +:Type: str + + ~~~~~~~~~~~~~~~~~~~~ ``enable_tool_tags`` ~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/galaxy/app.py b/lib/galaxy/app.py index 98a6114762c..1ca8e909a9f 100644 --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -19,6 +19,7 @@ from galaxy.managers.histories import HistoryManager from galaxy.managers.libraries import LibraryManager from galaxy.managers.tools import DynamicToolManager from galaxy.model.tags import GalaxyTagHandler +from galaxy.openid.providers import OpenIDProviders from galaxy.queue_worker import GalaxyQueueWorker from galaxy.tools.cache import ( ToolCache, @@ -38,6 +39,7 @@ from galaxy.visualization.data_providers.registry import DataProviderRegistry from galaxy.visualization.genomes import Genomes from galaxy.visualization.plugins.registry import VisualizationsRegistry from galaxy.web import url_for +from galaxy.web.framework import openid_manager from galaxy.web.proxy import ProxyManager from galaxy.web.stack import application_stack_instance from galaxy.web.stack.database_heartbeat import DatabaseHeartbeat @@ -168,6 +170,9 @@ class UniverseApplication(config.ConfiguresGalaxyMixin): self.quota_agent = galaxy.quota.NoQuotaAgent(self.model) # Heartbeat for thread profiling self.heartbeat = None + if self.config.enable_openid: + self.openid_manager = openid_manager.OpenIDManager(self.config.openid_consumer_cache_path) + self.openid_providers = OpenIDProviders.from_file(self.config.openid_config_file) from galaxy import auth self.auth_manager = auth.AuthManager(self) # Start the heartbeat process if configured and available (wait until diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index a7b57bf0af8..a46b5bef3eb 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -55,6 +55,7 @@ PATH_DEFAULTS = dict( workflow_resource_params_file=['config/workflow_resource_params_conf.xml', 'workflow_resource_params_conf.xml'], migrated_tools_config=['migrated_tools_conf.xml', 'config/migrated_tools_conf.xml'], object_store_config_file=['config/object_store_conf.xml', 'object_store_conf.xml'], + openid_config_file=['config/openid_conf.xml', 'openid_conf.xml', 'config/openid_conf.xml.sample'], shed_data_manager_config_file=['shed_data_manager_conf.xml', 'config/shed_data_manager_conf.xml'], shed_tool_data_table_config=['shed_tool_data_table_conf.xml', 'config/shed_tool_data_table_conf.xml'], tool_sheds_config_file=['config/tool_sheds_conf.xml', 'tool_sheds_conf.xml', 'config/tool_sheds_conf.xml.sample'], @@ -202,6 +203,7 @@ class Configuration(object): if override_tempdir: tempfile.tempdir = self.new_file_path self.shared_home_dir = kwargs.get("shared_home_dir", None) + self.openid_consumer_cache_path = resolve_path(kwargs.get("openid_consumer_cache_path", "database/openid_consumer_cache"), self.root) self.cookie_path = kwargs.get("cookie_path", "/") self.enable_quotas = string_as_bool(kwargs.get('enable_quotas', False)) self.enable_unique_workflow_defaults = string_as_bool(kwargs.get('enable_unique_workflow_defaults', False)) @@ -363,6 +365,7 @@ class Configuration(object): self.communication_server_host = kwargs.get('communication_server_host', 'http://localhost') self.communication_server_port = int(kwargs.get('communication_server_port', '7070')) self.persistent_communication_rooms = listify(kwargs.get("persistent_communication_rooms", []), do_strip=True) + self.enable_openid = string_as_bool(kwargs.get('enable_openid', 'False')) self.enable_quotas = string_as_bool(kwargs.get('enable_quotas', 'False')) # Tasked job runner. self.use_tasked_jobs = string_as_bool(kwargs.get('use_tasked_jobs', False)) diff --git a/lib/galaxy/openid/__init__.py b/lib/galaxy/openid/__init__.py new file mode 100644 index 00000000000..d196c842445 --- /dev/null +++ b/lib/galaxy/openid/__init__.py @@ -0,0 +1,3 @@ +""" +OpenID functionality +""" diff --git a/lib/galaxy/openid/providers.py b/lib/galaxy/openid/providers.py new file mode 100644 index 00000000000..39e51a0275f --- /dev/null +++ b/lib/galaxy/openid/providers.py @@ -0,0 +1,147 @@ +""" +Contains OpenID provider functionality +""" +import logging +import os + +import six + +from galaxy.util import parse_xml, string_as_bool +from galaxy.util.odict import odict + + +log = logging.getLogger(__name__) + +NO_PROVIDER_ID = 'None' +RESERVED_PROVIDER_IDS = [NO_PROVIDER_ID] + + +class OpenIDProvider(object): + '''An OpenID Provider object.''' + @classmethod + def from_file(cls, filename): + return cls.from_elem(parse_xml(filename).getroot()) + + @classmethod + def from_elem(cls, xml_root): + provider_elem = xml_root + provider_id = provider_elem.get('id', None) + provider_name = provider_elem.get('name', provider_id) + op_endpoint_url = provider_elem.find('op_endpoint_url') + if op_endpoint_url is not None: + op_endpoint_url = op_endpoint_url.text + never_associate_with_user = string_as_bool(provider_elem.get('never_associate_with_user', 'False')) + assert (provider_id and provider_name and op_endpoint_url), Exception("OpenID Provider improperly configured") + assert provider_id not in RESERVED_PROVIDER_IDS, Exception('Specified OpenID Provider uses a reserved id: %s' % (provider_id)) + sreg_required = [] + sreg_optional = [] + use_for = {} + store_user_preference = {} + use_default_sreg = True + for elem in provider_elem.findall('sreg'): + use_default_sreg = False + for field_elem in elem.findall('field'): + sreg_name = field_elem.get('name') + assert sreg_name, Exception('A name is required for a sreg element') + if string_as_bool(field_elem.get('required')): + sreg_required.append(sreg_name) + else: + sreg_optional.append(sreg_name) + for use_elem in field_elem.findall('use_for'): + use_for[use_elem.get('name')] = sreg_name + for store_user_preference_elem in field_elem.findall('store_user_preference'): + store_user_preference[store_user_preference_elem.get('name')] = sreg_name + if use_default_sreg: + sreg_required = None + sreg_optional = None + use_for = None + return cls(provider_id, provider_name, op_endpoint_url, sreg_required=sreg_required, sreg_optional=sreg_optional, use_for=use_for, store_user_preference=store_user_preference, never_associate_with_user=never_associate_with_user) + + def __init__(self, id, name, op_endpoint_url, sreg_required=None, sreg_optional=None, use_for=None, store_user_preference=None, never_associate_with_user=None): + '''When sreg options are not specified, defaults are used.''' + self.id = id + self.name = name + self.op_endpoint_url = op_endpoint_url + if sreg_optional is None: + self.sreg_optional = ['nickname', 'email'] + else: + self.sreg_optional = sreg_optional + if sreg_required: + self.sreg_required = sreg_required + else: + self.sreg_required = [] + if use_for is not None: + self.use_for = use_for + else: + self.use_for = {} + if 'nickname' in (self.sreg_optional + self.sreg_required): + self.use_for['username'] = 'nickname' + if 'email' in (self.sreg_optional + self.sreg_required): + self.use_for['email'] = 'email' + if store_user_preference: + self.store_user_preference = store_user_preference + else: + self.store_user_preference = {} + if never_associate_with_user: + self.never_associate_with_user = True + else: + self.never_associate_with_user = False + + def post_authentication(self, trans, openid_manager, info): + sreg_attributes = openid_manager.get_sreg(info) + for store_pref_name, store_pref_value_name in self.store_user_preference.items(): + if store_pref_value_name in (self.sreg_optional + self.sreg_required): + trans.user.preferences[store_pref_name] = sreg_attributes.get(store_pref_value_name) + else: + raise Exception('Only sreg is currently supported.') + trans.sa_session.add(trans.user) + trans.sa_session.flush() + + def has_post_authentication_actions(self): + return bool(self.store_user_preference) + + +class OpenIDProviders(object): + '''Collection of OpenID Providers''' + NO_PROVIDER_ID = NO_PROVIDER_ID + + @classmethod + def from_file(cls, filename): + try: + return cls.from_elem(parse_xml(filename).getroot()) + except Exception as e: + log.error('Failed to load OpenID Providers: %s' % (e)) + return cls() + + @classmethod + def from_elem(cls, xml_root): + oid_elem = xml_root + providers = odict() + for elem in oid_elem.findall('provider'): + try: + provider = OpenIDProvider.from_file(os.path.join('openid', elem.get('file'))) + providers[provider.id] = provider + log.debug('Loaded OpenID provider: %s (%s)' % (provider.name, provider.id)) + except Exception as e: + log.error('Failed to add OpenID provider: %s' % (e)) + return cls(providers) + + def __init__(self, providers=None): + if providers: + self.providers = providers + else: + self.providers = odict() + self._banned_identifiers = [provider.op_endpoint_url for provider in self.providers.values() if provider.never_associate_with_user] + + def __iter__(self): + for provider in six.itervalues(self.providers): + yield provider + + def get(self, name, default=None): + if name in self.providers: + return self.providers[name] + else: + return default + + def new_provider_from_identifier(self, identifier): + return OpenIDProvider(None, identifier, identifier, never_associate_with_user=identifier in self._banned_identifiers) diff --git a/lib/galaxy/web/framework/openid_manager.py b/lib/galaxy/web/framework/openid_manager.py new file mode 100644 index 00000000000..03a2a0f55f0 --- /dev/null +++ b/lib/galaxy/web/framework/openid_manager.py @@ -0,0 +1,75 @@ +""" +Manage the OpenID consumer and related data stores. +""" + +import logging +import os +import pickle + +try: + from openid import oidutil + from openid.consumer import consumer + from openid.extensions import sreg + from openid.store import filestore +except ImportError: + oidutil = None + + class FakeConsumer(object): + def __getattr__(x, y): + return None + consumer = FakeConsumer() + + +OPENID_IMPORT_MESSAGE = ('The Python openid package is required to use this ' + 'feature, please install it') + +log = logging.getLogger(__name__) + + +def oidlog(message, level=0): + log.debug(message) + + +if oidutil is not None: + oidutil.log = oidlog + + +class OpenIDManager(object): + def __init__(self, cache_path): + assert oidutil is not None, OPENID_IMPORT_MESSAGE + self.session_path = os.path.join(cache_path, 'session') + self.store_path = os.path.join(cache_path, 'store') + for dir in self.session_path, self.store_path: + if not os.path.exists(dir): + os.makedirs(dir) + self.store = filestore.FileOpenIDStore(self.store_path) + + def get_session(self, trans): + session_file = os.path.join(self.session_path, str(trans.galaxy_session.id)) + if not os.path.exists(session_file): + pickle.dump(dict(), open(session_file, 'w')) + return pickle.load(open(session_file)) + + def persist_session(self, trans, oidconsumer): + session_file = os.path.join(self.session_path, str(trans.galaxy_session.id)) + pickle.dump(oidconsumer.session, open(session_file, 'w')) + + def get_consumer(self, trans): + return consumer.Consumer(self.get_session(trans), self.store) + + def add_sreg(self, trans, request, required=None, optional=None): + if required is None: + required = [] + if optional is None: + optional = [] + sreg_request = sreg.SRegRequest(required=required, optional=optional) + request.addExtension(sreg_request) + + def get_sreg(self, info): + return sreg.SRegResponse.fromSuccessResponse(info) + + # so I don't have to expose all of openid.consumer.consumer + FAILURE = consumer.FAILURE + SUCCESS = consumer.SUCCESS + CANCEL = consumer.CANCEL + SETUP_NEEDED = consumer.SETUP_NEEDED diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml index 3c46acc6557..198855ae506 100644 --- a/lib/galaxy/webapps/galaxy/config_schema.yml +++ b/lib/galaxy/webapps/galaxy/config_schema.yml @@ -2186,6 +2186,29 @@ mapping: need to bootstrap Galaxy, you probably do not want to set this on public servers. + enable_openid: + type: bool + default: false + required: false + desc: | + Enable authentication via OpenID. Allows users to log in to their Galaxy + account by authenticating with an OpenID provider. + + openid_config_file: + type: str + default: config/openid_conf.xml + required: false + desc: | + If OpenID is enabled, this configuration file specifies providers to use. + Falls back to the .sample variant in config if default does not exist. + + openid_consumer_cache_path: + type: str + default: database/openid_consumer_cache + required: false + desc: | + If OpenID is enabled, consumer cache directory to use. + enable_tool_tags: type: bool default: false diff --git a/lib/galaxy/webapps/galaxy/controllers/openid.py b/lib/galaxy/webapps/galaxy/controllers/openid.py new file mode 100644 index 00000000000..2b5d17e20c7 --- /dev/null +++ b/lib/galaxy/webapps/galaxy/controllers/openid.py @@ -0,0 +1,69 @@ +""" +Contains the OpenID interface in the Universe class +""" + +import logging + +from markupsafe import escape + +import galaxy.util +from galaxy import web +from galaxy.web import error, url_for +from galaxy.web.base.controller import BaseUIController + +log = logging.getLogger(__name__) + + +class OpenID(BaseUIController): + + @web.expose + def openid_auth(self, trans, **kwd): + '''Handles user request to access an OpenID provider''' + if not trans.app.config.enable_openid: + return trans.show_error_message('OpenID authentication is not enabled in this instance of Galaxy') + consumer = trans.app.openid_manager.get_consumer(trans) + openid_provider = kwd.get('openid_provider', '') + if openid_provider: + openid_provider_obj = trans.app.openid_providers.get(openid_provider) + else: + return trans.show_error_message('An OpenID provider was not specified.') + if not openid_provider_obj: + return trans.show_error_message('An OpenID provider was not specified or invalid.') + process_url = trans.request.base.rstrip('/') + url_for(controller='openid', action='openid_process', openid_provider=openid_provider) + request = None + try: + request = consumer.begin(openid_provider_obj.op_endpoint_url) + if request is None: + return trans.show_error_message('No OpenID services are available at %s' % openid_provider_obj.op_endpoint_url) + except Exception as e: + return 'Failed to begin OpenID authentication: %s' % str(e) + if request is not None: + trans.app.openid_manager.add_sreg(trans, request, required=openid_provider_obj.sreg_required, optional=openid_provider_obj.sreg_optional) + if request.shouldSendRedirect(): + redirect_url = request.redirectURL( + trans.request.base, process_url) + trans.app.openid_manager.persist_session(trans, consumer) + return trans.response.send_redirect(redirect_url) + else: + form = request.htmlMarkup(trans.request.base, process_url, form_tag_attrs={'id': 'openid_message', 'target': '_top'}) + trans.app.openid_manager.persist_session(trans, consumer) + return form + return trans.show_error_message('OpenID request failed.') + + @web.expose + def openid_process(self, trans, **kwd): + '''Handle's response from OpenID Providers''' + if not trans.app.config.enable_openid: + return trans.show_error_message('OpenID authentication is not enabled in this instance of Galaxy') + consumer = trans.app.openid_manager.get_consumer(trans) + info = consumer.complete(kwd, trans.request.url) + display_identifier = info.getDisplayIdentifier() + openid_provider = kwd.get('openid_provider', None) + if info.status == trans.app.openid_manager.FAILURE and display_identifier: + return trans.show_error_message("Login via OpenID failed. The technical reason for this follows, please include this message in your email if you need to %s to resolve this problem: %s" % (contact, info.message)) + elif info.status == trans.app.openid_manager.SUCCESS: + if info.endpoint.canonicalID: + display_identifier = info.endpoint.canonicalID + openid_provider_obj = trans.app.openid_providers.get(openid_provider) + openid_provider_obj.post_authentication(trans, trans.app.openid_manager, info) + return trans.show_message("Processed OpenID authentication. Click here to return." % url_for("/"))