# HG changeset patch -- Bitbucket.org # Project galaxy-dist # URL http://bitbucket.org/galaxy/galaxy-dist/overview # User Nate Coraor <nate@bx.psu.edu> # Date 1290006822 18000 # Node ID a68c74ec8cfbc641db4b6fe424e3f77b9f3c5d3c # Parent 56431b214df16653a966b96bf399c7646da267b8 OpenID support for Galaxy. --- a/eggs.ini +++ b/eggs.ini @@ -44,6 +44,7 @@ Paste = 1.6 PasteDeploy = 1.3.3 PasteScript = 1.7.3 pexpect = 2.4 +python_openid = 2.2.5 Routes = 1.12.3 SQLAlchemy = 0.5.6 sqlalchemy_migrate = 0.5.4 --- /dev/null +++ b/templates/user/openid_manage.mako @@ -0,0 +1,17 @@ +## Template generates a grid that enables user to select items. +<%namespace file="../grid_base.mako" import="make_grid" /> +<%namespace file="login.mako" import="render_openid_form" /> + +<%inherit file="../grid_base.mako" /> + +<%def name="grid_body( grid )"> + ${make_grid( grid )} + <h2>Associate more OpenIDs</h2> + ${render_openid_form( kwargs['referer'], True, kwargs['openid_providers'] )} +</%def> + +<%def name="center_panel()"> + <div style="margin: 1em;"> + ${grid_body( grid )} + </div> +</%def> Binary file static/images/openid-16x16.gif has changed --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1953,6 +1953,12 @@ class UserAddress( object ): html = html + '<br/>' + 'Phone: ' + self.phone return html +class UserOpenID( object ): + def __init__( self, user=None, session=None, openid=None ): + self.user = user + self.session = session + self.openid = openid + class Page( object ): def __init__( self ): self.id = None --- a/templates/user/index.mako +++ b/templates/user/index.mako @@ -15,6 +15,9 @@ %if trans.app.config.enable_api: <li><a href="${h.url_for( controller='user', action='api_keys' )}">${_('Manage your API Keys')}</a> for new histories</li> %endif + %if trans.app.config.enable_openid: + <li><a href="${h.url_for( controller='user', action='openid_manage' )}">${ ('Manage OpenIDs')}</a> linked to your account</li> + %endif %else: <li><a href="${h.url_for( controller='user', action='show_info', webapp='community' )}">${_('Manage your information')}</a></li> %endif --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0062_user_openid_table.py @@ -0,0 +1,48 @@ +""" +Migration script to create table for associating sessions and users with +OpenIDs. +""" + +from sqlalchemy import * +from sqlalchemy.orm import * +from migrate import * +from migrate.changeset import * + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) +db_session = scoped_session( sessionmaker( bind=migrate_engine, autoflush=False, autocommit=True ) ) + +# Table to add + +UserOpenID_table = Table( "galaxy_user_openid", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, index=True, default=now, onupdate=now ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "openid", TEXT, index=True, unique=True ), + ) + +def upgrade(): + print __doc__ + metadata.reflect() + + # Create galaxy_user_openid table + try: + UserOpenID_table.create() + except Exception, e: + log.debug( "Creating galaxy_user_openid table failed: %s" % str( e ) ) + +def downgrade(): + metadata.reflect() + + # Drop galaxy_user_openid table + try: + UserOpenID_table.drop() + except Exception, e: + log.debug( "Dropping galaxy_user_openid table failed: %s" % str( e ) ) --- a/templates/user/login.mako +++ b/templates/user/login.mako @@ -1,23 +1,77 @@ -<%inherit file="/base.mako"/> +<%! + def inherit(context): + if context.get('use_panels'): + if context.get('webapp'): + webapp = context.get('webapp') + else: + webapp = 'galaxy' + return '/webapps/%s/base_panels.mako' % webapp + else: + return '/base.mako' +%> +<%inherit file="${inherit(context)}"/> + +<%def name="init()"> +<% + self.has_left_panel=False + self.has_right_panel=False + self.active_view=active_view + self.message_box_visible=False +%> +</%def> + <%namespace file="/message.mako" import="render_msg" /> -%if redirect_url: - <script type="text/javascript"> - top.location.href = '${redirect_url}'; - </script> -%endif +<%def name="center_panel()"> + ${body()} +</%def> -%if not redirect_url and message: - ${render_msg( message, status )} -%endif +<%def name="body()"> -%if not trans.user: + %if redirect_url: + <script type="text/javascript"> + top.location.href = '${redirect_url}'; + </script> + %endif + + %if context.get('use_panels'): + <div style="margin: 1em;"> + %else: + <div> + %endif + + %if message: + ${render_msg( message, status )} + %endif + + %if not trans.user: + + ${render_login_form()} + + %if trans.app.config.enable_openid: + <br/> + ${render_openid_form( referer, False, openid_providers )} + %endif + + %endif + + </div> + +</%def> + +<%def name="render_login_form( form_action=None )"> + + <% + if form_action is None: + form_action = h.url_for( controller='user', action='login', use_panels=use_panels ) + %> + %if header: ${header} %endif <div class="toolForm"><div class="toolFormTitle">Login</div> - <form name="login" id="login" action="${h.url_for( controller='user', action='login' )}" method="post" > + <form name="login" id="login" action="${form_action}" method="post" ><div class="form-row"><label>Email address:</label><input type="text" name="email" value="${email}" size="40"/> @@ -36,4 +90,32 @@ </div></form></div> -%endif + +</%def> + +<%def name="render_openid_form( referer, auto_associate, openid_providers )"> + + <div class="toolForm"> + <div class="toolFormTitle">OpenID Login</div> + <form name="openid" id="openid" action="${h.url_for( controller='user', action='openid_auth' )}" method="post" > + <div class="form-row"> + <label>OpenID URL:</label> + <input type="text" name="openid_url" size="60" style="background-image:url('${h.url_for( '/static/images/openid-16x16.gif' )}' ); background-repeat: no-repeat; padding-right: 20px; background-position: 99% 50%;"/> + <input type="hidden" name="webapp" value="${webapp}" size="40"/> + <input type="hidden" name="referer" value="${referer}" size="40"/> + <input type="hidden" name="auto_associate" value="${auto_associate}" size="40"/> + </div> + <div class="form-row"> + Or, authenticate with your <select name="openid_provider"> + %for provider in openid_providers.keys(): + <option>${provider}</option> + %endfor + </select> account. + </div> + <div class="form-row"> + <input type="submit" name="login_button" value="Login"/> + </div> + </form> + </div> + +</%def> --- a/lib/galaxy/eggs/__init__.py +++ b/lib/galaxy/eggs/__init__.py @@ -321,6 +321,7 @@ class GalaxyConfig( object ): "pbs_python": lambda: "pbs" in self.config.get( "app:main", "start_job_runners" ).split(","), "threadframe": lambda: self.config.get( "app:main", "use_heartbeat" ), "guppy": lambda: self.config.get( "app:main", "use_memdump" ), + "python_openid": lambda: self.config.get( "app:main", "enable_openid" ), "GeneTrack": lambda: sys.version_info[:2] >= ( 2, 5 ), "pysam": check_pysam() }.get( egg_name, lambda: True )() --- /dev/null +++ b/templates/user/openid_associate.mako @@ -0,0 +1,75 @@ +<%! + def inherit(context): + if context.get('use_panels'): + if context.get('webapp'): + webapp = context.get('webapp') + else: + webapp = 'galaxy' + return '/webapps/%s/base_panels.mako' % webapp + else: + return '/base.mako' +%> +<%inherit file="${inherit(context)}"/> + +<%def name="init()"> +<% + self.has_left_panel=False + self.has_right_panel=False + self.active_view=active_view + self.message_box_visible=False +%> +</%def> + +<%namespace file="/message.mako" import="render_msg" /> +<%namespace file="login.mako" import="render_login_form" /> +<%namespace file="register.mako" import="render_registration_form" /> + +<%def name="center_panel()"> + ${body()} +</%def> + +<%def name="body()"> + + %if context.get('use_panels'): + <div style="margin: 1em;"> + %else: + <div> + %endif + + %if message: + ${render_msg( message, status )} + %endif + + <h2>OpenID Account Association</h2> + <div> + OpenIDs must be associated with a Galaxy account before they can be used for authentication. This only needs to be done once per OpenID. You may associate your OpenID with an existing Galaxy account, or create a new one. + </div> + <br/> + + %if len( openids ) > 1: + <div> + The following OpenIDs will be associated with the account chosen or created below. + <ul> + %for openid in openids: + <li>${openid.openid}</li> + %endfor + </ul> + </div> + %else: + <div> + The OpenID <strong>${openids[0].openid}</strong> will be associated with the account chosen or created. + </div> + %endif + <br/> + + <% form_action = h.url_for( use_panels=use_panels ) %> + + ${render_login_form( form_action=form_action )} + + <br/> + + ${render_registration_form( form_action=form_action )} + + </div> + +</%def> --- a/templates/user/register.mako +++ b/templates/user/register.mako @@ -22,8 +22,18 @@ ## An admin user may be creating a new user account, in which case we want to display the registration form. ## But if the current user is not an admin user, then don't display the registration form. %if trans.user_is_admin() or not trans.user: + ${render_registration_form()} +%endif + +<%def name="render_registration_form( form_action=None )"> + + <% + if form_action is None: + form_action = h.url_for( controller='user', action='create', admin_view=admin_view ) + %> + <div class="toolForm"> - <form name="registration" id="registration" action="${h.url_for( controller='user', action='create', admin_view=admin_view )}" method="post" > + <form name="registration" id="registration" action="${form_action}" method="post" ><div class="toolFormTitle">Create account</div><div class="form-row"><label>Email address:</label> @@ -83,4 +93,5 @@ </div></form></div> -%endif + +</%def> --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -324,6 +324,10 @@ use_interactive = True # WYSIWYG editor that is very similar to a word processor. #enable_pages = False +# Enable authentication via OpenID. Allows users to log in to their Galaxy +# account by authenticating with an OpenID provider. +#enable_openid = False + # Enable the (experimental! beta!) Web API. Documentation forthcoming. #enable_api = False --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -68,6 +68,15 @@ UserAddress.table = Table( "user_address Column( "deleted", Boolean, index=True, default=False ), Column( "purged", Boolean, index=True, default=False ) ) +UserOpenID.table = Table( "galaxy_user_openid", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, index=True, default=now, onupdate=now ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "openid", TEXT, index=True, unique=True ), + ) + History.table = Table( "history", metadata, Column( "id", Integer, primary_key=True), Column( "create_time", DateTime, default=now ), @@ -968,6 +977,17 @@ assign_mapper( context, UserAddress, Use order_by=desc(UserAddress.table.c.update_time)), ) ) +assign_mapper( context, UserOpenID, UserOpenID.table, + properties=dict( + session=relation( GalaxySession, + primaryjoin=( UserOpenID.table.c.session_id == GalaxySession.table.c.id ), + backref='openids', + order_by=desc( UserOpenID.table.c.update_time ) ), + user=relation( User, + primaryjoin=( UserOpenID.table.c.user_id == User.table.c.id ), + backref='openids', + order_by=desc( UserOpenID.table.c.update_time ) ) ) ) + assign_mapper( context, ValidationError, ValidationError.table ) --- a/templates/display_common.mako +++ b/templates/display_common.mako @@ -85,6 +85,8 @@ class_plural = "Forms" elif a_class == model.RequestType: class_plural = "sequencer configurations" + elif a_class == model.UserOpenID: + class_plural = "OpenIDs" else: class_plural = a_class.__name__ + "s" return class_plural --- a/lib/galaxy/app.py +++ b/lib/galaxy/app.py @@ -60,6 +60,10 @@ class UniverseApplication( object ): self.heartbeat = None self.memdump = None self.memory_usage = None + # Container for OpenID authentication routines + if self.config.enable_openid: + from galaxy.web.framework import openid_manager + self.openid_manager = openid_manager.OpenIDManager( self.config.openid_consumer_cache_path ) # Start the heartbeat process if configured and available if self.config.use_heartbeat: from galaxy.util import heartbeat --- /dev/null +++ b/lib/galaxy/web/framework/openid_manager.py @@ -0,0 +1,53 @@ +""" +Mange the OpenID consumer and related data stores. +""" + +import os, pickle, logging + +from galaxy import eggs +eggs.require( 'python-openid' ) + +import openid +from openid import oidutil +from openid.store import filestore +from openid.consumer import consumer +from openid.extensions import sreg + +log = logging.getLogger( __name__ ) +def oidlog( message, level=0 ): + log.debug( message ) +oidutil.log = oidlog + +class OpenIDManager( object ): + def __init__( self, cache_path ): + 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 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -39,9 +39,11 @@ class Configuration( object ): # Where dataset files are stored self.file_path = resolve_path( kwargs.get( "file_path", "database/files" ), self.root ) self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root ) + 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", "/" ) # web API self.enable_api = string_as_bool( kwargs.get( 'enable_api', False ) ) + self.enable_openid = string_as_bool( kwargs.get( 'enable_openid', False ) ) # Communication with a sequencer self.enable_sequencer_communication = string_as_bool( kwargs.get( 'enable_sequencer_communication', False ) ) # dataset Track files @@ -161,7 +163,17 @@ class Configuration( object ): if not os.path.isdir( path ): raise ConfigurationError("Directory does not exist: %s" % path ) # Create the directories that it makes sense to create - for path in self.file_path, self.new_file_path, self.job_working_directory, self.cluster_files_directory, self.template_cache, self.ftp_upload_dir, self.library_import_dir, self.user_library_import_dir, self.nginx_upload_store, './static/genetrack/plots', os.path.join( self.tool_data_path, 'shared', 'jars' ): + for path in self.file_path, \ + self.new_file_path, \ + self.job_working_directory, \ + self.cluster_files_directory, \ + self.template_cache, \ + self.ftp_upload_dir, \ + self.library_import_dir, \ + self.user_library_import_dir, \ + self.nginx_upload_store, \ + './static/genetrack/plots', \ + os.path.join( self.tool_data_path, 'shared', 'jars' ): if path not in [ None, False ] and not os.path.isdir( path ): try: os.makedirs( path ) --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -1,9 +1,10 @@ """ Contains the user interface in the Universe class """ +from galaxy.web.framework.helpers import time_ago, grids from galaxy.web.base.controller import * from galaxy.model.orm import * -from galaxy import util +from galaxy import util, model import logging, os, string, re, smtplib, socket from random import choice from email.MIMEText import MIMEText @@ -26,48 +27,337 @@ require_login_creation_template = requir VALID_USERNAME_RE = re.compile( "^[a-z0-9\-]+$" ) +OPENID_PROVIDERS = { 'Google' : 'https://www.google.com/accounts/o8/id', + 'Yahoo!' : 'http://yahoo.com', + 'AOL/AIM' : 'http://openid.aol.com', + 'Flickr' : 'http://flickr.com', + 'Launchpad' : 'http://login.launchpad.net', + } + +class UserOpenIDGrid( grids.Grid ): + use_panels = False + title = "OpenIDs linked to your account" + model_class = model.UserOpenID + template = '/user/openid_manage.mako' + default_filter = { "openid" : "All" } + default_sort_key = "-create_time" + columns = [ + grids.TextColumn( "OpenID URL", key="openid" ), + grids.GridColumn( "Created", key="create_time", format=time_ago ), + ] + operations = [ + grids.GridOperation( "Delete", async_compatible=True ), + ] + def build_initial_query( self, trans, **kwd ): + return trans.sa_session.query( self.model_class ).filter( self.model_class.user_id == trans.user.id ) + class User( BaseController, UsesFormDefinitionWidgets ): + user_openid_grid = UserOpenIDGrid() @web.expose def index( self, trans, webapp='galaxy', **kwd ): return trans.fill_template( '/user/index.mako', webapp=webapp ) @web.expose + def openid_auth( self, trans, webapp='galaxy', **kwd ): + if not trans.app.config.enable_openid: + return trans.show_error_message( 'OpenID authentication is not enabled in this instance of Galaxy' ) + message = 'Unspecified failure authenticating via OpenID' + status = kwd.get( 'status', 'done' ) + openid_url = kwd.get( 'openid_url', '' ) + openid_provider = kwd.get( 'openid_provider', '' ) + referer = kwd.get( 'referer', trans.request.referer ) + auto_associate = util.string_as_bool( kwd.get( 'auto_associate', False ) ) + use_panels = util.string_as_bool( kwd.get( 'use_panels', False ) ) + action = 'login' + if auto_associate: + action = 'openid_manage' + if not referer: + referer = url_for( '/' ) + consumer = trans.app.openid_manager.get_consumer( trans ) + process_url = trans.request.base.rstrip( '/' ) + url_for( controller='user', action='openid_process', referer=referer, auto_associate=auto_associate ) + if not openid_url and openid_provider and openid_provider in OPENID_PROVIDERS: + openid_url = OPENID_PROVIDERS[openid_provider] + if openid_url: + request = None + try: + request = consumer.begin( openid_url ) + if request is None: + message = 'No OpenID services are available at %s' % openid_url + except Exception, e: + message = 'Failed to begin OpenID authentication: %s' % str( e ) + if request is not None: + trans.app.openid_manager.add_sreg( trans, request, optional=[ 'nickname', 'email' ] ) + if request.shouldSendRedirect(): + redirect_url = request.redirectURL( + trans.request.base, process_url ) + trans.app.openid_manager.persist_session( trans, consumer ) + trans.response.send_redirect( redirect_url ) + return + 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.response.send_redirect( url_for( controller='user', + action=action, + use_panels=use_panels, + message=message, + status='error' ) ) + @web.expose + def openid_process( self, trans, webapp='galaxy', **kwd ): + if not trans.app.config.enable_openid: + return trans.show_error_message( 'OpenID authentication is not enabled in this instance of Galaxy' ) + auto_associate = util.string_as_bool( kwd.get( 'auto_associate', False ) ) + action = 'login' + if auto_associate: + action = 'openid_manage' + if trans.app.config.bugs_email is not None: + contact = '<a href="mailto:%s">contact support</a>' % trans.app.config.bugs_email + else: + contact = 'contact support' + message = 'Verification failed for an unknown reason. Please contact support for assistance.' + status = 'error' + consumer = trans.app.openid_manager.get_consumer( trans ) + info = consumer.complete( kwd, trans.request.url ) + display_identifier = info.getDisplayIdentifier() + redirect_url = kwd.get( 'referer', url_for( '/' ) ) + if info.status == trans.app.openid_manager.FAILURE and display_identifier: + 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 ) + return trans.response.send_redirect( url_for( controller='user', + action=action, + use_panels=True, + message=message, + status='error' ) ) + elif info.status == trans.app.openid_manager.SUCCESS: + if info.endpoint.canonicalID: + display_identifier = info.endpoint.canonicalID + user_openid = trans.sa_session.query( trans.app.model.UserOpenID ).filter( trans.app.model.UserOpenID.table.c.openid == display_identifier ).first() + if not user_openid: + user_openid = trans.app.model.UserOpenID( session=trans.galaxy_session, openid=display_identifier ) + elif not user_openid.user and user_openid.session.id != trans.galaxy_session.id: + user_openid.session = trans.galaxy_session + elif user_openid.user and not auto_associate: + trans.handle_user_login( user_openid.user, webapp ) + trans.log_event( "User logged in via OpenID: %s" % display_identifier ) + trans.response.send_redirect( redirect_url ) + return + if auto_associate and trans.user: + # The user is already logged in and requested association from + # the user prefs as opposed to using the OpenID form on the + # login page. + if user_openid.user and user_openid.user.id != trans.user.id: + message = "The OpenID <strong>%s</strong> is already associated with another Galaxy account, <strong>%s</strong>. Please disassociate it from that account before attempting to associate it with a new account." % ( display_identifier, user_openid.user.email ) + status = "error" + elif user_openid.user and user_openid.user.id == trans.user.id: + message = "The OpenID <strong>%s</strong> is already associated with your Galaxy account, <strong>%s</strong>." % ( display_identifier, trans.user.email ) + status = "warning" + else: + user_openid.user_id = trans.user.id + trans.sa_session.add( user_openid ) + trans.sa_session.flush() + trans.log_event( "User associated OpenID: %s" % display_identifier ) + message = "The OpenID <strong>%s</strong> has been associated with your Galaxy account, <strong>%s</strong>." % ( display_identifier, trans.user.email ) + status = "done" + trans.response.send_redirect( url_for( controller='user', + action='openid_manage', + use_panels=True, + message=message, + status=status ) ) + return + trans.sa_session.add( user_openid ) + trans.sa_session.flush() + message = "OpenID authentication was successful, but you need to associate your OpenID with a Galaxy account." + sreg_resp = trans.app.openid_manager.get_sreg( info ) + try: + username = sreg_resp.get( 'nickname', '' ) + except AttributeError: + username = '' + try: + email = sreg_resp.get( 'email', '' ) + except AttributeError: + email = '' + trans.response.send_redirect( url_for( controller='user', + action='openid_associate', + use_panels=True, + username=username, + email=email, + message=message, + status='warning' ) ) + elif info.status == trans.app.openid_manager.CANCEL: + message = "Login via OpenID was cancelled by an action at the OpenID provider's site." + status = "warning" + elif info.status == trans.app.openid_manager.SETUP_NEEDED: + if info.setup_url: + return trans.response.send_redirect( info.setup_url ) + else: + message = "Unable to log in via OpenID. Setup at the provider is required before this OpenID can be used. Please visit your provider's site to complete this step." + return trans.response.send_redirect( url_for( controller='user', + action=action, + use_panels=True, + message=message, + status=status ) ) + @web.expose + def openid_associate( self, trans, webapp='galaxy', **kwd ): + if not trans.app.config.enable_openid: + return trans.show_error_message( 'OpenID authentication is not enabled in this instance of Galaxy' ) + use_panels = util.string_as_bool( kwd.get( 'use_panels', False ) ) + message = kwd.get( 'message', '' ) + status = kwd.get( 'status', 'done' ) + email = kwd.get( 'email', '' ) + username = kwd.get( 'username', '' ) + referer = kwd.get( 'referer', trans.request.referer ) + params = util.Params( kwd ) + admin_view = util.string_as_bool( params.get( 'admin_view', False ) ) + openids = trans.galaxy_session.openids + if not openids: + return trans.show_error_message( 'You have not successfully completed an OpenID authentication in this session. You can do so on the <a href="%s">login</a> page.' % url_for( controller='user', action='login', use_panels=use_panels ) ) + elif admin_view: + return trans.show_error_message( 'Associating OpenIDs with accounts cannot be done by administrators.' ) + if kwd.get( 'login_button', False ): + message, status, user, success = self.__validate_login( trans, webapp, **kwd ) + if success: + for openid in openids: + openid.user = user + trans.sa_session.add( openid ) + trans.sa_session.flush() + for openid in openids: + trans.log_event( "User associated OpenID: %s" % openid.openid ) + redirect_url = referer + if not redirect_url: + redirect_url = url_for( '/' ) + trans.response.send_redirect( redirect_url ) + return + if kwd.get( 'create_user_button', False ): + password = kwd.get( 'password', '' ) + confirm = kwd.get( 'confirm', '' ) + subscribe = params.get( 'subscribe', '' ) + subscribe_checked = CheckboxField.is_checked( subscribe ) + error = '' + if not trans.app.config.allow_user_creation and not trans.user_is_admin(): + error = 'User registration is disabled. Please contact your Galaxy administrator for an account.' + else: + # Check email and password validity + error = self.__validate( trans, params, email, password, confirm, username, webapp ) + if not error: + # all the values are valid + message, status, user, success = self.__register( trans, webapp, email, password, username, subscribe_checked, kwd ) + if success: + trans.handle_user_login( user, webapp ) + trans.log_event( "User created a new account" ) + trans.log_event( "User logged in" ) + for openid in openids: + openid.user = user + trans.sa_session.add( openid ) + trans.sa_session.flush() + for openid in openids: + trans.log_event( "User associated OpenID: %s" % openid.openid ) + redirect_url = referer + if not redirect_url: + redirect_url = url_for( '/' ) + trans.response.send_redirect( redirect_url ) + else: + message = error + status = 'error' + if webapp == 'galaxy': + user_info_select, user_info_form, widgets = self.__user_info_ui( trans, **kwd ) + else: + user_info_select = [] + user_info_form = [] + widgets = [] + return trans.fill_template( '/user/openid_associate.mako', + webapp=webapp, + email=email, + password='', + confirm='', + username=username, + header='', + use_panels=use_panels, + redirect_url='', + referer='', + refresh_frames=[], + message=message, + status=status, + active_view="user", + subscribe_checked=False, + admin_view=False, + user_info_select=user_info_select, + user_info_form=user_info_form, + widgets=widgets, + openids=openids ) + @web.expose + @web.require_login( 'manage OpenIDs' ) + def openid_disassociate( self, trans, webapp='galaxy', **kwd ): + if not trans.app.config.enable_openid: + return trans.show_error_message( 'OpenID authentication is not enabled in this instance of Galaxy' ) + params = util.Params( kwd ) + ids = params.get( 'id', None ) + message = params.get( 'message', None ) + status = params.get( 'status', None ) + use_panels = params.get( 'use_panels', False ) + user_openids = [] + if not ids: + message = 'You must select at least one OpenID to disassociate from your Galaxy account.' + status = 'error' + else: + ids = util.listify( params.id ) + for id in ids: + id = trans.security.decode_id( id ) + user_openid = trans.sa_session.query( trans.app.model.UserOpenID ).get( int( id ) ) + if not user_openid or ( trans.user.id != user_openid.user_id ): + message = 'The selected OpenID(s) are not associated with your Galaxy account.' + status = 'error' + user_openids = [] + break + user_openids.append( user_openid ) + if user_openids: + deleted_urls = [] + for user_openid in user_openids: + trans.sa_session.delete( user_openid ) + deleted_urls.append( user_openid.openid ) + trans.sa_session.flush() + for deleted_url in deleted_urls: + trans.log_event( "User disassociated OpenID: %s" % deleted_url ) + message = '%s OpenIDs were disassociated from your Galaxy account.' % len( ids ) + status = 'done' + trans.response.send_redirect( url_for( controller='user', + action='openid_manage', + use_panels=use_panels, + message=message, + status=status ) ) + @web.expose + @web.require_login( 'manage OpenIDs' ) + def openid_manage( self, trans, webapp='galaxy', **kwd ): + if not trans.app.config.enable_openid: + return trans.show_error_message( 'OpenID authentication is not enabled in this instance of Galaxy' ) + use_panels = kwd.get( 'use_panels', False ) + if 'operation' in kwd: + operation = kwd['operation'].lower() + if operation == "delete": + trans.response.send_redirect( url_for( controller='user', + action='openid_disassociate', + use_panels=use_panels, + id=kwd['id'] ) ) + kwd['referer'] = url_for( controller='user', action='openid_manage', use_panels=True ) + kwd['openid_providers'] = OPENID_PROVIDERS + return self.user_openid_grid( trans, **kwd ) + @web.expose def login( self, trans, webapp='galaxy', redirect_url='', refresh_frames=[], **kwd ): referer = kwd.get( 'referer', trans.request.referer ) - use_panels = util.string_as_bool( kwd.get( 'use_panels', True ) ) + use_panels = util.string_as_bool( kwd.get( 'use_panels', False ) ) message = kwd.get( 'message', '' ) status = kwd.get( 'status', 'done' ) header = '' user = None email = kwd.get( 'email', '' ) if kwd.get( 'login_button', False ): - password = kwd.get( 'password', '' ) - referer = kwd.get( 'referer', '' ) if webapp == 'galaxy' and not refresh_frames: if trans.app.config.require_login: refresh_frames = [ 'masthead', 'history', 'tools' ] else: refresh_frames = [ 'masthead', 'history' ] - user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email==email ).first() - if not user: - message = "No such user" - status = 'error' - elif user.deleted: - message = "This account has been marked deleted, contact your Galaxy administrator to restore the account." - status = 'error' - elif user.external: - message = "This account was created for use with an external authentication method, contact your local Galaxy administrator to activate it." - status = 'error' - elif not user.check_password( password ): - message = "Invalid password" - status = 'error' - else: - trans.handle_user_login( user, webapp ) - trans.log_event( "User logged in" ) - message = 'You are now logged in as %s.<br>You can <a target="_top" href="%s">go back to the page you were visiting</a> or <a target="_top" href="%s">go to the home page</a>.' % \ - ( user.email, referer, url_for( '/' ) ) - if trans.app.config.require_login: - message += ' <a target="_top" href="%s">Click here</a> to continue to the home page.' % web.url_for( '/static/welcome.html' ) + message, status, user, success = self.__validate_login( trans, webapp, **kwd ) + if success and referer: redirect_url = referer + elif success: + redirect_url = url_for( '/' ) if not user and trans.app.config.require_login: if trans.app.config.allow_user_creation: header = require_login_creation_template % web.url_for( action='create' ) @@ -83,7 +373,37 @@ class User( BaseController, UsesFormDefi refresh_frames=refresh_frames, message=message, status=status, + openid_providers=OPENID_PROVIDERS, active_view="user" ) + def __validate_login( self, trans, webapp='galaxy', **kwd ): + message = kwd.get( 'message', '' ) + status = kwd.get( 'status', 'done' ) + email = kwd.get( 'email', '' ) + password = kwd.get( 'password', '' ) + referer = kwd.get( 'referer', trans.request.referer ) + success = False + user = trans.sa_session.query( trans.app.model.User ).filter( trans.app.model.User.table.c.email==email ).first() + if not user: + message = "No such user" + status = 'error' + elif user.deleted: + message = "This account has been marked deleted, contact your Galaxy administrator to restore the account." + status = 'error' + elif user.external: + message = "This account was created for use with an external authentication method, contact your local Galaxy administrator to activate it." + status = 'error' + elif not user.check_password( password ): + message = "Invalid password" + status = 'error' + else: + trans.handle_user_login( user, webapp ) + trans.log_event( "User logged in" ) + message = 'You are now logged in as %s.<br>You can <a target="_top" href="%s">go back to the page you were visiting</a> or <a target="_top" href="%s">go to the home page</a>.' % \ + ( user.email, referer, url_for( '/' ) ) + if trans.app.config.require_login: + message += ' <a target="_top" href="%s">Click here</a> to continue to the home page.' % web.url_for( '/static/welcome.html' ) + success = True + return ( message, status, user, success ) @web.expose def logout( self, trans, webapp='galaxy' ): if webapp == 'galaxy': @@ -138,54 +458,25 @@ class User( BaseController, UsesFormDefi error = self.__validate( trans, params, email, password, confirm, username, webapp ) if not error: # all the values are valid - user = trans.app.model.User( email=email ) - user.set_password_cleartext( password ) - user.username = username - trans.sa_session.add( user ) - trans.sa_session.flush() - trans.app.security_agent.create_private_user_role( user ) - message = 'Now logged in as %s.<br><a target="_top" href="%s">Return to the home page.</a>' % ( user.email, url_for( '/' ) ) - if webapp == 'galaxy': - # We set default user permissions, before we log in and set the default history permissions - trans.app.security_agent.user_set_default_permissions( user, - default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) - # save user info - self.__save_user_info( trans, user, action='create', new_user=True, **kwd ) - if subscribe_checked: - # subscribe user to email list - if trans.app.config.smtp_server is None: - error = "Now logged in as " + user.email + ". However, subscribing to the mailing list has failed because mail is not configured for this Galaxy instance." - else: - msg = MIMEText( 'Join Mailing list.\n' ) - to = msg[ 'To' ] = trans.app.config.mailing_join_addr - frm = msg[ 'From' ] = email - msg[ 'Subject' ] = 'Join Mailing List' - try: - s = smtplib.SMTP() - s.connect( trans.app.config.smtp_server ) - s.sendmail( frm, [ to ], msg.as_string() ) - s.close() - except: - error = "Now logged in as " + user.email + ". However, subscribing to the mailing list has failed." - if not error and not admin_view: - # The handle_user_login() method has a call to the history_set_default_permissions() method - # (needed when logging in with a history), user needs to have default permissions set before logging in - trans.handle_user_login( user, webapp ) - trans.log_event( "User created a new account" ) - trans.log_event( "User logged in" ) - elif not error: - trans.response.send_redirect( web.url_for( controller='admin', - action='users', - message='Created new user account (%s)' % user.email, - status='done' ) ) - elif not admin_view: + message, status, user, success = self.__register( trans, webapp, email, password, username, subscribe_checked, kwd ) + if success and not admin_view and webapp != 'galaxy': # Must be logging into the community space webapp trans.handle_user_login( user, webapp ) - if not error: - redirect_url = referer - if error: - message=error - status='error' + redirect_url = referer + if success and not admin_view: + # The handle_user_login() method has a call to the history_set_default_permissions() method + # (needed when logging in with a history), user needs to have default permissions set before logging in + trans.handle_user_login( user, webapp ) + trans.log_event( "User created a new account" ) + trans.log_event( "User logged in" ) + elif success: + trans.response.send_redirect( web.url_for( controller='admin', + action='users', + message='Created new user account (%s)' % user.email, + status='done' ) ) + else: + message = error + status = 'error' if webapp == 'galaxy': user_info_select, user_info_form, widgets = self.__user_info_ui( trans, **kwd ) else: @@ -209,6 +500,57 @@ class User( BaseController, UsesFormDefi refresh_frames=refresh_frames, message=message, status=status ) + def __register( self, trans, webapp, email, password, username, subscribe_checked, kwd ): + status = kwd.get( 'status', 'done' ) + admin_view = util.string_as_bool( kwd.get( 'admin_view', False ) ) + user = trans.app.model.User( email=email ) + user.set_password_cleartext( password ) + user.username = username + trans.sa_session.add( user ) + trans.sa_session.flush() + trans.app.security_agent.create_private_user_role( user ) + error = '' + if webapp == 'galaxy': + # We set default user permissions, before we log in and set the default history permissions + trans.app.security_agent.user_set_default_permissions( user, + default_access_private=trans.app.config.new_user_dataset_access_role_default_private ) + # save user info + self.__save_user_info( trans, user, action='create', new_user=True, **kwd ) + if subscribe_checked: + # subscribe user to email list + if trans.app.config.smtp_server is None: + error = "Now logged in as " + user.email + ". However, subscribing to the mailing list has failed because mail is not configured for this Galaxy instance." + else: + msg = MIMEText( 'Join Mailing list.\n' ) + to = msg[ 'To' ] = trans.app.config.mailing_join_addr + frm = msg[ 'From' ] = email + msg[ 'Subject' ] = 'Join Mailing List' + try: + s = smtplib.SMTP() + s.connect( trans.app.config.smtp_server ) + s.sendmail( frm, [ to ], msg.as_string() ) + s.close() + except: + error = "Now logged in as " + user.email + ". However, subscribing to the mailing list has failed." + if not error and not admin_view: + # The handle_user_login() method has a call to the history_set_default_permissions() method + # (needed when logging in with a history), user needs to have default permissions set before logging in + trans.handle_user_login( user, webapp ) + trans.log_event( "User created a new account" ) + trans.log_event( "User logged in" ) + elif not error: + trans.response.send_redirect( web.url_for( controller='admin', + action='users', + message='Created new user account (%s)' % user.email, + status='done' ) ) + if error: + message = error + status = 'error' + success = False + else: + message = 'Now logged in as %s.<br><a target="_top" href="%s">Return to the home page.</a>' % ( user.email, url_for( '/' ) ) + success = True + return ( message, status, user, success ) def __save_user_info(self, trans, user, action, new_user=True, **kwd): ''' This method saves the user information for new users as well as editing user --- a/templates/grid_base.mako +++ b/templates/grid_base.mako @@ -29,12 +29,19 @@ ## <%def name="center_panel()"> - ${make_grid( grid )} + ${self.grid_body( grid )} </%def> ## Render the grid's basic elements. Each of these elements can be subclassed. <%def name="body()"> - ${make_grid( grid )} + ${self.grid_body( grid )} +</%def> + +## Because body() is special and always exists even if not explicitly defined, +## it's not possible to override body() in the topmost template in the chain. +## Because of this, override grid_body() instead. +<%def name="grid_body( grid )"> + ${self.make_grid( grid )} </%def><%def name="title()">${grid.title}</%def>