def in_system(self, id_type, entity_id, system): """Check if a user is represented in a system. :type id_type: basestring :param id_type: The id-type to look-up by. :type entity_id: basestring :param entity_id: The entitys id. :type system: basestring :param system: The system to check.""" co = Factory.get('Constants')(self.db) # Fetch entity e = Utils.get(self.db, 'entity', id_type, entity_id) if not e: # TODO: Should this be raised in the Utils-module/class/whatever? raise Errors.CerebrumRPCException('Entity does not exist') try: sys = co.Spread(system) int(sys) except Errors.NotFoundError: raise Errors.CerebrumRPCException('System does not exist') try: return bool(e.get_subclassed_object().has_spread(sys)) except AttributeError: # If we wind up here, the entity does not have the EntitySpread # class mixed into itself. When the EntitySpread-class is not mixed # in, we get an AttributeError since has_spread() is not defined. # TBD: Return false, or raise something? return False
def group_create(self, name, description, expire_date=None, visibility='A'): """Create a group. :type name: str :param name: The groups name. :type description: str :param description: The groups description. :type expire_date: DateTime :param expire_date: The groups expiration date. :type visibility: str :param visibility: The groups visibility. Can one of: 'A' All 'I' Internal 'N' None """ # Perform auth-check self.ba.can_create_group(self.operator_id, groupname=name) # Check if group exists if Utils.get_group(self.db, 'name', name): raise Errors.CerebrumRPCException('Group already exists.') co = Factory.get('Constants')(self.db) gr = Factory.get('Group')(self.db) # Test if visibility is sane vis = co.GroupVisibility(visibility) try: int(vis) except (Errors.NotFoundError, TypeError): raise Errors.CerebrumRPCException('Invalid visibility.') # Create the group # TODO: Moar try/except? GroupAPI.group_create(gr, self.operator_id, vis, name, description, expire_date) # Set moderator if appropriate. # TODO: use the commented version when we pass the config as a dict. if getattr(self.config, 'GROUP_OWNER_OPSET', None): # Fetch operator. en = Utils.get_entity_by_id(self.db, self.operator_id) # Grant auth GroupAPI.grant_auth(en, gr, getattr(self.config, 'GROUP_OWNER_OPSET')) return gr.entity_id
def get_addresses_by_affiliation(self, status, source, skos=None): """Find persons that has the given affiliations/statuses from the given source systems and at the given stedkoder (SKOs), if any. Return a list of all the persons' primary e-mail addresses. Note that some persons might not have any user affiliations, thus having no *primary* affiliation, even if they have user accounts with e-mail addresses. """ affs = stats = ou_ids = None if status: affs, stats = self._get_aff_status(status) if source: source = [self.co.AuthoritativeSystem(s) for s in source] if skos: ou_ids = self._get_ous(skos) if not ou_ids: raise Errors.CerebrumRPCException('OUs not found') pe = Factory.get('Person')(self.db) pe2email = dict(pe.list_primary_email_address(self.co.entity_person)) rows = [] if affs: rows += pe.list_affiliations(affiliation=affs, ou_id=ou_ids) if stats: rows += pe.list_affiliations(status=stats, ou_id=ou_ids) ret = set(pe2email[row['person_id']] for row in rows if pe2email.has_key(row['person_id'])) print 'DEBUG: Returning %d e-mail addresses' % len(ret) return ret
def check_too_many_attempts(self, account): """ Checks if a user has tried to use the service too many times. Creates the trait if it doesn't exist, and increments the numval. Raises an exception when too many attempts occur in the block period. """ attempts = 0 trait = account.get_trait(self.co.trait_password_failed_attempts) block_period = now() - RelativeDateTime( seconds=cereconf.INDIVIDUATION_ATTEMPTS_BLOCK_PERIOD) if trait and trait['date'] > block_period: attempts = int(trait['numval']) logger.debug('User %r has tried %r times', account.account_name, attempts) if attempts > cereconf.INDIVIDUATION_ATTEMPTS: logger.info("User %r too many attempts, temporarily blocked", account.account_name) raise Errors.CerebrumRPCException('toomanyattempts') account.populate_trait(code=self.co.trait_password_failed_attempts, target_id=account.entity_id, date=now(), numval=attempts + 1) account.write_db() account._db.commit()
def get_group(db, id_type, group): """Fetch a group by id. :type db: <Cerebrum.Database.Database> :param db: A Cerebrum database object. :type id_type: str :param id_type: The identifier type, 'name' or 'id' :type group: str or int :param group: The group identifier :rtype: Group or None """ gr = Factory.get('Group')(db) # Determine lookup type if id_type == 'name': lookup = gr.find_by_name elif id_type == 'id': lookup = gr.find else: raise Errors.CerebrumRPCException('Invalid id_type.') # Perform actual lookup # TODO: How do we handle NotFoundErrors? Is this correct? try: lookup(group) except Errors.NotFoundError: return None return gr
def search_members_flat(self, groupname): # TODO: add access control for who is allowed to get the members. Only # moderators of the given group? #if not self.ba.is_superuser(self.operator_id): # raise NotAuthorizedError('Only for superusers') # Raises Cerebrum.modules.bofh.errors.PermissionDenied - how to handle # these? #self.ba.can_set_trait(self.operator_id) try: self.grp.clear() self.grp.find_by_name(groupname) except Errors.NotFoundError: raise Errors.CerebrumRPCException("Group %s not found." % groupname) grp_id = self.grp.entity_id self.grp.clear() type_account = str(self.co.entity_account) member_rows = self.grp.search_members(group_id=grp_id, indirect_members=True, include_member_entity_name=True) return [{ 'member_type': type_account, 'member_id': str(row['member_id']), 'uname': row['member_name'] } for row in member_rows if row['member_type'] == self.co.entity_account]
def check_phone(self, phone_no, numbers, person, account): """Check if given phone_no belongs to person. The phone number is only searched for in source systems that the person has active affiliations from and contact types as defined in INDIVIDUATION_PHONE_TYPES. Other numbers are ignored. Set delays are also checked, to avoid that changed phone numbers are used for some period. """ is_fresh = self.entity_is_fresh(person, account) for num in numbers: if not self.number_match(stored=num['number'], given=phone_no): continue if is_fresh: # delay is ignored for fresh entities return True delay = self.get_delay(num['system_name'], num['type']) for row in self.db.get_log_events(types=self.co.entity_cinfo_add, any_entity=person.entity_id, sdate=delay): data = pickle.loads(row['change_params']) if num['number'] == data['value']: log.info('person_id=%s recently changed phoneno' % person.entity_id) self.mail_warning( person=person, account=account, reason=("Your phone number has recently been" + " changed. Due to security reasons, it" + " can not be used by the password service" + " for a few days.")) raise Errors.CerebrumRPCException('fresh_phonenumber') return True return False
def get_account(db, id_type, account): """Fetch a group by id. :type db: <Cerebrum.Database.Database> :param db: A Cerebrum database object. :type id_type: str :param id_type: The identifier type, 'name' or 'id' :type id_type: str or int :param group: The account identifier :rtype: Account or None """ ac = Factory.get('Account')(db) if id_type == 'name': lookup = ac.find_by_name elif id_type == 'id': lookup = ac.find else: raise Errors.CerebrumRPCException('Invalid id_type.') try: lookup(account) except Errors.NotFoundError: return None return ac
def get_addresses_by_affiliation(ctx, status=None, skos=None, source=None): """Get primary e-mail addresses for persons that match given criterias.""" if not source and not status: raise Errors.CerebrumRPCException('Input needed') return ctx.udc['postmaster'].get_addresses_by_affiliation(status=status, skos=skos, source=source)
def set_password(self, uname, new_password, token, browser_token): if not self.check_token(uname, token, browser_token): return False account = self.get_account(uname) try: check_password(new_password, account) except PasswordNotGoodEnough as e: m = str(e).decode('utf-8') raise Errors.CerebrumRPCException('password_invalid', m) # All data is good. Set password account.set_password(new_password) try: account.write_db() account._db.commit() log.info("Password for %s altered." % uname) except self.db.DatabaseError, m: log.error("Error when setting password for %s: %s" % (uname, m)) raise Errors.CerebrumRPCException('error_unknown')
def get_account(self, uname): account = Factory.get('Account')(self.db) try: account.find_by_name(uname) except Errors.NotFoundError: log.info("Couldn't find account %s" % uname) raise Errors.CerebrumRPCException('person_notfound') else: return account
def check_token(self, uname, token, browser_token): """Check if token and other data from user is correct.""" try: account = self.get_account(uname) except Errors.CerebrumRPCException: # shouldn't tell what went wrong return False # Check browser_token. The given browser_token may be "" but if so # the stored browser_token must be "" as well for the test to pass. bt = account.get_trait(self.co.trait_browser_token) if not bt or bt['strval'] != self.hash_token(browser_token, uname): log.info("Incorrect browser_token %s for user %s" % (browser_token, uname)) return False # Check password token. Keep track of how many times a token is # checked to protect against brute force attack (defaults to 20). pt = account.get_trait(self.co.trait_password_token) no_checks = int(pt['numval']) if no_checks > getattr(cereconf, 'INDIVIDUATION_TOKEN_ATTEMPTS', 20): log.info("No. of token checks exceeded for user %s" % uname) raise Errors.CerebrumRPCException('toomanyattempts_check') # Check if we're within time limit time_limit = now() - RelativeDateTime( minutes=cereconf.INDIVIDUATION_TOKEN_LIFETIME) if pt['date'] < time_limit: log.debug("Password token's timelimit for user %s exceeded" % uname) raise Errors.CerebrumRPCException('timeout_check') if pt and pt['strval'] == self.hash_token(token, uname): # All is fine return True log.debug("Token %s incorrect for user %s" % (token, uname)) account.populate_trait(self.co.trait_password_token, strval=pt['strval'], date=pt['date'], numval=no_checks + 1) account.write_db() account._db.commit() return False
def get_person(self, id_type, ext_id): person = Factory.get('Person')(self.db) person.clear() if not hasattr(self.co, id_type): log.error("Wrong id_type: '%s'" % id_type) raise Errors.CerebrumRPCException('person_notfound') try: person.find_by_external_id(getattr(self.co, id_type), ext_id) return person except Errors.NotFoundError: log.debug("Couldn't find person with %s='%s'" % (id_type, ext_id)) # Try without leading zeros, as FS use that, and which could confuse # students. TODO: Note that this does not help if the external IDs are # stored _with_ leading zeros in the database, i.e. the opposite way. if ext_id.isdigit(): try: person.find_by_external_id(getattr(self.co, id_type), str(int(ext_id))) log.debug( "Found person %s without leading zeros in ext_id: %s" % (person.entity_id, ext_id)) return person except Errors.NotFoundError: pass # Still not found? Try to padd with zeros if it's a student number # with less than 6 digits: if (hasattr(self.co, 'externalid_studentnr') and getattr( self.co, id_type) == self.co.externalid_studentnr and len(ext_id) < 6): try: person.find_by_external_id(getattr(self.co, id_type), '%06d' % int(ext_id)) log.debug( "Found person %s with padded zeros in ext_id: %s" % (person.entity_id, ext_id)) return person except Errors.NotFoundError: pass raise Errors.CerebrumRPCException('person_notfound')
def group_remove_member(self, group_id_type, group_id, member_id_type, member_id): """Remove a member from a group. :type group_id_type: str :param group_id_type: Group identifier type, 'id' or 'group_name' :type group_id: str :param group_id: Group identifier :type member_id_type: str :param member_id_type: Member identifier type, 'id' or 'account_name' :type member_id: str :param member_id: Member identifier :rtype: boolean """ # Get the group gr = Utils.get(self.db, 'group', group_id_type, group_id) if not gr: raise Errors.CerebrumRPCException( 'Group %s:%s does not exist.' % (group_id_type, group_id)) # Perform auth check self.ba.can_alter_group(self.operator_id, gr) # Get the member we want to add member = Utils.get(self.db, 'entity', member_id_type, member_id) if not member: raise Errors.CerebrumRPCException( 'Entity %s:%s does not exist.' % (member_id_type, member_id)) if not gr.has_member(member.entity_id): return False GroupAPI.remove_member(gr, member.entity_id) return True
def validate_password(self, password, account_name, structured): """ Validate any password :param password: the password to be validated :type password: str :param account_name: the account name to be used or '' :type account_name: str :param structured: whether to ask for a strctured (json) output :type structured: bool """ account = None if account_name: try: account = Factory.get('Account')(self.db) account.find_by_name(account_name) except Errors.NotFoundError: raise Errors.CerebrumRPCException('unknown_error') try: result = check_password(password, account, structured) # exceptions are obsolete and used only for backward # compatibility here (f.i. old brukerinfo clients) except PhrasePasswordNotGoodEnough as e: # assume that structured is False m = str(e).decode('utf-8') # separate exception for phrases on the client?? # no point of having separate except block otherwise raise Errors.CerebrumRPCException('password_invalid', m) except PasswordNotGoodEnough as e: # assume that structured is False m = str(e).decode('utf-8') raise Errors.CerebrumRPCException('password_invalid', m) else: if structured: # success or error data sent to the caller return json.dumps(result, indent=4) else: # no PasswordNotGoodEnough exception thrown return 'OK'
def get_person_accounts(self, id_type, ext_id): """Find Person given by id_type and external id and return a list of dicts with username, status and priority. Note that if the person is reserved from publication, it will get an SMS with its usernames instead. @param id_type: type of external id @type id_type: string @param ext_id: external id @type ext_id: string @return: list of dicts with username, status and priority, sorted by priority @rtype: list of dicts """ # Check if person exists try: person = self.get_person(id_type, ext_id) except Errors.CerebrumRPCException: raise Errors.CerebrumRPCException('person_notfound_usernames') # Check reservation if self.is_reserved_publication(person): log.info("Person id=%s is reserved from publication" % person.entity_id) # if person has a phone number, we could send the usernames by SMS: phone_nos = self.get_phone_numbers(person, only_first_affiliation=False) if phone_nos: accounts = [a['uname'] for a in self.get_account_list(person)] log.debug('Sending SMS with usernames: %s' % ', '.join(accounts)) self.send_sms(phone_nos[0]['number'], cisconf.SMS_MSG_USERNAMES % '\n'.join(accounts)) else: log.debug('No phone number for person %s' % person.entity_id) raise Errors.CerebrumRPCException('person_notfound_usernames') return self.get_account_list(person)
def add_to_system(self, id_type, entity_id, system): """Add an entity to a system. :type id_type: basestring :param id_type: The id-type to look-up by. :type entity_id: basestring :param entity_id: The entitys id. :type system: basestring :param system: The system the entity should be added to.""" # Fetch entity en = Utils.get(self.db, 'entity', id_type, entity_id) if not en: # TODO: Should this be raised in the Utils-module/class/whatever? raise Errors.CerebrumRPCException('Entity does not exist') co = Factory.get('Constants')(self.db) try: sys = co.Spread(system) int(sys) except Errors.NotFoundError: raise Errors.CerebrumRPCException('System does not exist') self.ba.can_add_spread(self.operator_id, en, sys) try: if en.get_subclassed_object().has_spread(sys): return en.get_subclassed_object().add_spread(sys) except AttributeError: raise Errors.CerebrumRPCException('Can\'t add entity to system.') except self.db.IntegrityError: # TODO: This seems correct? raise Errors.CerebrumRPCException( 'Entity not applicable for system.')
def set_password(self, uname, new_password, token, browser_token): if not self.check_token(uname, token, browser_token): return False account = self.get_account(uname) try: check_password(new_password, account) except PasswordNotGoodEnough as e: m = text_type(e) raise Errors.CerebrumRPCException('password_invalid', m) # All data is good. Set password account.set_password(new_password) try: account.write_db() account._db.commit() logger.info("Password for %r altered", uname) except self.db.DatabaseError as m: logger.error("Error when setting password for %r: %s", uname, m) raise Errors.CerebrumRPCException('error_unknown') # Remove "weak password" quarantine for r in account.get_entity_quarantine(): for qua in (self.co.quarantine_autopassord, self.co.quarantine_svakt_passord): if int(r['quarantine_type']) == qua: account.delete_entity_quarantine(qua) account.write_db() account._db.commit() # TODO: move these checks up and raise exceptions? Wouldn't happen, # since generate_token() checks this already, but might get other # authentication methods later. if account.is_deleted(): logger.warning("user %r is deleted", uname) elif account.is_expired(): logger.warning("user %r is expired", uname) elif QuarantineHandler.check_entity_quarantines( self.db, account.entity_id).is_locked(): logger.info("user %r has an active quarantine", uname) return True
def group_info(self, group_id_type, group_id): """Get information about a group. :type group_id_type: str :param group_id_type: Group identifier type, 'id' or 'group_name' :type group_id: str :param group_id: Group identifier """ gr = Utils.get(self.db, 'group', group_id_type, group_id) # Check if group exists if not gr: raise Errors.CerebrumRPCException( 'Group %s:%s does not exist.' % (group_id_type, group_id)) return GroupAPI.group_info(gr)
def group_list(self, group_id_type, group_id): """Get list of group members :type group_id_type: str :param group_id_type: Group identifier type, 'id' or 'group_name' :type group_id: unicode or str :param group_id: Group identifier :rtype: list(dict{'name': name or id, 'type': type}) """ gr = Utils.get(self.db, 'group', group_id_type, group_id) # Check if group exists if not gr: raise Errors.CerebrumRPCException( 'Group %s:%s does not exist.' % (group_id_type, group_id)) lst = [{'name': x[1], 'type': x[0]} for x in map(Utils.get_entity_designator, GroupAPI.group_list(gr))] return lst
def spread_list(self, id_type, entity_id): """List account's spreads. :type id_type: basestring :param id_type: The id-type to look-up by. :type entity_id: basestring :param entity_id: The entitys id.""" co = Factory.get('Constants')(self.db) e = Utils.get(self.db, 'entity', id_type, entity_id) spreads = dict() def fixer(id): if id[0] in spreads: return str(spreads[id[0]]) s = spreads[id[0]] = co.map_const(id[0]) return str(s) try: return map(fixer, e.get_subclassed_object().get_spread()) except NameError: raise Errors.CerebrumRPCException("No spreads in entity")
def group_set_expire(self, group_id_type, group_id, expire_date=None): """Set an expire-date on a group. :type group_id_type: str :param group_id_type: Group identifier type, 'id' or 'group_name' :type group_id: str :param group_id: Group identifier :type expire_date: <mx.DateTime> :param expire_date: The expire-date to set, or None. """ # Get group gr = Utils.get(self.db, 'group', group_id_type, group_id) if not gr: raise Errors.CerebrumRPCException( 'Group %s:%s does not exist.' % (group_id_type, group_id)) # Perform auth check self.ba.can_alter_group(self.operator_id, gr) GroupAPI.set_expire_date(gr, expire_date)
def validate_password(self, password): """Overwriting this to return data in an exception, for checking data""" raise Errors.CerebrumRPCException(password)
def generate_token(self, id_type, ext_id, uname, phone_no, browser_token=''): """Generate a token that functions as a short time password for the user and send it by SMS. @param id_type: type of external id @type id_type: string @param ext_id: external id @type ext_id: string @param uname: username @type uname: string @param phone_no: phone number @type phone_no: string @param browser_token: browser id @type browser_token: string @return: True if success, False otherwise @rtype: bool """ # Check if account exists account = self.get_account(uname) # Check if account has been checked too many times self.check_too_many_attempts(account) # Check if person exists person = self.get_person(id_type, ext_id) if not account.owner_id == person.entity_id: log.info("Account %s doesn't belong to person %d" % (uname, person.entity_id)) raise Errors.CerebrumRPCException('person_notfound') # Check if account is blocked if not self.check_account(account): log.info("Account %s is blocked" % (account.account_name)) raise Errors.CerebrumRPCException('account_blocked') # Check if person/account is reserved if self.is_reserved(account=account, person=person): log.info("Account %s (or person) is reserved" % (account.account_name)) raise Errors.CerebrumRPCException('account_reserved') # Check if person/account is self reserved if self.is_self_reserved(account=account, person=person): log.info("Account %s (or person) is self reserved" % (account.account_name)) raise Errors.CerebrumRPCException('account_self_reserved') # Check phone_no phone_nos = self.get_phone_numbers(person) if not phone_nos: log.info("No relevant affiliation or phone registered for %s" % account.account_name) raise Errors.CerebrumRPCException('person_miss_info') if not self.check_phone( phone_no, numbers=phone_nos, person=person, account=account): log.info("phone_no %s not found for %s" % (phone_no, account.account_name)) raise Errors.CerebrumRPCException('person_notfound') # Create and send token token = self.create_token() log.debug("Generated token %s for %s" % (token, uname)) # TODO: remove when done testing if not self.send_token(phone_no, token): log.error("Couldn't send token to %s for %s" % (phone_no, uname)) raise Errors.CerebrumRPCException('token_notsent') account._db.log_change(account.entity_id, self.co.account_password_token, None, change_params={'phone_to': phone_no}) # store password token as a trait account.populate_trait(self.co.trait_password_token, date=now(), numval=0, strval=self.hash_token(token, uname)) # store browser token as a trait if type(browser_token) is not str: log.err("Invalid browser_token, type='%s', value='%s'" % (type(browser_token), browser_token)) browser_token = '' account.populate_trait(self.co.trait_browser_token, date=now(), strval=self.hash_token(browser_token, uname)) account.write_db() account._db.commit() return True