def logoutChild(self, session_id): """expire a subsession""" if not self.logged_in: # XXX raise an error? raise koji.AuthError("Not logged in") update = """UPDATE sessions SET expired=TRUE,exclusive=NULL WHERE id = %(session_id)i AND master = %(master)i""" master = self.id c = context.cnx.cursor() c.execute(update, locals()) context.cnx.commit()
def sslLogin(self, proxyuser=None): if self.logged_in: raise koji.AuthError("Already logged in") if context.environ.get('REMOTE_USER'): username = context.environ.get('REMOTE_USER') client_dn = username authtype = koji.AUTHTYPE_GSSAPI else: if context.environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': raise koji.AuthError('could not verify client: %s' % context.environ.get('SSL_CLIENT_VERIFY')) name_dn_component = context.opts.get('DNUsernameComponent', 'CN') username = context.environ.get('SSL_CLIENT_S_DN_%s' % name_dn_component) if not username: raise koji.AuthError('unable to get user information (%s) from client certificate' % name_dn_component) client_dn = context.environ.get('SSL_CLIENT_S_DN') authtype = koji.AUTHTYPE_SSL if proxyuser: proxy_dns = [dn.strip() for dn in context.opts.get('ProxyDNs', '').split('|')] if client_dn in proxy_dns: # the SSL-authenticated user authorized to login other users username = proxyuser else: raise koji.AuthError('%s is not authorized to login other users' % client_dn) user_id = self.getUserId(username) if not user_id: if context.opts.get('LoginCreatesUser'): user_id = self.createUser(username) else: raise koji.AuthError('Unknown user: %s' % username) self.checkLoginAllowed(user_id) hostip = self.get_remote_ip() sinfo = self.createSession(user_id, hostip, authtype) return sinfo
def createUserFromKerberos(self, krb_principal): """Create a new user, based on the Kerberos principal. Their username will be everything before the "@" in the principal. Return the ID of the newly created user.""" atidx = krb_principal.find('@') if atidx == -1: raise koji.AuthError('invalid Kerberos principal: %s' % krb_principal) user_name = krb_principal[:atidx] # check if user already exists c = context.cnx.cursor() q = """SELECT krb_principal FROM users WHERE name = %(user_name)s""" c.execute(q, locals()) r = c.fetchone() if not r: return self.createUser(user_name, krb_principal=krb_principal) else: existing_user_krb = r[0] if existing_user_krb is not None: raise koji.AuthError('user %s already associated with other Kerberos principal: %s' % (user_name, existing_user_krb)) return self.setKrbPrincipal(user_name, krb_principal)
def setKrbPrincipal(self, name, krb_principal): usertype = koji.USERTYPES['NORMAL'] status = koji.USER_STATUS['NORMAL'] update = """UPDATE users SET krb_principal = %(krb_principal)s WHERE name = %(name)s AND usertype = %(usertype)i AND status = %(status)i RETURNING users.id""" cursor = context.cnx.cursor() cursor.execute(update, locals()) r = cursor.fetchall() if len(r) != 1: context.cnx.rollback() raise koji.AuthError('could not automatically associate Kerberos Principal with existing user %s' % name) else: context.cnx.commit() return r[0][0]
def logout(self): """expire a login session""" if not self.logged_in: #XXX raise an error? raise koji.AuthError("Not logged in") update = """UPDATE sessions SET expired=TRUE,exclusive=NULL WHERE id = %(id)i OR master = %(id)i""" #note we expire subsessions as well c = context.cnx.cursor() c.execute(update, {'id': self.id}) context.cnx.commit() self.logged_in = False
def setKrbPrincipal(self, name, krb_principal, krb_princ_check=True): if krb_princ_check: self.checkKrbPrincipal(krb_principal) select = """SELECT id FROM users WHERE %s""" if isinstance(name, six.integer_types): user_condition = 'id = %(name)i' else: user_condition = 'name = %(name)s' select = select % user_condition cursor = context.cnx.cursor() cursor.execute(select, locals()) r = cursor.fetchone() if not r: context.cnx.rollback() raise koji.AuthError('No such user: %s' % name) else: user_id = r[0] insert = """INSERT INTO user_krb_principals (user_id, krb_principal) VALUES (%(user_id)i, %(krb_principal)s)""" cursor.execute(insert, locals()) context.cnx.commit() return user_id
def __init__(self, args=None, hostip=None): self.logged_in = False self.id = None self.master = None self.key = None self.user_id = None self.authtype = None self.hostip = None self.user_data = {} self.message = '' self.exclusive = False self.lockerror = None self.callnum = None # we look up perms, groups, and host_id on demand, see __getattr__ self._perms = None self._groups = None self._host_id = '' #get session data from request if args is None: environ = getattr(context, 'environ', {}) args = environ.get('QUERY_STRING', '') if not args: self.message = 'no session args' return args = urlparse.parse_qs(args, strict_parsing=True) hostip = self.get_remote_ip(override=hostip) try: id = int(args['session-id'][0]) key = args['session-key'][0] except KeyError as field: raise koji.AuthError('%s not specified in session args' % field) try: callnum = args['callnum'][0] except: callnum = None #lookup the session c = context.cnx.cursor() fields = { 'authtype': 'authtype', 'callnum': 'callnum', 'exclusive': 'exclusive', 'expired': 'expired', 'master': 'master', 'start_time': 'start_time', 'update_time': 'update_time', 'EXTRACT(EPOCH FROM start_time)': 'start_ts', 'EXTRACT(EPOCH FROM update_time)': 'update_ts', 'user_id': 'user_id', } # sort for stability (unittests) fields, aliases = list(zip(*list(sorted(fields.items(), key=lambda x: x[1])))) q = """ SELECT %s FROM sessions WHERE id = %%(id)i AND key = %%(key)s AND hostip = %%(hostip)s FOR UPDATE """ % ",".join(fields) c.execute(q, locals()) row = c.fetchone() if not row: raise koji.AuthError('Invalid session or bad credentials') session_data = dict(list(zip(aliases, row))) #check for expiration if session_data['expired']: raise koji.AuthExpired('session "%i" has expired' % id) #check for callnum sanity if callnum is not None: try: callnum = int(callnum) except (ValueError, TypeError): raise koji.AuthError("Invalid callnum: %r" % callnum) lastcall = session_data['callnum'] if lastcall is not None: if lastcall > callnum: raise koji.SequenceError("%d > %d (session %d)" \ % (lastcall, callnum, id)) elif lastcall == callnum: #Some explanation: #This function is one of the few that performs its own commit. #However, our storage of the current callnum is /after/ that #commit. This means the the current callnum only gets committed if #a commit happens afterward. #We only schedule a commit for dml operations, so if we find the #callnum in the db then a previous attempt succeeded but failed to #return. Data was changed, so we cannot simply try the call again. method = getattr(context, 'method', 'UNKNOWN') if method not in RetryWhitelist: raise koji.RetryError( "unable to retry call %d (method %s) for session %d" \ % (callnum, method, id)) # read user data #historical note: # we used to get a row lock here as an attempt to maintain sanity of exclusive # sessions, but it was an imperfect approach and the lock could cause some # performance issues. fields = ('name', 'status', 'usertype') q = """SELECT %s FROM users WHERE id=%%(user_id)s""" % ','.join(fields) c.execute(q, session_data) user_data = dict(list(zip(fields, c.fetchone()))) if user_data['status'] != koji.USER_STATUS['NORMAL']: raise koji.AuthError('logins by %s are not allowed' % user_data['name']) #check for exclusive sessions if session_data['exclusive']: #we are the exclusive session for this user self.exclusive = True else: #see if an exclusive session exists q = """SELECT id FROM sessions WHERE user_id=%(user_id)s AND "exclusive" = TRUE AND expired = FALSE""" #should not return multiple rows (unique constraint) c.execute(q, session_data) row = c.fetchone() if row: (excl_id,) = row if excl_id == session_data['master']: #(note excl_id cannot be None) #our master session has the lock self.exclusive = True else: #a session unrelated to us has the lock self.lockerror = "User locked by another session" # we don't enforce here, but rely on the dispatcher to enforce # if appropriate (otherwise it would be impossible to steal # an exclusive session with the force option). # update timestamp q = """UPDATE sessions SET update_time=NOW() WHERE id = %(id)i""" c.execute(q, locals()) #save update time context.cnx.commit() #update callnum (this is deliberately after the commit) #see earlier note near RetryError if callnum is not None: q = """UPDATE sessions SET callnum=%(callnum)i WHERE id = %(id)i""" c.execute(q, locals()) # record the login data self.id = id self.key = key self.hostip = hostip self.callnum = callnum self.user_id = session_data['user_id'] self.authtype = session_data['authtype'] self.master = session_data['master'] self.session_data = session_data self.user_data = user_data self.logged_in = True
def krbLogin(self, krb_req, proxyuser=None): """Authenticate the user using the base64-encoded AP_REQ message in krb_req. If proxyuser is not None, log in that user instead of the user associated with the Kerberos principal. The principal must be an authorized "proxy_principal" in the server config.""" if self.logged_in: raise koji.AuthError("Already logged in") if krbV is None: # python3 is not supported raise koji.AuthError("krbV module not installed") if not (context.opts.get('AuthPrincipal') and context.opts.get('AuthKeytab')): raise koji.AuthError('not configured for Kerberos authentication') ctx = krbV.default_context() srvprinc = krbV.Principal(name=context.opts.get('AuthPrincipal'), context=ctx) srvkt = krbV.Keytab(name=context.opts.get('AuthKeytab'), context=ctx) ac = krbV.AuthContext(context=ctx) ac.flags = krbV.KRB5_AUTH_CONTEXT_DO_SEQUENCE|krbV.KRB5_AUTH_CONTEXT_DO_TIME conninfo = self.getConnInfo() ac.addrs = conninfo # decode and read the authentication request req = base64.decodestring(krb_req) ac, opts, sprinc, ccreds = ctx.rd_req(req, server=srvprinc, keytab=srvkt, auth_context=ac, options=krbV.AP_OPTS_MUTUAL_REQUIRED) cprinc = ccreds[2] # Successfully authenticated via Kerberos, now log in if proxyuser: proxyprincs = [princ.strip() for princ in context.opts.get('ProxyPrincipals', '').split(',')] if cprinc.name in proxyprincs: login_principal = proxyuser else: raise koji.AuthError( 'Kerberos principal %s is not authorized to log in other users' % cprinc.name) else: login_principal = cprinc.name user_id = self.getUserIdFromKerberos(login_principal) if not user_id: user_id = self.getUserId(login_principal) if not user_id: # Only do autocreate if we also couldn't find by username AND the proxyuser # looks like a krb5 principal if context.opts.get('LoginCreatesUser') and '@' in login_principal: user_id = self.createUserFromKerberos(login_principal) else: raise koji.AuthError('Unknown Kerberos principal: %s' % login_principal) self.checkLoginAllowed(user_id) hostip = self.get_remote_ip() sinfo = self.createSession(user_id, hostip, koji.AUTHTYPE_KERB) # encode the reply rep = ctx.mk_rep(auth_context=ac) rep_enc = base64.encodestring(rep) # encrypt and encode the login info sinfo_priv = ac.mk_priv('%(session-id)s %(session-key)s' % sinfo) sinfo_enc = base64.encodestring(sinfo_priv) return (rep_enc, sinfo_enc, conninfo)
def sslLogin(self, proxyuser=None): if self.logged_in: raise koji.AuthError("Already logged in") # we use REMOTE_USER to identify user if context.environ.get('REMOTE_USER'): # it is kerberos principal rather than user's name. username = context.environ.get('REMOTE_USER') client_dn = username authtype = koji.AUTHTYPE_GSSAPI else: if context.environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': raise koji.AuthError('could not verify client: %s' % context.environ.get('SSL_CLIENT_VERIFY')) name_dn_component = context.opts.get('DNUsernameComponent', 'CN') username = context.environ.get('SSL_CLIENT_S_DN_%s' % name_dn_component) if not username: raise koji.AuthError( 'unable to get user information (%s) from client certificate' % name_dn_component) client_dn = context.environ.get('SSL_CLIENT_S_DN') authtype = koji.AUTHTYPE_SSL if proxyuser: if authtype == koji.AUTHTYPE_GSSAPI: delimiter = ',' proxy_opt = 'ProxyPrincipals' else: delimiter = '|' proxy_opt = 'ProxyDNs' proxy_dns = [dn.strip() for dn in context.opts.get(proxy_opt, '').split(delimiter)] # backwards compatible for GSSAPI. # in old way, proxy user whitelist is ProxyDNs. # TODO: this should be removed in future release if authtype == koji.AUTHTYPE_GSSAPI and not context.opts.get( 'DisableGSSAPIProxyDNFallback', False): proxy_dns += [dn.strip() for dn in context.opts.get('ProxyDNs', '').split('|')] if client_dn in proxy_dns: # the user authorized to login other users username = proxyuser else: raise koji.AuthError('%s is not authorized to login other users' % client_dn) if authtype == koji.AUTHTYPE_GSSAPI and '@' in username: user_id = self.getUserIdFromKerberos(username) else: user_id = self.getUserId(username) if not user_id: if context.opts.get('LoginCreatesUser'): if authtype == koji.AUTHTYPE_GSSAPI and '@' in username: user_id = self.createUserFromKerberos(username) else: user_id = self.createUser(username) else: raise koji.AuthError('Unknown user: %s' % username) self.checkLoginAllowed(user_id) hostip = self.get_remote_ip() sinfo = self.createSession(user_id, hostip, authtype) return sinfo
def sslLogin(self, proxyuser=None, proxyauthtype=None): """Login into brew via SSL. proxyuser name can be specified and if it is allowed in the configuration file then connection is allowed to login as that user. By default we assume that proxyuser is coming via same authentication mechanism but proxyauthtype can be set to koji.AUTHTYPE_* value for different handling. Typical case is proxying kerberos user via web ui which itself is authenticated via SSL certificate. (See kojiweb for usage). proxyauthtype is working only if AllowProxyAuthType option is set to 'On' in the hub.conf """ if self.logged_in: raise koji.AuthError("Already logged in") # we use REMOTE_USER to identify user if context.environ.get('REMOTE_USER'): # it is kerberos principal rather than user's name. username = context.environ.get('REMOTE_USER') client_dn = username authtype = koji.AUTHTYPE_GSSAPI else: if context.environ.get('SSL_CLIENT_VERIFY') != 'SUCCESS': raise koji.AuthError('could not verify client: %s' % context.environ.get('SSL_CLIENT_VERIFY')) name_dn_component = context.opts.get('DNUsernameComponent', 'CN') username = context.environ.get('SSL_CLIENT_S_DN_%s' % name_dn_component) if not username: raise koji.AuthError( 'unable to get user information (%s) from client certificate' % name_dn_component) client_dn = context.environ.get('SSL_CLIENT_S_DN') authtype = koji.AUTHTYPE_SSL if proxyuser: if authtype == koji.AUTHTYPE_GSSAPI: delimiter = ',' proxy_opt = 'ProxyPrincipals' else: delimiter = '|' proxy_opt = 'ProxyDNs' proxy_dns = [ dn.strip() for dn in context.opts.get(proxy_opt, '').split(delimiter) ] # backwards compatible for GSSAPI. # in old way, proxy user whitelist is ProxyDNs. # TODO: this should be removed in future release if authtype == koji.AUTHTYPE_GSSAPI and not context.opts.get( 'DisableGSSAPIProxyDNFallback', False): proxy_dns += [ dn.strip() for dn in context.opts.get('ProxyDNs', '').split('|') ] if client_dn in proxy_dns: # the user authorized to login other users username = proxyuser else: raise koji.AuthError( '%s is not authorized to login other users' % client_dn) # in this point we can continue with proxied user in same way as if it is not proxied if proxyauthtype is not None: if not context.opts['AllowProxyAuthType']: raise koji.AuthError( "Proxy must use same auth mechanism as hub (behaviour " "can be overriden via AllowProxyAuthType hub option)") if proxyauthtype not in (koji.AUTHTYPE_GSSAPI, koji.AUTHTYPE_SSL): raise koji.AuthError( "Proxied authtype %s is not valid for sslLogin" % proxyauthtype) authtype = proxyauthtype if authtype == koji.AUTHTYPE_GSSAPI and '@' in username: user_id = self.getUserIdFromKerberos(username) else: user_id = self.getUserId(username) if not user_id: if context.opts.get('LoginCreatesUser'): if authtype == koji.AUTHTYPE_GSSAPI and '@' in username: user_id = self.createUserFromKerberos(username) else: user_id = self.createUser(username) else: raise koji.AuthError('Unknown user: %s' % username) self.checkLoginAllowed(user_id) hostip = self.get_remote_ip() sinfo = self.createSession(user_id, hostip, authtype) return sinfo