def wrapper(*args, **kwargs): try: session.modified = True auth_filter(current_app.app_config) body, status = f(*args, **kwargs) response = jsonify(body) _audit_trail() _add_custom_header(response) db.session.commit() return response, status except Exception as e: response = jsonify(message=e.description if isinstance(e, HTTPException) else str(e), error=True) response.status_code = 500 ctx_logger("base").exception(response) if isinstance(e, NoResultFound): response.status_code = 404 elif isinstance(e, HTTPException): response.status_code = e.code elif isinstance(e, ValidationError): response.status_code = 400 _add_custom_header(response) db.session.rollback() # We want to send emails if the exception is unexpected and validation errors should not happen server-side if response.status_code == 500 or response.status_code == 400: send_error_mail(tb=traceback.format_exc()) return response
def _audit_trail(): method = current_request.method if method in _audit_trail_methods: # Prevent logo base64 logging body = current_request.json if method != "DELETE" and current_request.is_json else {} if isinstance(body, dict): body.pop("logo", None) ctx_logger("base").info(f"Path {current_request.path} {method} {json.dumps(body, default=str)}")
def error(): js_dump = json.dumps(current_request.json, default=str) ctx_logger("user").exception(js_dump) mail_conf = current_app.app_config.mail if mail_conf.send_js_exceptions and not os.environ.get("TESTING"): user = current_user() user_id = user.get("email", user.get("name")) mail_error(mail_conf.environment, user_id, mail_conf.send_exceptions_recipients, js_dump) return {}, 201
def me(): if "user" in session and not session["user"]["guest"]: user_from_session = session["user"] user_from_db = _user_query() \ .filter(User.id == user_from_session["id"]) \ .first() if user_from_db is None: return {"uid": "anonymous", "guest": True, "admin": False}, 200 if user_from_db.suspended: logger = ctx_logger("user") logger.info( f"Returning error for user {user_from_db.uid} as user is suspended") return {"error": f"user {user_from_db.uid} is suspended"}, 409 # Do not expose the actual secret of second_factor_auth user_from_session["second_factor_auth"] = bool(user_from_db.second_factor_auth) # Do not send all information if second_factor is required if not user_from_session["second_factor_confirmed"]: return user_from_session, 200 user = {**jsonify(user_from_db).json, **user_from_session} if len(user_from_db.suspend_notifications) > 0: user["successfully_activated"] = True user_from_db.suspend_notifications = [] db.session.merge(user_from_db) db.session.commit() _add_counts(user) _add_service_aups(user, user_from_db) return user, 200 else: return {"uid": "anonymous", "guest": True, "admin": False}, 200
def sfo(): logger = ctx_logger("oidc") oidc_config = current_app.app_config.oidc encoded_access_token = query_param("access_token") access_token = decode_jwt_token(encoded_access_token) logger.debug(f"MFA endpoint with access_token {access_token}") uid = access_token["sub"] user = User.query.filter(User.uid == uid).first() if not user: error_msg = f"Unknown user with sub {uid}" logger.error(error_msg) return redirect(f"{oidc_config.sfo_eduteams_redirect_uri}?error={quote(error_msg)}") if not oidc_config.second_factor_authentication_required: id_token = _construct_jwt(user, access_token.get("nonce", str(uuid4())), oidc_config) return redirect(f"{oidc_config.sfo_eduteams_redirect_uri}?id_token={id_token}") # need to remember this if the user response comes back session["in_proxy_flow"] = True store_user_in_session(user, False, user.has_agreed_with_aup()) return redirect(f"{current_app.app_config.base_url}/2fa")
def _do_send_mail(subject, recipients, template, context, preview, working_outside_of_request_context=False): recipients = recipients if isinstance(recipients, list) else list( map(lambda x: x.strip(), recipients.split(","))) mail_ctx = current_app.app_config.mail msg = Message(subject=subject, sender=(mail_ctx.get("sender_name", "SURF"), mail_ctx.get("sender_email", "*****@*****.**")), recipients=recipients, extra_headers={ "Auto-submitted": "auto-generated", "X-Auto-Response-Suppress": "yes", "Precedence": "bulk" }) msg.html = render_template(f"{template}.html", **context) msg.body = render_template(f"{template}.txt", **context) msg.msgId = f"<{str(uuid.uuid4())}@{os.uname()[1]}.internal.sram.surf.nl>".replace("-", ".") logger = logging.getLogger("mail") if working_outside_of_request_context else ctx_logger("user") logger.debug(f"Sending mail message with Message-id {msg.msgId}") suppress_mail = "suppress_sending_mails" in mail_ctx and mail_ctx.suppress_sending_mails open_mail_in_browser = current_app.config["OPEN_MAIL_IN_BROWSER"] if not preview and not suppress_mail and not open_mail_in_browser: mail = current_app.mail ctx = current_app.app_context() thr = Thread(target=_send_async_email, args=[ctx, msg, mail]) thr.start() if suppress_mail and not preview: logger.info(f"Sending mail {msg.html}") if open_mail_in_browser and not preview: _open_mail_in_browser(msg.html) return msg.html
def publish(self, topic, msg, qos=1): res = False if self.enabled: try: publish.single(topic, payload=msg, hostname=self.host, retain=False, qos=qos, client_id=self.client_id, auth=self.auth) res = True except Exception as e: logger = ctx_logger("mqtt_client") logger.error(f"Fail {e}") return res
def mfa_idp_allowed(user, schac_home=None, entity_id=None): logger = ctx_logger("user_api") idp_allowed = current_app.app_config.mfa_idp_allowed entity_id_allowed = entity_id and [idp for idp in idp_allowed if idp.entity_id == entity_id.lower()] schac_home_allowed = schac_home and [idp for idp in idp_allowed if idp.schac_home == schac_home.lower()] last_login_date = user.last_login_date minutes_ago = datetime.now() - timedelta(hours=0, minutes=int(current_app.app_config.mfa_sso_time_in_minutes)) valid_mfa_sso = last_login_date and last_login_date > minutes_ago result = entity_id_allowed or schac_home_allowed or valid_mfa_sso logger.debug(f"mfa_idp_allowed: {result} (entity_id_allowed={entity_id_allowed}, " f"schac_home_allowed={schac_home_allowed}, valid_mfa_sso={valid_mfa_sso}, " f"entity_id={entity_id}, schac_home={schac_home}, last_login={minutes_ago} minutes ago") return result
def delete_collaboration_membership(collaboration_id, user_id): if current_user_id() != int(user_id): confirm_collaboration_admin(collaboration_id) logger = ctx_logger("collaboration_membership_api") memberships = CollaborationMembership.query \ .filter(CollaborationMembership.collaboration_id == collaboration_id) \ .filter(CollaborationMembership.user_id == user_id) \ .all() for membership in memberships: db.session.delete(membership) logger.info(f"Deleted {len(memberships)} collaboration memberships of {user_id}") res = {'collaboration_id': collaboration_id, 'user_id': user_id} return (res, 204) if len(memberships) > 0 else (None, 404)
def resume_session(): logger = ctx_logger("oidc") cfg = current_app.app_config oidc_config = cfg.oidc code = query_param("code", required=False, default=None) if not code: # This means that we are not in the redirect callback, but at the redirect from eduTeams logger.debug("Redirect to login in resume-session to start OIDC flow") authorization_endpoint = _get_authorization_url() return redirect(authorization_endpoint) scopes = " ".join(oidc_config.scopes) payload = { "code": code, "grant_type": "authorization_code", "scope": scopes, "redirect_uri": oidc_config.redirect_uri } headers = { "Content-Type": "application/x-www-form-urlencoded", "Cache-Control": "no-cache", "Accept": "application/json, application/json;charset=UTF-8" } response = requests.post(oidc_config.token_endpoint, data=urllib.parse.urlencode(payload), headers=headers, auth=(oidc_config.client_id, oidc_config.client_secret)) if response.status_code != 200: return _redirect_with_error(logger, f"Server error: Token endpoint error (http {response.status_code}") token_json = response.json() access_token = token_json["access_token"] headers = { "Accept": "application/json, application/json;charset=UTF-8", "Authorization": f"Bearer {access_token}" } response = requests.get(oidc_config.userinfo_endpoint, headers=headers) if response.status_code != 200: return _redirect_with_error(logger, f"Server error: User info endpoint error (http {response.status_code}") logger = ctx_logger("user") user_info_json = response.json() logger.debug(f"Userinfo endpoint results {user_info_json}") uid = user_info_json["sub"] user = User.query.filter(User.uid == uid).first() if not user: user = User(uid=uid, created_by="system", updated_by="system") add_user_claims(user_info_json, uid, user) # last_login_date is set later in this method user.last_accessed_date = datetime.datetime.now() logger.info(f"Provisioning new user {user.uid}") else: logger.info(f"Updating user {user.uid} with new claims / updated at") add_user_claims(user_info_json, uid, user) encoded_id_token = token_json["id_token"] id_token = decode_jwt_token(encoded_id_token) no_mfa_required = not oidc_config.second_factor_authentication_required idp_mfa = id_token.get("acr") == ACR_VALUES idp_allowed = mfa_idp_allowed(user, user.schac_home_organisation, None) second_factor_confirmed = no_mfa_required or idp_mfa or idp_allowed if second_factor_confirmed: user.last_login_date = datetime.datetime.now() user = db.session.merge(user) db.session.commit() user_accepted_aup = user.has_agreed_with_aup() store_user_in_session(user, second_factor_confirmed, user_accepted_aup) if not user_accepted_aup: location = f"{cfg.base_url}/aup" elif not second_factor_confirmed: location = f"{cfg.base_url}/2fa" else: location = session.get("original_destination", cfg.base_url) return redirect(location)
def _do_attributes(uid, service_entity_id, not_authorized_func, authorized_func, require_2fa=False, issuer_id=None): confirm_read_access() logger = ctx_logger("user_api") service = Service.query.filter(Service.entity_id == service_entity_id).first() if not service: msg = f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id} " \ f"as the service is unknown" logger.error(msg) send_error_mail(tb=msg, session_exists=False) return not_authorized_func(service_entity_id, SERVICE_UNKNOWN) no_free_ride = not service.non_member_users_access_allowed user = User.query.filter(User.uid == uid).first() if not user: logger.error(f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id}" f" as the user is unknown") return not_authorized_func(service.name, USER_UNKNOWN) if user.suspended and no_free_ride: logger.error(f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id}" f" as the user is suspended") return not_authorized_func(service.name, USER_IS_SUSPENDED) connected_collaborations = [] memberships = [] for cm in user.collaboration_memberships: connected = list(filter(lambda s: s.id == service.id, cm.collaboration.services)) if connected or list(filter(lambda s: s.id == service.id, cm.collaboration.organisation.services)): connected_collaborations.append(cm.collaboration) memberships.append(cm) if not connected_collaborations and no_free_ride: logger.error(f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id}" f" as the service is not connected to any of the user collaborations") return not_authorized_func(service.name, SERVICE_NOT_CONNECTED) if all(coll.status != STATUS_ACTIVE for coll in connected_collaborations) and no_free_ride: logger.error(f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id}" f" as the service is not connected to any active collaborations") return not_authorized_func(service.name, COLLABORATION_NOT_ACTIVE) if all(m.is_expired() for m in memberships) and no_free_ride: logger.error(f"Returning unauthorized for user {uid} and service_entity_id {service_entity_id}" f" as none of the collaboration memberships are active") return not_authorized_func(service.name, MEMBERSHIP_NOT_ACTIVE) # Leave the 2FAand AUP checks as the last checks as these are the only exceptions that can be recovered from if require_2fa: idp_allowed = mfa_idp_allowed(user, user.schac_home_organisation, issuer_id) if not idp_allowed: logger.debug(f"Returning interrupt for user {uid} from issuer {issuer_id} to perform 2fa") return not_authorized_func(user, SECOND_FA_REQUIRED) if not has_agreed_with(user, service): logger.debug(f"Returning interrupt for user {uid} and service_entity_id {service_entity_id} to accept AUP") return not_authorized_func(service, AUP_NOT_AGREED) now = datetime.now() for coll in connected_collaborations: coll.last_activity_date = now db.session.merge(coll) user.last_accessed_date = now user.last_login_date = now user.suspend_notifications = [] user = db.session.merge(user) db.session.commit() all_memberships = user_memberships(user, connected_collaborations) all_attributes, http_status = authorized_func(user, all_memberships) logger.info(f"Returning attributes {all_attributes} for user {uid} and service_entity_id {service_entity_id}") return all_attributes, http_status