def post(self): parser = reqparse.RequestParser() parser.add_argument('username', type=str, required=True, help='Username') parser.add_argument('org', type=str, required=True, help='Org for user membership') parser.add_argument('email', type=str, required=True, help='Email address for user') parser.add_argument('parentuser', type=str, required=False, help='Parent user in form of user@org') args = parser.parse_args() try: session = CassandraCluster.getSession( config['cassandra']['auth_keyspace']) checkUserExistsQuery = CassandraCluster.getPreparedStatement( """ SELECT username, org FROM users WHERE org = ? AND username = ? """, keyspace=session.keyspace) results = session.execute(checkUserExistsQuery, (args['org'], args['username']) ).current_rows if len(results) == 0: checkOrgSetting = CassandraCluster.getPreparedStatement( """ SELECT value FROM orgsettings WHERE org = ? AND setting = ? """, keyspace=session.keyspace) results = session.execute(checkOrgSetting, (args['org'], 'registrationOpen') ).current_rows if len(results) == 0 or results[0].value == 0: return {'Message': 'Cannot create user "%s@%s". Organization is ' % (args['username'], args['org']) + 'closed for registrations or does not exist.'}, 400 else: createUserQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO users ( org, username, email, parentuser, createdate ) VALUES ( ?, ?, ?, ?, dateof(now()) ) """, keyspace=session.keyspace) createUserQuery.consistency_level = ConsistencyLevel.QUORUM session.execute(createUserQuery, (args['org'], args['username'], args['email'], args['parentuser']) ) else: return {'Message': 'Cannot create user "%s@%s", as it already exists.' % (args['username'], args['org'])}, 400 except Exception as e: log.error('Exception in Users.Post: %s' % (e,)) return {'ServerError': 500, 'Message': 'There was an error fulfiling your request'}, 500 return {'Message': 'User "%s@%s" created.' % (args['username'], args['org'])}
def createUser(org, username, email, parentuser, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Create a user in the authdb.users table. :org: Name of organization :username: Name of user :email: Email address for the user :parentuser: Parent user for this user (in the form of user@org) or None :consistency: Cassandra ConsistencyLevel (default LOCAL_QUORUM) """ createUserQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO users ( org, username, email, parentuser, createdate ) VALUES ( ?, ?, ?, ?, dateof(now()) ) """, keyspace=session.keyspace) createUserQuery.consistency_level = consistency return session.execute(createUserQuery, (org, username, email, parentuser))
def createPasswordReset(org, username, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Create a password reset in authdb.userpasswordresets table. :org: Name of organization :username: Name of user """ createPasswordResetQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO userpasswordresets ( org, username, requestdate, resetid ) VALUES ( ?, ?, dateof(now()), ? ) """, keyspace=session.keyspace) createPasswordResetQuery.consistency_level = consistency resetid = uuid.uuid4() try: session.execute(createPasswordResetQuery, (org, username, resetid)) return resetid except Exception as e: log.error("Caught exception in AuthDB.createPasswordReset: %s" % (e,)) return False
def createUserSessionKey(org, username, sessionId, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Create a session key record in the usersessionkeys table for the given user session :org: Name of organization for the user :username: Name of the user :sessionid: ID of the session to create a key for """ charList = (list(range(48, 58)) + # Numbers list(range(65, 91)) + # Uppercase list(range(97, 123))) # Lowercase sysrand = SystemRandom() sessionKey = ''.join( chr(sysrand.choice(charList)) for i in range(64)) try: createUserSessionKeyQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO usersessionkeys ( sessionkey, org, username, sessionid ) VALUES ( ?, ?, ?, ? ) """, keyspace=session.keyspace) createUserSessionKeyQuery.consistency_level = consistency session.execute(createUserSessionKeyQuery, (sessionKey, org, username, sessionId)) return sessionKey except Exception as e: log.critical("Exception in AuthDB.createUserSessionKey: %s" % (e,))
def createUserSession(org, username, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Create a session record in the usersessions table for the given user :org: Name of organization for the user :username: Name of the user """ sessionId = uuid.uuid4() try: createUserSessionQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO usersessions ( org, username, sessionid, startdate, lastupdate ) VALUES ( ?, ?, ?, dateof(now()), dateof(now()) ) """, keyspace=session.keyspace) createUserSessionQuery.consistency_level = consistency session.execute(createUserSessionQuery, (org, username, sessionId)) return sessionId except Exception as e: log.critical("Exception in AuthDB.createUserSession: %s" % (e,))
def setPassword(org, username, passwordHash, salt, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Update/set user's password with given hash and salt :org: Name of org the user is in :username: Name of user :passwordHash: The Argon2 hash of the salted password :salt: Salt used to generate the hash :consistency: Cassandra consistency level. Defaults to LOCAL_QUORUM. """ setPasswordQuery = CassandraCluster.getPreparedStatement( """ UPDATE users SET hash = ?, salt = ? WHERE org = ? AND username = ? """, keyspace=session.keyspace) setPasswordQuery.consistency_level = consistency session.execute(setPasswordQuery, (passwordHash, salt, org, username))
def getUserSessions(org, username, session=None): getUserSessionQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM usersessions WHERE org = ? AND username = ? """, keyspace=session.keyspace) return session.execute(getUserSessionQuery, (org, username)).current_rows
def getOrg(org, session=None): """ Retrieve an org from the authdb.orgs table :org: Name of organization the user belongs to """ getOrgQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM orgs WHERE org = ? """, keyspace=session.keyspace) return session.execute(getOrgQuery, (org,))
def getGlobalSetting(setting, session=None): """ Get a setting/property for system from the authdb.globalsettings table. :setting: Setting/property name """ getGlobalSettingQuery = CassandraCluster.getPreparedStatement( """ SELECT value FROM globalsettings WHERE setting = ? """, keyspace=session.keyspace) return session.execute(getGlobalSettingQuery, (setting,))
def getUser(org, username, session=None): """ Retrieve a user from the authdb.users table :org: Name of organization the user belongs to :username: Name of the user """ getUserQuery = CassandraCluster.getPreparedStatement( """ SELECT username, org, parentuser, createdate FROM users WHERE org = ? AND username = ? """, keyspace=session.keyspace) return session.execute(getUserQuery, (org, username))
def getPasswordReset(org, username, session=None): """ Retrieve a password reset request from the authdb.userpasswordresets table :org: Name of organization the user belongs to :username: Name of the user """ getPasswordResetQuery = CassandraCluster.getPreparedStatement( """ SELECT username, org, requestdate, resetid FROM userpasswordresets WHERE org = ? AND username = ? """, keyspace=session.keyspace) return session.execute(getPasswordResetQuery, (org, username))
def getOrgSetting(org, setting, session=None): """ Get a setting/property for an organization from the authdb.orgsettings table. :org: Name of organization :setting: Setting/property name """ checkOrgSetting = CassandraCluster.getPreparedStatement( """ SELECT value FROM orgsettings WHERE org = ? AND setting = ? """, keyspace=session.keyspace) return session.execute(checkOrgSetting, (org, setting))
def waitForMigrationCompletion(session): """ Wait for a migration task running on another node to complete :keyspace: Keyspace to wait to complete migrating """ migrationsRunning = True migrationsFailedOrStalled = False migrationRequestsQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM schema_migration_requests """, keyspace=session.keyspace) log.info('Waiting for migrations to complete on "%s"' % (session.keyspace,)) while migrationsRunning: time.sleep(0.5) migrationRequests = session.execute(migrationRequestsQuery)\ .current_rows if len(migrationRequests) == 0: # No Migrations running/requested, we're finished waiting migrationsRunning = False break # Check for stale or failed migrations staleTime = datetime.datetime.now() - datetime.timedelta(minutes=1) for req in migrationRequests: if (req.failed or (req.inprogress and req.lastupdate < staleTime)): # We found a failed or stale request (that had started), # we should re-request a migration migrationsRunning = False migrationsFailedOrStalled = True log.info('Finished waiting for migration of "%s"' % (session.keyspace,)) if migrationsFailedOrStalled: log.warning('Detected failed migration of "%s", ' % (session.keyspace,) + 'will re-request migration') DB.requestMigration(session)
def updateReq(session, reqid): """ Update the lastupdate time on a reqid :keyspace: Keyspace the reqid applies to :reqid: ID of the request to be updated """ t = datetime.datetime.now() reqUpdateQuery = CassandraCluster.getPreparedStatement( """ UPDATE schema_migration_requests SET lastupdate = ? WHERE reqid = ? """, keyspace=session.keyspace) session.execute(reqUpdateQuery, (t, reqid))
def setGlobalSetting(setting, value, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Set a global setting/property in the authdb.globalsettings table :setting: Setting/property name :value: Value of the setting :consistency: Cassandra consistency level. Defaults to LOCAL_QUORUM. """ setGlobalSettingQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO globalsettings (setting, value) VALUES (?, ?) """, keyspace=session.keyspace) setGlobalSettingQuery.consistency_level = consistency session.execute(setGlobalSettingQuery, (setting, value))
def getUserSalt(org, username, session=None): """ Retrieve a user's salt from the authdb.users table :org: Name of organization the user belongs to :username: Name of the user """ getUserSaltQuery = CassandraCluster.getPreparedStatement( """ SELECT salt FROM users WHERE org = ? AND username = ? """, keyspace=session.keyspace) res = session.execute(getUserSaltQuery, (org, username)).current_rows if len(res) > 0: return res[0].salt else: return None
def createOrg(org, parentorg, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Create an organization in the authdb.orgs table. :org: Name of the organization :parentorg: Parent organization for this organization :consistency: Cassandra consistency level. Defaults to LOCAL_QUORUM. """ createOrgQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO orgs (org, parentorg) VALUES (?, ?) """, keyspace=session.keyspace) createOrgQuery.consistency_level = consistency session.execute(createOrgQuery, (org,))
def deletePasswordReset(org, username, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Delete/remove a password reset request for a user :org: Name of organization the user belongs to :username: Name of user :consistency: Cassandra ConsistencyLevel (default LOCAL_QUORUM) """ deletePasswordResetQuery = CassandraCluster.getPreparedStatement( """ DELETE FROM userpasswordresets WHERE org = ? AND username = ? """, keyspace=session.keyspace) deletePasswordResetQuery.consistency_level = consistency session.execute(deletePasswordResetQuery, (org, username))
def getUserSessionByKey(sessionKey, session=None): """ Get session record using a session key :sessionKey: 64-character session key for the session """ getUserSessionByKeyQuery = CassandraCluster.getPreparedStatement( """ SELECT sessionid, username, org FROM usersessionkeys WHERE sessionkey = ? """, keyspace=session.keyspace) res = session.execute(getUserSessionByKeyQuery, (sessionKey,))\ .current_rows numRows = len(res) if numRows == 1: return AuthDB.getUserSession(res[0].org, res[0].username, res[0].sessionid) elif numRows == 0: return None elif numRows > 1: raise ValueError('Multiple sessions returned by key')
def tableExists(keyspace, table): """ Determine if the given table exists in the keyspace :keyspace: The keyspace to check for the table :table: Table to check for """ if keyspace is None or table is None: return False session = CassandraCluster.getSession('system') lookuptable = CassandraCluster.getPreparedStatement(""" SELECT columnfamily_name FROM schema_columnfamilies WHERE keyspace_name=? and columnfamily_name=? """, keyspace=session.keyspace) table_count = len(session.execute(lookuptable, (keyspace, table)) .current_rows) return table_count == 1
def getUserSession(org, username, sessionId, session=None): """ Get Session record :org: Name of user's organization :username: Name of user :sessionId: ID of session to lookup """ getUserSessionQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM usersessions WHERE org = ? AND username = ? AND sessionid = ? """, keyspace=session.keyspace) res = session.execute(getUserSessionQuery, (org, username, sessionId))\ .current_rows if len(res) > 0: return res[0] else: return None
def deleteUserSessionByKey(sessionKey, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Delete/remove a session key from AuthDB.usersessionkeys and remove the associated session record from AuthDB.usersessions. :sessionKey: Key of the session to delete :consistency: Cassandra ConsistencyLevel (default LOCAL_QUORUM) """ userSession = AuthDB.getUserSessionByKey(sessionKey) deleteUserSessionByKeyQuery = CassandraCluster.getPreparedStatement( """ DELETE FROM usersessionkeys WHERE sessionkey = ? """, keyspace=session.keyspace) deleteUserSessionByKeyQuery.consistency_level = consistency if userSession is not None: AuthDB.deleteUserSession(userSession.org, userSession.username, userSession.sessionid, consistency=consistency) session.execute(deleteUserSessionByKeyQuery, (sessionKey,))
def deleteUserSession(org, username, sessionId, consistency=ConsistencyLevel.LOCAL_QUORUM, session=None): """ Delete/remove a session record from AuthDB.usersessions. :org: Organization the user belongs to :username: Name of the user :sessionId: UUID of the session :consistency: """ deleteUserSessionQuery = CassandraCluster.getPreparedStatement( """ DELETE FROM usersessions WHERE org = ? AND username = ? AND sessionid = ? """, keyspace=session.keyspace) deleteUserSessionQuery.consistency_level = consistency session.execute(deleteUserSessionQuery, (org, username, sessionId))
def get(self, username, org): """ Retrieve basic user record information. """ try: session = CassandraCluster.getSession( config['cassandra']['auth_keyspace']) getUserQuery = CassandraCluster.getPreparedStatement( """ SELECT username, org, parentuser, createdate FROM users WHERE org = ? AND username = ? """, keyspace=session.keyspace) results = session.execute(getUserQuery, (org, username)).current_rows except Exception as e: log.error('Exception on User/get: %s' % str(e)) return {'ServerError': 500, 'Message': 'There was an error fulfiling your request'}, 500 if len(results) == 0: return {'Message': 'No user matched "%s"@"%s"' % (username, org)}, 404 elif len(results) == 1: # dict(zip(n._fields, list(n))) user = { 'username': results[0].username, 'org': results[0].org, 'parentuser': str(results[0].parentuser), 'createdate': str(results[0].createdate) } if results[0].parentuser is not None: user['parentuser'] = results[0].parentuser return user else: return {'RequestError': 400, 'Message': 'Request returned too many results'}, 400
def migrateSchema(path, session): """ Execute a CQL schema migration script within a keyspace. File will not be run if it is marked as successfully run in the schema_migrations table within the keyspace. """ # Get the filename part filestart = path.rfind('/')+1 filename = path[filestart:] log.info('Checking migration script "%s"' % (filename,)) # Get the migration history for the script migrationScriptHistoryQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM schema_migrations WHERE scriptname = ?; """, keyspace=session.keyspace) migrationScriptHistory = session.execute(migrationScriptHistoryQuery, (filename,)).current_rows # Run if there is no history or the last execution failed if (len(migrationScriptHistory) == 0 or migrationScriptHistory[-1].failed or not migrationScriptHistory[-1].run): log.info('Running "%s" as it has not been run sucessfully' % (filename,)) content = open(path).read() exectime = datetime.datetime.now() # Insert a record of this script into the schema_migrations table # and mark as not run and not failed. migrationScriptRunInsert = CassandraCluster.getPreparedStatement( """ INSERT INTO schema_migrations (scriptname, time, run, failed, error, content) VALUES (?, ?, false, false, '', ?) """, keyspace=session.keyspace) migrationScriptRunInsert.consistency_level = ConsistencyLevel.QUORUM session.execute(migrationScriptRunInsert, (filename, exectime, content)) try: # Run the migration script session.execute(SimpleStatement(content, consistency_level=ConsistencyLevel.QUORUM)) log.info('Successfully ran "%s"' % (filename,)) # Update the script's run record as completed with success migrationScriptUpdateSuccess = \ CassandraCluster.getPreparedStatement(""" UPDATE schema_migrations SET run = true, failed = false WHERE scriptname = ? AND time = ? """, keyspace=session.keyspace) migrationScriptUpdateSuccess.consistency_level = \ ConsistencyLevel.QUORUM session.execute(migrationScriptUpdateSuccess, (filename, exectime)) except Exception as e: log.info('Failed to run "%s"' % (filename,)) # Log failure migrationScriptUpdateFailure = \ CassandraCluster.getPreparedStatement( """ UPDATE schema_migrations SET run = false, failed = true, error = ? WHERE scriptname = ? AND time = ? """, keyspace=session.keyspace) migrationScriptUpdateFailure.consistency_level = \ ConsistencyLevel.QUORUM session.execute(migrationScriptUpdateFailure, (str(e), filename, exectime)) # Pass failure upwards raise e else: log.info('Script "%s" has already been run on %s' % (filename, migrationScriptHistory[-1].time))
def requestMigration(session=None): """ Request migration tasks on a keyspace, run if selected or wait if not :session: Session name to request migration task on """ # Pre-fetch these prepared statements as they are used more than once deleteReqQuery = CassandraCluster.getPreparedStatement( """ DELETE FROM schema_migration_requests WHERE reqid = ? """, keyspace=session.keyspace) log.info('Checking schema migration requests table') migrationRequestsQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM schema_migration_requests """, keyspace=session.keyspace) rawMigrationRequests = session.execute(migrationRequestsQuery)\ .current_rows migrationRequests = [] staleTime = datetime.datetime.now() - datetime.timedelta(minutes=1) for req in rawMigrationRequests: # A request is "stale" if it is not in progress and it's request # time is older than 1 minute (something happend while waiting), # or if it is in progress but hasn't been updated in more than 1 # minute (something happened while it was updating), or if it is # marked as "failed" (something else happened and the update was # aborted). if (req.failed or (not req.inprogress and req.reqtime < staleTime) or (req.inprogress and req.lastupdate < staleTime)): # Delete the "stale" request (cleanup task) try: log.info('Found stale request %s, deleting' % (req.reqid,)) session.execute(deleteReqQuery, (req.reqid,)) except: pass else: # Keep the record and continue migrationRequests.append(req) if len(migrationRequests) == 0: # No other pending/active migration requests reqid = uuid.uuid4() t = datetime.datetime.now() log.info('No outstanding migration requests, ' + 'requesting migration with ID %s' % (reqid,)) # Nominate ourselves to run migration tasks requestMigrationQuery = CassandraCluster.getPreparedStatement( """ INSERT INTO schema_migration_requests (reqid, reqtime, inprogress, failed, lastupdate) VALUES (?, ?, false, false, ?) """, keyspace=session.keyspace) requestMigrationQuery.consistency_level = ConsistencyLevel.QUORUM session.execute(requestMigrationQuery, (reqid, t, t)) time.sleep(2) log.info('Checking migration requests table to see if we ' + 'are selected for migration') # Check and see if we were selected migrationRequestsQuery = CassandraCluster.getPreparedStatement( """ SELECT * FROM schema_migration_requests """, keyspace=session.keyspace) migrationRequests = session.execute(migrationRequestsQuery)\ .current_rows # Sort by request time migrationRequests = sorted(migrationRequests, key=lambda x: x.reqtime) if (migrationRequests[0].reqid == reqid): # We were selected (only request or first request) t = datetime.datetime.now() # Mark ourselves as "In Progress" markRequestInProgressQuery = CassandraCluster\ .getPreparedStatement( """ UPDATE schema_migration_requests SET inprogress = true, lastupdate = ? WHERE reqid = ? """, keyspace=session.keyspace) session.execute(markRequestInProgressQuery, (t, reqid)) try: # Run migration DB.doMigration(session, reqid) # Delete our req if successfully completed session.execute(deleteReqQuery, (reqid,)) log.info('Migration completed successfully') except Exception as e: log.info('Migration failed') # Something went wrong, mark our req as failed t = datetime.datetime.now() reqFailedQuery = CassandraCluster.getPreparedStatement( """ UPDATE schema_migration_requests SET lastupdate = ?, failed = true, inprogress = false WHERE reqid = ? """, keyspace=session.keyspace) session.execute(reqFailedQuery, (t, reqid)) raise e else: log.info('Not selected for migration (lost election)') # Not selected, delete our request session.execute(deleteReqQuery, (reqid,)) # Wait for selected node to complete the migration DB.waitForMigrationCompletion(session) else: log.info('Not selected for migration (in progress)') # Wait for migration to complete DB.waitForMigrationCompletion(session)