def delete_user( user_data: user_pb2.User, token: str, *, user_db: Optional[mongo.UsersDatabase] = None) -> Optional[str]: """Delete a user.""" if not user_db: user_db = _get_user_db() filter_user: Optional[dict[str, Any]] if user_data.user_id: try: auth_token.check_token(user_data.user_id, token, role='unsubscribe') except ValueError: try: auth_token.check_token(user_data.user_id, token, role='auth') except ValueError: flask.abort( 403, i18n.flask_translate("Mauvais jeton d'authentification")) filter_user = {'_id': safe_object_id(user_data.user_id)} elif user_data.profile.email: try: auth_token.check_google_token(token, re.compile('@bayesimpact.org')) except i18n.TranslatableException: try: auth_token.check_admin_token(token) except ValueError: flask.abort( 403, i18n.flask_translate( 'Accès refusé, action seulement pour le super-administrateur.' )) filter_user = user_db.user.find_one( {'hashedEmail': auth.hash_user_email(user_data.profile.email)}, {'_id': 1}) else: flask.abort( 400, i18n.flask_translate( 'Impossible de supprimer un utilisateur sans son ID.')) if not filter_user: return None user_proto = get_user_data(str(filter_user['_id']), collection=user_db.user) if not auth.delete_user(user_proto, user_db): flask.abort( 500, i18n.flask_translate( 'Erreur serveur, impossible de supprimer le compte.')) return str(filter_user['_id'])
def _decorated_fun(*args: Any, **kwargs: Any) -> Any: auth_token = flask.request.args.get('token') user_id = flask.request.args.get('user') if not user_id or not auth_token: flask.abort(422, i18n.flask_translate('Paramètres manquants.')) try: token.check_token(user_id, auth_token, role=role) except ValueError: flask.abort(403, i18n.flask_translate('Accès non autorisé.')) return func(*args, **dict(kwargs, user_id=user_id))
def authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: """Authenticate a user.""" if auth_request.google_token_id: return self._google_authenticate(auth_request) if auth_request.facebook_access_token: return self._facebook_authenticate(auth_request) if auth_request.pe_connect_code: return self._pe_connect_authenticate(auth_request) if auth_request.linked_in_code: return self._linked_in_authenticate(auth_request) if auth_request.email: return self._email_authenticate(auth_request) if auth_request.user_id: return self._token_authenticate(auth_request) # Create a guest user. if auth_request.first_name: return self._create_guest_user(auth_request) logging.warning('No mean of authentication found:\n%s', auth_request) flask.abort( 422, i18n.flask_translate( "Aucun moyen d'authentification n'a été trouvé."))
def _abort_failed_login() -> None: flask.abort( 403, i18n.flask_translate( "L'email et le mot de passe ne correspondent pas. " + "Si vous avez déjà créé un compte mais que vous n'avez pas créé votre mot de passe, " + 'nous venons de vous envoyer un email pour vous connecter.'))
def _pe_connect_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: token_data = _get_oauth2_access_token( 'https://authentification-candidat.pole-emploi.fr/connexion/oauth2/access_token?' 'realm=/individu', code=auth_request.pe_connect_code, client_id=_EMPLOI_STORE_CLIENT_ID or '', client_secret=_EMPLOI_STORE_CLIENT_SECRET or '', auth_name='PE Connect', ) if token_data.get('nonce') != auth_request.pe_connect_nonce: flask.abort(403, i18n.flask_translate('Mauvais paramètre nonce')) bearer = token_data.get('token_type', 'Bearer') access_token = token_data.get('access_token', '') authorization_header = f'{bearer} {access_token}' user_info_response = requests.get( 'https://api.emploi-store.fr/partenaire/peconnect-individu/v1/userinfo', headers={'Authorization': authorization_header}) if user_info_response.status_code < 200 or user_info_response.status_code >= 400: logging.warning('PE Connect fails (%d): "%s"', user_info_response.status_code, user_info_response.text) flask.abort(403, user_info_response.text) user_info = typing.cast(dict[str, str], user_info_response.json()) response = auth_pb2.AuthResponse() user_dict = self._user_collection.find_one( {'peConnectId': user_info['sub']}) if proto.parse_from_mongo(user_dict, response.authenticated_user, 'user_id'): self._handle_returning_user(response) else: user = response.authenticated_user is_existing_user = self._load_user_from_token_or_email( auth_request, user, user_info.get('email')) user.pe_connect_id = user_info['sub'] response.is_new_user = force_update = not user.has_account user.has_account = True if is_existing_user: self._handle_returning_user(response, force_update=force_update) else: # TODO(pascal): Handle the case where one of the name is missing. user.profile.name = french.cleanup_firstname( user_info.get('given_name', '')) user.profile.last_name = french.cleanup_firstname( user_info.get('family_name', '')) user.profile.gender = _PE_CONNECT_GENDER.get( user_info.get('gender', ''), user_profile_pb2.UNKNOWN_GENDER) self.save_new_user(user, auth_request.user_data) response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def _decorated_fun(*args: Any, **kwargs: Any) -> Any: auth_token = flask.request.headers.get('Authorization', '').replace('Bearer ', '') if not auth_token: auth_token = flask.request.args.get('token', '') if not auth_token: auth_token = flask.request.cookies.get( _AUTH_TOKEN_COOKIE_NAME, '') if not auth_token: flask.abort(401, i18n.flask_translate('Token manquant')) user_id = get_user_id(*args, **kwargs) try: token.check_token(user_id, auth_token, role=role) except ValueError: flask.abort(403, i18n.flask_translate('Unauthorized token')) return func(*args, **kwargs)
def delete_user(*, user_id: str) -> Tuple[str, int]: """Delete a user from their internal ID.""" user_db = mongo.get_connections_from_env().user_db.with_prefix('jobflix_') auth_token = flask.request.headers.get('Authorization', '').replace('Bearer ', '') or \ flask.request.args.get('token', '') user_data = user_pb2.User(user_id=user_id) user_data.profile.email = flask.request.args.get('email', '') deleted_id = user.delete_user(user_data, auth_token, user_db=user_db) if not deleted_id: return i18n.flask_translate( "Nous n'avons pas trouvé votre email dans notre base de données.\n" 'Si vous ne vous étiez pas encore désabonné·e, ' 'contactez nous à [email protected] pour vous assurer de votre désinscription.'), 404 return i18n.flask_translate( 'Votre requête a été prise en compte.\n' 'Votre adresse email sera supprimée de la base Jobflix dans les prochaines 24 heures.'), 202
def _parse_user_from_mongo(user_dict: dict[str, Any], user: user_pb2.User) -> None: if not proto.parse_from_mongo(user_dict, user, 'user_id'): flask.abort( 500, i18n.flask_translate( 'Les données utilisateur sont corrompues dans la base de données.' ))
def get_more_jobs( user_proto: user_pb2.User, *, section_id: str, state: str) -> upskilling_pb2.Section: """Return more jobs for a given section.""" if not user_proto.projects: flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer.")) try: generator_id, section_state = state.split(':', 1) except ValueError: flask.abort( 422, i18n.flask_translate("Le paramètre d'état {state} n'a pas le bon format.") .format(state=state)) project = user_proto.projects[0] database = mongo.get_connections_from_env().stats_db scoring_project = scoring.ScoringProject(project, user_proto, database) try: generator = _SECTION_GENERATORS[generator_id] except KeyError: flask.abort( 404, i18n.flask_translate('Générateur de section inconnu: {generator_id}') .format(generator_id=generator_id)) try: section = generator.get_more_jobs( scoring_project=scoring_project, section_id=section_id, state=section_state) except _InvalidState: flask.abort( 422, i18n.flask_translate('Impossible de commencer à {start_from}') .format(start_from=section_state)) best_jobs_in_area = _get_best_jobs_in_area(scoring_project) are_all_jobs_hiring = _get_are_all_jobs_hiring() best_salaries = { job.job_group.rome_id for job in best_jobs_in_area.best_salaries_jobs} for job in section.jobs: _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring) return section
def get_project_data(user_proto: user_pb2.User, project_id: str) -> project_pb2.Project: """Get the data for a project or abort.""" try: return next(project for project in user_proto.projects if project.project_id == project_id) except StopIteration: flask.abort( 404, i18n.flask_translate('Projet "{project_id}" inconnu.').format( project_id=project_id))
def _token_authenticate( self, auth_request: auth_pb2.AuthRequest) -> auth_pb2.AuthResponse: instant = int(time.time()) response = auth_pb2.AuthResponse() response.hash_salt = token.timestamped_hash(instant, auth_request.email) self._load_user_with_token(auth_request, response.authenticated_user) if response.authenticated_user.HasField('deleted_at'): flask.abort(404, i18n.flask_translate('Compte supprimé')) response.auth_token = token.create_token(auth_request.user_id, 'auth') self._handle_returning_user(response) return response
def _email_register( self, auth_request: auth_pb2.AuthRequest, response: auth_pb2.AuthResponse) \ -> auth_pb2.AuthResponse: """Registers a new user using an email address.""" is_existing_user = self._load_user_with_token( auth_request, response.authenticated_user, is_timestamp_required=False) force_update = False if not is_existing_user and auth_request.hashed_password: if not auth_request.first_name: flask.abort( 422, i18n.flask_translate('Le champ first_name est nécessaire')) should_create_account = not is_existing_user and auth_request.hashed_password if is_existing_user or should_create_account: force_update |= response.authenticated_user.profile.email != auth_request.email response.authenticated_user.profile.email = auth_request.email response.authenticated_user.hashed_email = hash_user_email( auth_request.email) response.is_new_user = not response.authenticated_user.has_account force_update |= response.is_new_user response.authenticated_user.has_account = True force_update |= \ response.authenticated_user.has_password != bool(auth_request.hashed_password) response.authenticated_user.has_password = bool( auth_request.hashed_password) if is_existing_user: self._handle_returning_user(response, force_update=force_update) elif should_create_account: response.authenticated_user.profile.name = auth_request.first_name response.authenticated_user.profile.last_name = auth_request.last_name self.save_new_user(response.authenticated_user, auth_request.user_data) if auth_request.hashed_password: object_id = objectid.ObjectId(response.authenticated_user.user_id) self._user_db.user_auth.replace_one( {'_id': object_id}, {'hashedPassword': auth_request.hashed_password}, upsert=True) response.is_password_updated = True response.is_new_user = not is_existing_user # TODO(cyrille): Consider dropping if there's no user_id. response.auth_token = token.create_token( response.authenticated_user.user_id, 'auth') return response
def change_email(self, user_proto: user_pb2.User, auth_request: auth_pb2.AuthRequest) \ -> user_pb2.User: """Change user's email address.""" new_hashed_email = hash_user_email(auth_request.email) if user_proto.hashed_email == new_hashed_email: # Trying to set the same email. return user_proto user_auth_dict = self._user_db.user_auth.find_one( {'_id': objectid.ObjectId(user_proto.user_id)}) if user_auth_dict or auth_request.hashed_password: if not user_auth_dict: flask.abort( 422, i18n.flask_translate( "L'utilisateur n'a pas encore de mot de passe")) stored_hashed_password = user_auth_dict.get('hashedPassword', '') _check_password(stored_hashed_password, auth_request.hash_salt, auth_request.hashed_password) existing = self._user_collection.find_one( {'hashedEmail': new_hashed_email}, {'_id': 1}) if existing: flask.abort( 403, i18n.flask_translate( 'L\'email "{email}" est déjà utilisé par un autre compte'). format(email=auth_request.email)) user_proto.profile.email = auth_request.email user_proto.hashed_email = new_hashed_email if user_auth_dict: self._user_db.user_auth.replace_one( {'_id': objectid.ObjectId(user_proto.user_id)}, {'hashedPassword': auth_request.new_hashed_password}) self._update_returning_user(user_proto, force_update=True) return user_proto
def _load_user_with_token(self, auth_request: auth_pb2.AuthRequest, out_user: user_pb2.User, is_timestamp_required: bool = True) -> bool: if not auth_request.user_id: return False try: user_id = objectid.ObjectId(auth_request.user_id) except bson.errors.InvalidId: flask.abort( 400, i18n.flask_translate( 'L\'identifiant utilisateur "{user_id}" n\'a pas le bon format.', ).format(user_id=auth_request.user_id)) try: if not token.assert_valid_salt( auth_request.auth_token, str(user_id), int(time.time()), validity_seconds=datetime.timedelta(days=5).total_seconds(), role='auth') \ and is_timestamp_required: raise ExpiredTokenException( i18n.flask_translate("Token d'authentification périmé")) except ValueError as error: flask.abort( 403, i18n.flask_translate( "Le sel n'a pas été généré par ce serveur\xa0: {error}."). format(error=error)) user_dict = self._user_collection.find_one({'_id': user_id}) if not user_dict: flask.abort(404, i18n.flask_translate('Utilisateur inconnu.')) _parse_user_from_mongo(user_dict, out_user) return True
def get_sections_for_project(user_proto: user_pb2.User) -> upskilling_pb2.Sections: """Return all the sections to browse.""" if not user_proto.projects: flask.abort(422, i18n.flask_translate("Il n'y a pas de projet à explorer.")) project = user_proto.projects[0] database = mongo.get_connections_from_env().stats_db scoring_project = scoring.ScoringProject(project, user_proto, database) result = upskilling_pb2.Sections() good_jobs = jobs.get_all_good_job_group_ids(scoring_project.database) best_salaries = { job.job_group.rome_id for job in _get_best_jobs_in_area(scoring_project).best_salaries_jobs} slots = list(_SECTION_SLOTS.get_collection(database)) are_all_jobs_hiring = _get_are_all_jobs_hiring() for section in slots: if section.is_for_alpha_only and not user_proto.features_enabled.alpha: continue generator_id = section.generator try: generator = _SECTION_GENERATORS[generator_id] except KeyError: logging.error('Unknown upskilling section generator "%s"', generator_id) continue computed_section = generator.get_jobs( scoring_project=scoring_project, allowed_job_ids=good_jobs, previous_sections={ section.id for section in result.sections if section.state.startswith(f'{generator_id}:') }) if not computed_section or len(computed_section.jobs) < 2: continue result.sections.add( id=computed_section.new_id or generator_id, state=f'{generator_id}:{computed_section.state or ""}', name=scoring_project.populate_template(scoring_project.translate_key_string( f'jobflix_sections:{computed_section.new_id or generator_id}', hint=computed_section.new_name or generator.name, context=_get_bob_deployment(), is_hint_static=True)), jobs=[ _add_perks_to_job(job, best_salaries, is_hiring=are_all_jobs_hiring) for job in computed_section.jobs], ) return result
def _get_google_email(authorization: str) -> str: try: token.check_admin_token(authorization) return '*****@*****.**' except ValueError: pass if authorization.startswith('Bearer '): authorization = authorization.removeprefix('Bearer ') else: authorization = flask.request.args.get('token', '') if not authorization: flask.abort(401, i18n.flask_translate('Token manquant')) try: return token.check_google_token(authorization, emails_regexp) except i18n.TranslatableException as error: flask.abort(401, error.flask_translate())
def _get_auth_error_message() -> str: return i18n.flask_translate( "Les informations d'authentification ne sont pas valides.")