def create(self, name, value, expiration=None, encrypt=False, user_id=None): """ Creates a new named attribute, raising an exception if it already exists. """ # Audit comes first audit_pii.info(self.cid, 'attr.create', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('create', user_id) with closing(self.odb_session_func()) as session: try: return self._create(session, name, value, expiration, encrypt, user_id) except IntegrityError: logger.warn(format_exc()) raise ValidationError(status_code.attr.already_exists)
def _set_expiry(self, session, name, expiration, user_id=None, needs_commit=True): """ A low-level implementation of self.set_expiry which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._set_expiry', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_set_expiry', user_id) return self._update(session, name, expiration=expiration, user_id=user_id, needs_commit=needs_commit)
def renew(self, cid, ust, current_app, remote_addr, user_agent=None, needs_decrypt=True): """ Renew timelife of a user session, if it is valid, and returns its new expiration time in UTC. """ # PII audit comes first audit_pii.info(cid, 'session.renew', extra={ 'current_app': current_app, 'remote_addr': remote_addr }) with closing(self.odb_session_func()) as session: expiration_time = self._get(session, ust, current_app, remote_addr, 'renew', needs_decrypt=needs_decrypt, renew=True, user_agent=user_agent, check_if_password_expired=True) session.commit() return expiration_time
def names(self, user_id=None, _utcnow=_utcnow): """ Returns names of all attributes as a list (unsorted). """ # Audit comes first audit_pii.info(self.cid, 'attr.names', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('names', user_id) now = _utcnow() with closing(self.odb_session_func()) as session: q = session.query(AttrModel.name).\ filter(AttrModel.user_id==(user_id or self.user_id)).\ filter(AttrModel.ust==self.ust).\ filter(AttrModel.expiration_time > now) if self.ust: q = q.\ filter(AttrModel.ust==SSOSession.ust).\ filter(SSOSession.expiration_time > now) result = q.all() return [item.name for item in result]
def get_current_session(self, cid, current_ust, current_app, remote_addr, needs_super_user): """ Returns current session info or raises an exception if it could not be found. Optionally, requires that a super-user be owner of current_ust. """ # PII audit comes first audit_pii.info(cid, 'session.get_current_session', extra={ 'current_app': current_app, 'remote_addr': remote_addr }) # Verify current session's very existence first .. current_session = self._get_session(current_ust, current_app, remote_addr, 'get_current_session') if not current_session: logger.warn('Could not verify session `%s` `%s` `%s` `%s`', current_ust, current_app, remote_addr, format_exc()) raise ValidationError(status_code.auth.not_allowed, True) # .. the session exists but it may be still the case that we require a super-user on input. if needs_super_user: if not current_session.is_super_user: logger.warn( 'Current UST does not belong to a super-user, cannot continue (session.get_current_session), ' \ 'current user is `%s` `%s`', current_session.user_id, current_session.username) raise ValidationError(status_code.auth.not_allowed, True) return current_session
def require_super_user(self, cid, current_ust, current_app, remote_addr): """ Makes sure that current_ust belongs to a super-user or raises an exception if it does not. """ # PII audit comes first audit_pii.info(cid, 'session.require_super_user', extra={'current_app':current_app, 'remote_addr':remote_addr}) return self.get_current_session(cid, current_ust, current_app, remote_addr, True)
def verify(self, cid, target_ust, current_ust, current_app, remote_addr, user_agent=None): """ Verifies a user session without renewing it. """ # PII audit comes first audit_pii.info(cid, 'session.verify', extra={ 'current_app': current_app, 'remote_addr': remote_addr }) self.require_super_user(cid, current_ust, current_app, remote_addr) try: with closing(self.odb_session_func()) as session: return self._get(session, target_ust, current_app, remote_addr, 'verify', renew=False, user_agent=user_agent) except Exception: logger.warn('Could not verify UST, e:`%s`', format_exc()) return False
def get_list(self, cid, ust, target_ust, current_ust, current_app, remote_addr, _unused_user_agent=None): """ Returns a list of sessions. Regular users may receive basic information about their own sessions only whereas super-users may look up any other user's session list. """ # PII audit comes first audit_pii.info(cid, 'session.get_list', extra={'current_app':current_app, 'remote_addr':remote_addr}) # Local aliases has_ust = bool(ust) current_ust_elem = ust if has_ust else current_ust current_session = self.get_current_session(cid, current_ust_elem, current_app, remote_addr, False) # We return a list of sessions for currently logged in user if has_ust: return self._get_session_list_by_user_id(current_session.user_id) else: # If we are to return a list of sessions for another UST, we need to be a super-user if not current_session.is_super_user: logger.warn( 'Current UST does not belong to a super-user, cannot continue (session.get_list), current user is ' \ '`%s` `%s`', current_session.user_id, current_session.username) raise ValidationError(status_code.auth.not_allowed, True) # If we are here, it means that we are a super-user and we are to return sessions # by another person's UST but there is still a chance that this other person is actually us. if current_ust == target_ust: return self._get_session_list_by_user_id(current_session.user_id) else: return self._get_session_list_by_ust(target_ust)
def get(self, cid, target_ust, current_ust, current_app, remote_addr, user_agent=None, check_if_password_expired=True): """ Gets details of a session given by its UST on input, without renewing it. Must be called by a super-user. """ # PII audit comes first audit_pii.info(cid, 'session.get', extra={'current_app':current_app, 'remote_addr':remote_addr}) # Only super-users are allowed to call us current_session = self.require_super_user(cid, current_ust, current_app, remote_addr) # This returns all attributes .. session = self._get_session( target_ust, current_app, remote_addr, 'get', check_if_password_expired, user_agent=user_agent) # .. and we need to build a session entity with a few selected ones only out = SessionEntity() out.creation_time = session.creation_time out.expiration_time = session.expiration_time out.remote_addr = session.remote_addr out.user_agent = session.user_agent out.attr = AttrAPI(cid, current_session.user_id, current_session.is_super_user, current_app, remote_addr, self.odb_session_func, self.is_sqlite, self.encrypt_func, self.decrypt_func, session.user_id, session.ust) return out
def set_expiry_many(self, data, expiration=None, user_id=None): """ Sets expiry for multiple attributes in one call. """ # Audit comes first audit_pii.info(self.cid, 'attr.set_expiry_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) with closing(self.odb_session_func()) as session: for item in data: # Check access permissions to that user's attributes _user_id = item.get('user_id', user_id) self._require_correct_user('set_expiry_many', _user_id) # Check OK self._set_expiry(session, item['name'], item.get('expiration', expiration), _user_id, needs_commit=False) # Commit now everything added to session thus far session.commit()
def _get(self, session, data, decrypt, serialize_dt, user_id=None, columns=AttrModel, exists_only=False, _utcnow=_utcnow): """ A low-level implementation of self.get which knows how to return one or more named attributes. """ # Audit comes first audit_pii.info(self.cid, 'attr._get', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'data': data, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_get', user_id) data = [data] if isinstance(data, basestring) else data out = dict.fromkeys(data, None) now = _utcnow() q = session.query(columns).\ filter(AttrModel.user_id==(user_id or self.user_id)).\ filter(AttrModel.ust==self.ust).\ filter(AttrModel.name.in_(data)).\ filter(AttrModel.expiration_time > now) if self.ust: q = q.\ filter(AttrModel.ust==SSOSession.ust).\ filter(SSOSession.expiration_time > now) result = q.all() for item in result: out[item.name] = True if exists_only else AttrEntity.from_sql( item, decrypt, self.decrypt_func, serialize_dt) # Explicitly convert None to False to satisfy the requirement of returning a boolean value # if we are being called from self.exist (otherwise, None is fine). if exists_only: for key, value in out.items(): if value is None: out[key] = False if len(data) == 1: return out[data[0]] else: return out
def _update(self, session, name, value=None, expiration=None, encrypt=False, user_id=None, needs_commit=True, _utcnow=_utcnow): """ A low-level implementation of self.update which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._update', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'name': name, 'expiration': expiration, 'encrypt': encrypt, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_update', user_id) now = _utcnow() values = {'last_modified': now} if value: values['value'] = dumps( self.encrypt_func(value.encode('utf8')) if encrypt else value) if expiration: values['expiration_time'] = now + timedelta(seconds=expiration) and_condition = [ AttrModelTable.c.user_id == (user_id or self.user_id), AttrModelTable.c.ust == self.ust, AttrModelTable.c.name == name, AttrModelTable.c.expiration_time > now, ] if self.ust: and_condition.extend([ AttrModelTable.c.ust == SSOSessionTable.c.ust, SSOSessionTable.c.expiration_time > now ]) session.execute( AttrModelTableUpdate().\ values(values).\ where(and_(*and_condition))) if needs_commit: session.commit()
def on_external_auth_succeeded(self, cid, sec_def, user_id, ext_session_id, current_app, remote_addr, user_agent=None, _basic_auth=SEC_DEF_TYPE.BASIC_AUTH, _jwt=SEC_DEF_TYPE.JWT, _utcnow=datetime.utcnow, _sha256=sha256): """ Invoked when a user succeeded in authentication via means external to default SSO credentials, e.g. through Basic Auth or JWT. Creates an SSO session related to that event or renews an existing one. """ # type: (unicode, Bunch, unicode, unicode, unicode, unicode) -> SessionInfo # PII audit comes first audit_pii.info(cid, 'session.on_external_auth_succeeded', extra={ 'current_app': current_app, 'remote_addr': remote_addr, 'sec.sec_type': sec_def.sec_type, 'sec.id': sec_def.id, 'sec.username': sec_def.username, }) if sec_def.sec_type == _basic_auth: ext_session_id = '{}.{}'.format(sec_def.sec_type, sec_def.id) elif sec_def.sec_type == _jwt: # JWT tokens tend to be long so we store and hashes rather than raw values ext_session_id = _sha256(ext_session_id).hexdigest() else: raise NotImplementedError() existing_ust = None # type: unicode # Check if there is already a session associated with this external one with closing(self.odb_session_func()) as session: sso_session = get_session_by_ext_id(session, ext_session_id, _utcnow()) if sso_session: existing_ust = sso_session.ust # .. if there is, renew it .. if existing_ust: self.renew(cid, existing_ust, current_app, remote_addr, user_agent, False) # .. otherwise, create a new one. Note that we get here only if else: ctx = LoginCtx(remote_addr, user_agent, False, False, { 'user_id': user_id, 'current_app': current_app }, ext_session_id) return self.login(ctx, is_logged_in_ext=True)
def _create(self, session, name, value, expiration=None, encrypt=False, user_id=None, needs_commit=True, _utcnow=_utcnow): """ A low-level implementation of self.create which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._create', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'name': name, 'expiration': expiration, 'encrypt': encrypt, 'is_super_user': self.is_super_user }) self._require_correct_user('_create', user_id) now = _utcnow() attr_model = AttrModel() attr_model.user_id = user_id or self.user_id attr_model.ust = self.ust attr_model._ust_string = self.ust or '' # Cannot, and will not be, NULL, check the comment in the model for details attr_model.is_session_attr = self.is_session_attr attr_model.name = name attr_model.value = dumps( self.encrypt_func(value.encode('utf8')) if encrypt else value) attr_model.is_encrypted = encrypt attr_model.creation_time = now attr_model.last_modified = now # Expiration is optional attr_model.expiration_time = now + timedelta( seconds=expiration) if expiration else _default_expiration session.add(attr_model) if needs_commit: session.commit()
def delete(self, data, user_id=None, _utcnow=_utcnow): """ Deletes one or more names attributes. """ # Audit comes first audit_pii.info(self.cid, 'attr.delete/delete_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'data': data, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('delete', user_id) now = _utcnow() data = [data] if isinstance(data, basestring) else data and_condition = [ AttrModelTable.c.user_id == (user_id or self.user_id), AttrModelTable.c.ust == self.ust, AttrModelTable.c.name.in_(data), AttrModelTable.c.expiration_time > now, ] with closing(self.odb_session_func()) as session: if self.ust: # Check comment in self._update for a comment why the below is needed if self.is_sqlite: result = self._ensure_ust_is_not_expired( session, self.ust, now) if not result: raise ValidationError( status_code.session.no_such_session) else: and_condition.extend([ AttrModelTable.c.ust == SSOSessionTable.c.ust, SSOSessionTable.c.expiration_time > now ]) session.execute( AttrModelTableDelete().\ where(and_(*and_condition))) session.commit()
def set_many(self, data, expiration=None, encrypt=False, user_id=None): """ Sets values of multiple attributes in one call. """ # Audit comes first audit_pii.info(self.cid, 'attr.set_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('set_many', user_id) self._call_many(self._set, data, expiration, encrypt, user_id)
def exists(self, data, user_id=None): """ Returns a boolean flag to indicate if input attribute(s) exist(s) or not. """ # Audit comes first audit_pii.info(self.cid, 'attr.exists/exists_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('exists', user_id) with closing(self.odb_session_func()) as session: return self._exists(session, data, user_id)
def set_expiry(self, name, expiration, user_id=None): """ Sets expiration for a named attribute. """ # Audit comes first audit_pii.info(self.cid, 'attr.set_expiry', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('set_expiry', user_id) with closing(self.odb_session_func()) as session: return self._set_expiry(session, name, expiration, user_id)
def get(self, data, decrypt=True, serialize_dt=False, user_id=None): """ Returns a named attribute. """ # Audit comes first audit_pii.info(self.cid, 'attr.get/get_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('get', user_id) with closing(self.odb_session_func()) as session: return self._get(session, data, decrypt, serialize_dt, user_id)
def set(self, name, value, expiration=None, encrypt=False, user_id=None): """ Set value of a named attribute, creating it if it does not already exist. """ # Audit comes first audit_pii.info(self.cid, 'attr.set', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('set', user_id) with closing(self.odb_session_func()) as session: self._set(session, name, value, expiration, encrypt)
def on_external_auth_succeeded(self, cid, sec_type, sec_def_id, sec_def_username, user_id, ext_session_id, totp_code, current_app, remote_addr, user_agent=None, _utcnow=datetime.utcnow, ): """ Invoked when a user succeeded in authentication via means external to default SSO credentials, e.g. through Basic Auth or JWT. Creates an SSO session related to that event or renews an existing one. """ # type: (unicode, Bunch, unicode, unicode, unicode, unicode) -> SessionInfo remote_addr = remote_addr if isinstance(remote_addr, unicode) else remote_addr.decode('utf8') # PII audit comes first audit_pii.info(cid, 'session.on_external_auth_succeeded', extra={ 'current_app':current_app, 'remote_addr':remote_addr, 'sec.sec_type': sec_type, 'sec.id': sec_def_id, 'sec.username': sec_def_username, }) existing_ust = None # type: unicode ext_session_id = self._format_ext_session_id(sec_type, sec_def_id, ext_session_id) # Check if there is already a session associated with this external one sso_session = self._get_session_by_ext_id(sec_type, sec_def_id, ext_session_id) if sso_session: existing_ust = sso_session.ust # .. if there is, renew it .. if existing_ust: expiration_time = self.renew(cid, existing_ust, current_app, remote_addr, user_agent, False) session_info = SessionInfo() session_info.ust = existing_ust session_info.expiration_time = expiration_time return session_info # .. otherwise, create a new one. Note that we get here only if else: ctx = LoginCtx(remote_addr, user_agent, False, False, { 'user_id': user_id, 'current_app': current_app, 'totp_code': totp_code, 'sec_type': sec_type, }, ext_session_id) return self.login(ctx, is_logged_in_ext=True)
def _exists(self, session, data, user_id=None): """ A low-level implementation of self.exists which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._exists', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_exists', user_id) return self._get(session, data, False, False, user_id, AttrModel.name, True)
def _call_many(self, func, data, expiration=None, encrypt=False, user_id=None): """ A reusable method for manipulation of multiple attributes at a time. """ # Audit comes first audit_pii.info(self.cid, 'attr._call_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'is_super_user': self.is_super_user, 'func': func.__func__.__name__ }) with closing(self.odb_session_func()) as session: for item in data: # Check access permissions to that user's attributes _user_id = item.get('user_id', user_id) self._require_correct_user('_call_many', _user_id) func(session, item['name'], item['value'], item.get('expiration', expiration), item.get('encrypt', encrypt), _user_id, needs_commit=False) # Commit now everything added to session thus far try: session.commit() except IntegrityError: logger.warn(format_exc()) raise ValidationError(status_code.attr.already_exists)
def delete(self, data, user_id=None, _utcnow=_utcnow): """ Deletes one or more names attributes. """ # Audit comes first audit_pii.info(self.cid, 'attr.delete/delete_many', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'data': data, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('delete', user_id) now = _utcnow() data = [data] if isinstance(data, basestring) else data and_condition = [ AttrModelTable.c.user_id == (user_id or self.user_id), AttrModelTable.c.ust == self.ust, AttrModelTable.c.name.in_(data), AttrModelTable.c.expiration_time > now, ] if self.ust: and_condition.extend([ AttrModelTable.c.ust == SSOSessionTable.c.ust, SSOSessionTable.c.expiration_time > now ]) with closing(self.odb_session_func()) as session: session.execute( AttrModelTableDelete().\ where(and_(*and_condition))) session.commit()
def _set(self, session, name, value, expiration=None, encrypt=False, user_id=None, needs_commit=True): """ A low-level implementation of self.set which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._set', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'name': name, 'expiration': expiration, 'encrypt': encrypt, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_set', user_id) # Check if the attribute exists .. if self._exists(session, name, user_id): # .. it does, so we need to set its new value. self._update(session, name, value, expiration, encrypt, user_id, needs_commit) # .. does not exist, so we need to create it else: self._create(session, name, value, expiration, encrypt, user_id, needs_commit)
def update(self, name, value, expiration=None, encrypt=False, user_id=None): """ Updates an existing attribute, raising an exception if it does not already exist. """ # Audit comes first audit_pii.info(self.cid, 'attr.update', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr }) # Check access permissions to that user's attributes self._require_correct_user('update', user_id) with closing(self.odb_session_func()) as session: return self._update(session, name, value, expiration, encrypt, user_id)
def _update(self, session, name, value=None, expiration=None, encrypt=False, user_id=None, needs_commit=True, _utcnow=_utcnow): """ A low-level implementation of self.update which expects an SQL session on input. """ # Audit comes first audit_pii.info(self.cid, 'attr._update', self.current_user_id, user_id, extra={ 'current_app': self.current_app, 'remote_addr': self.remote_addr, 'name': name, 'expiration': expiration, 'encrypt': encrypt, 'is_super_user': self.is_super_user }) # Check access permissions to that user's attributes self._require_correct_user('_update', user_id) now = _utcnow() values = {'last_modified': now} if value: values['value'] = dumps( self.encrypt_func(value.encode('utf8')) if encrypt else value) if expiration: values['expiration_time'] = now + timedelta(seconds=expiration) and_condition = [ AttrModelTable.c.user_id == (user_id or self.user_id), AttrModelTable.c.ust == self.ust, AttrModelTable.c.name == name, AttrModelTable.c.expiration_time > now, ] if self.ust: # SQLite needs to be treated in a special way, otherwise we get an exception from SQLAlchemy # NotImplementedError: This backend does not support multiple-table criteria within UPDATE # which means that on SQLite we need an additional query. if self.is_sqlite: result = self._ensure_ust_is_not_expired( session, self.ust, now) if not result: raise ValidationError(status_code.session.no_such_session) else: and_condition.extend([ AttrModelTable.c.ust == SSOSessionTable.c.ust, SSOSessionTable.c.expiration_time > now ]) session.execute( AttrModelTableUpdate().\ values(values).\ where(and_(*and_condition))) if needs_commit: session.commit()