def change_password(self, user, password, new_password, password_confirm): if new_password != password_confirm: msg = "Your password doesn't match the confirmation" raise RestApiException(msg, status_code=hcodes.HTTP_BAD_CONFLICT) if self.auth.VERIFY_PASSWORD_STRENGTH: check = True if password is not None: check, msg = self.verify_password_strength( new_password, old_pwd=password ) else: check, msg = self.verify_password_strength( new_password, old_hash=user.password ) if not check: raise RestApiException(msg, status_code=hcodes.HTTP_BAD_CONFLICT) if new_password is not None and password_confirm is not None: now = datetime.now(pytz.utc) user.password = BaseAuthentication.get_password_hash(new_password) user.last_password_change = now self.auth.save_user(user) tokens = self.auth.get_tokens(user=user) for token in tokens: self.auth.invalidate_token(token=token["token"]) # changes the user uuid invalidating all tokens self.auth.invalidate_all_tokens() return True
def verify_password_strength(self, pwd, old_pwd=None, old_hash=None): if old_pwd is not None and pwd == old_pwd: return False, "The new password cannot match the previous password" if old_hash is not None: new_hash = BaseAuthentication.get_password_hash(pwd) if old_hash == new_hash: return False, "The new password cannot match the previous password" # FIXME: min length should configurable? if len(pwd) < 8: return False, "Password is too short, use at least 8 characters" if not re.search("[a-z]", pwd): return False, "Password is too weak, missing lower case letters" if not re.search("[A-Z]", pwd): return False, "Password is too weak, missing upper case letters" if not re.search("[0-9]", pwd): return False, "Password is too weak, missing numbers" # special_characters = "['\s!#$%&\"(),*+,-./:;<=>?@[\\]^_`{|}~']" special_characters = "[^a-zA-Z0-9]" if not re.search(special_characters, pwd): return False, "Password is too weak, missing special characters" return True, None
def put(self, user_id: str, target_user: User, user: User, **kwargs: Any) -> Response: if "password" in kwargs: unhashed_password = kwargs["password"] kwargs["password"] = BaseAuthentication.get_password_hash( kwargs["password"]) else: unhashed_password = None payload = kwargs.copy() roles: List[str] = kwargs.pop("roles", []) # The role is already refused by webards... This is an additional check # to improve the security, but can't be reached if not self.auth.is_admin( user) and Role.ADMIN in roles: # pragma: no cover raise Forbidden("This role is not allowed") group_id = kwargs.pop("group", None) email_notification = kwargs.pop("email_notification", False) self.auth.link_roles(target_user, roles) userdata, extra_userdata = self.auth.custom_user_properties_pre(kwargs) prev_expiration = target_user.expiration # mypy correctly raises errors because update_properties is not defined # in generic Connector instances, but in this case this is an instance # of an auth db and their implementation always contains this method self.auth.db.update_properties(target_user, userdata) # type: ignore self.auth.custom_user_properties_post(target_user, userdata, extra_userdata, self.auth.db) self.auth.save_user(target_user) if group_id is not None: group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because group_id is prefiltered by marshmallow raise NotFound( "This group cannot be found") # pragma: no cover self.auth.add_user_to_group(target_user, group) if email_notification and unhashed_password is not None: notify_update_credentials_to_user(target_user, unhashed_password) if target_user.expiration: # Set expiration on a previously non-expiring account # or update the expiration by reducing the validity period # In both cases tokens should be invalited to prevent to have tokens # with TTL > account validity # dt_lower (alias for date_lower_than) is a comparison fn that ignores tz if not prev_expiration or dt_lower(target_user.expiration, prev_expiration): for token in self.auth.get_tokens(user=target_user): # Invalidate all tokens with expiration after the account expiration if dt_lower(target_user.expiration, token["expiration"]): self.auth.invalidate_token(token=token["token"]) self.log_event(self.events.modify, target_user, payload) return self.empty_response()
def put(self, user_id=None): if user_id is None: raise RestApiException("Please specify a user id", status_code=hcodes.HTTP_BAD_REQUEST) schema = self.get_endpoint_custom_definition() if self.neo4j_enabled: self.graph = self.get_service_instance('neo4j') is_admin = self.auth.verify_admin() is_local_admin = self.auth.verify_local_admin() if not is_admin and not is_local_admin: raise RestApiException( "You are not authorized: missing privileges", status_code=hcodes.HTTP_BAD_UNAUTHORIZED, ) v = self.get_input() user = self.auth.get_users(user_id) if user is None: raise RestApiException( "This user cannot be found or you are not authorized") user = user[0] current_user = self.get_current_user() is_authorized = self.check_permissions(current_user, user, is_admin, is_local_admin) if not is_authorized: raise RestApiException( "This user cannot be found or you are not authorized") if "password" in v and v["password"] == "": del v["password"] if "password" in v: unhashed_password = v["password"] v["password"] = BaseAuthentication.get_password_hash(v["password"]) else: unhashed_password = None if "email" in v: v["email"] = v["email"].lower() roles = self.parse_roles(v) if not is_admin: allowed_roles = get_project_configuration( "variables.backend.allowed_roles", default=[], ) for r in roles: if r not in allowed_roles: raise RestApiException( "You are not allowed to assign users to this role") self.auth.link_roles(user, roles) # Cannot update email address (unique username used to login-in) v.pop('email', None) if self.neo4j_enabled: self.update_properties(user, schema, v) elif self.sql_enabled: self.update_sql_properties(user, schema, v) elif self.mongo_enabled: self.update_mongo_properties(user, schema, v) else: raise RestApiException( "Invalid auth backend, all known db are disabled") self.auth.save_user(user) # FIXME: groups management is only implemented for neo4j if 'group' in v: group = self.parse_group(v) if not is_admin and group.shortname != "default": if not group.coordinator.is_connected(current_user): raise RestApiException( "You are not allowed to assign users to this group") p = None for p in user.belongs_to.all(): if p == group: continue if p is not None: user.belongs_to.reconnect(p, group) else: user.belongs_to.connect(group) email_notification = v.get('email_notification', False) if email_notification and unhashed_password is not None: self.send_notification(user, unhashed_password, is_update=True) return self.empty_response()
def put(self, user_id: str, **kwargs: Any) -> Response: user = self.auth.get_user(user_id=user_id) if user is None: raise NotFound( "This user cannot be found or you are not authorized") if "password" in kwargs: unhashed_password = kwargs["password"] kwargs["password"] = BaseAuthentication.get_password_hash( kwargs["password"]) else: unhashed_password = None payload = kwargs.copy() roles: List[str] = kwargs.pop("roles", []) group_id = kwargs.pop("group", None) email_notification = kwargs.pop("email_notification", False) self.auth.link_roles(user, roles) userdata, extra_userdata = self.auth.custom_user_properties_pre(kwargs) prev_expiration = user.expiration self.auth.db.update_properties(user, userdata) self.auth.custom_user_properties_post(user, userdata, extra_userdata, self.auth.db) self.auth.save_user(user) if group_id is not None: group = self.auth.get_group(group_id=group_id) if not group: # Can't be reached because grup_id is prefiltered by marshmallow raise NotFound( "This group cannot be found") # pragma: no cover self.auth.add_user_to_group(user, group) if email_notification and unhashed_password is not None: smtp_client = smtp.get_instance() send_notification(smtp_client, user, unhashed_password, is_update=True) if user.expiration: # Set expiration on a previously non-expiring account # or update the expiration by reducing the validity period # In both cases tokens should be invalited to prevent to have tokens # with TTL > account validity # dt_lower (alias for date_lower_than) is a comparison fn that ignores tz if prev_expiration is None or dt_lower(user.expiration, prev_expiration): for token in self.auth.get_tokens(user=user): # Invalidate all tokens with expiration after the account expiration if dt_lower(user.expiration, token["expiration"]): self.auth.invalidate_token(token=token["token"]) self.log_event(self.events.modify, user, payload) return self.empty_response()