Beispiel #1
0
 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
Beispiel #2
0
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)}")
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #5
0
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")
Beispiel #6
0
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
Beispiel #7
0
 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
Beispiel #8
0
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
Beispiel #9
0
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)
Beispiel #10
0
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)
Beispiel #11
0
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