Beispiel #1
0
def oauth_register_POST():
    valid = Validation(request)

    client_name = valid.require("client-name")
    redirect_uri = valid.require("redirect-uri")

    valid.expect(not redirect_uri or valid_url(redirect_uri),
            "Must be a valid HTTP or HTTPS URI", field="redirect-uri")

    if not valid.ok:
        return render_template("oauth-register.html",
                client_name=client_name,
                redirect_uri=redirect_uri,
                valid=valid)

    client = OAuthClient(current_user, client_name, redirect_uri)
    secret = client.gen_client_secret()
    session["client_id"] = client.client_id
    session["client_secret"] = secret
    session["client_event"] = "registered"
    db.session.add(client)
    audit_log("register oauth client",
            "Registered OAuth client {}".format(client.client_id))
    db.session.commit()
    return redirect("/oauth/registered")
Beispiel #2
0
def forgot_POST():
    valid = Validation(request)
    email = valid.require("email", friendly_name="Email")
    if not valid.ok:
        return render_template("forgot.html", **valid.kwargs)
    user = User.query.filter(User.email == email).first()
    valid.expect(user, "No account found with this email address.")
    if not valid.ok:
        return render_template("forgot.html", **valid.kwargs)
    factors = (UserAuthFactor.query.filter(
        UserAuthFactor.user_id == user.id)).all()
    valid.expect(
        not any(f for f in factors
                if f.factor_type in [FactorType.totp, FactorType.u2f]),
        "This account has two-factor authentication enabled, contact support.")
    if not valid.ok:
        return render_template("forgot.html", **valid.kwargs)
    rh = user.gen_reset_hash()
    db.session.commit()
    send_email(
        'reset_pw',
        user.email,
        'Reset your password on {}'.format(site_name),
        headers={
            "From":
            f"{cfg('sr.ht', 'owner-name')} <*****@*****.**>",
            "To":
            "{} <{}>".format(user.username, user.email),
            "Reply-To":
            f"{cfg('sr.ht', 'owner-name')} <{cfg('sr.ht', 'owner-email')}>",
        },
        user=user)
    audit_log("password reset requested", user=user)
    return render_template("forgot.html", done=True)
Beispiel #3
0
def logout():
    if current_user:
        audit_log("logged out")
        logout_user()
        db.session.commit()
        metrics.meta_logouts.inc()
    return redirect("/login")
Beispiel #4
0
def ssh_keys_POST():
    user = User.query.get(current_user.id)
    valid = Validation(request)

    ssh_key = valid.require("ssh-key")
    if valid.ok:
        try:
            parsed_key = ssh.SSHKey(ssh_key)
            valid.expect(parsed_key.bits, "This is not a valid SSH key",
                         "ssh-key")
        except:
            valid.error("This is not a valid SSH key", "ssh-key")
    if valid.ok:
        fingerprint = parsed_key.hash_md5()[4:]
        valid.expect(SSHKey.query\
            .filter(SSHKey.fingerprint == fingerprint) \
            .count() == 0, "We already have this SSH key on file.", "ssh-key")

    if not valid.ok:
        return render_template("keys.html",
                               current_user=user,
                               ssh_key=ssh_key,
                               valid=valid)

    key = SSHKey(user, ssh_key, fingerprint, parsed_key.comment)
    db.session.add(key)
    audit_log("ssh key added", 'Added SSH key {}'.format(fingerprint))
    db.session.commit()
    return redirect("/keys")
Beispiel #5
0
def client_delete_POST(client_id):
    client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first()
    if not client or client.user_id != current_user.id:
        abort(404)
    audit_log("deleted oauth client", "Deleted OAuth client {}".format(client_id))
    db.session.delete(client)
    db.session.commit()
    return redirect("/oauth")
Beispiel #6
0
def revoke_tokens_POST(client_id):
    client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first()
    if not client or client.user_id != current_user.id:
        abort(404)
    OAuthToken.query.filter(OAuthToken.client_id == client.id).delete()
    audit_log("revoked oauth tokens",
            "Revoked all OAuth tokens for {}".format(client_id))
    db.session.commit()
    return redirect("/oauth")
Beispiel #7
0
def pgp_keys_delete(key_id):
    user = User.query.get(current_user.id)
    key = PGPKey.query.get(int(key_id))
    if not key or key.user_id != user.id:
        abort(404)
    audit_log("pgp key deleted", 'Deleted PGP key {}'.format(key.key_id))
    db.session.delete(key)
    db.session.commit()
    return redirect("/keys")
Beispiel #8
0
def ssh_keys_delete(key_id):
    user = User.query.get(current_user.id)
    key = SSHKey.query.get(int(key_id))
    if not key or key.user_id != user.id:
        abort(404)
    audit_log("ssh key deleted", 'Deleted SSH key {}'.format(key.fingerprint))
    db.session.delete(key)
    db.session.commit()
    return redirect("/keys")
Beispiel #9
0
def personal_token_POST():
    oauth_token = OAuthToken(current_user, None)
    token = oauth_token.gen_token()
    oauth_token._scopes = "*"
    audit_log("issued oauth token", "issued personal access token {}...".format(
        oauth_token.token_partial))
    db.session.add(oauth_token)
    db.session.commit()
    return render_template("oauth-personal-token.html", token=token)
Beispiel #10
0
def oauth_exchange_POST():
    valid = Validation(request)
    client_id = valid.require('client_id')
    client_secret = valid.require('client_secret')
    exchange = valid.require('exchange')
    if not valid.ok:
        return valid.response

    client = (OAuthClient.query.filter(
        OAuthClient.client_id == client_id)).one_or_none()
    valid.expect(client, 'Unknown client ID')
    if not valid.ok:
        return valid.response

    client_secret_hash = hashlib.sha512(client_secret.encode()).hexdigest()
    valid.expect(client_secret_hash == client.client_secret_hash,
                 'Invalid client secret')
    if not valid.ok:
        return valid.response

    stash = redis.get(exchange)
    valid.expect(stash, 'Exchange token expired')
    if not valid.ok:
        return valid.response

    stash = json.loads(stash.decode())
    redis.delete(exchange)

    user = stash.get('user_id')
    scopes = stash.get('scopes')
    user = User.query.filter(User.id == user).first()
    valid.expect(
        user, "Unknown user ID stored for "
        "this exchange token (this isn't supposed to happen")
    if not valid.ok:
        return valid.response

    previous = (OAuthToken.query.filter(OAuthToken.user_id == user.id).filter(
        OAuthToken.client_id == client.id)).one_or_none()
    if not previous:
        oauth_token = OAuthToken(user, client)
    else:
        oauth_token = previous
        previous.expires = datetime.utcnow() + timedelta(days=365)

    oauth_token.scopes = [OAuthScope(s) for s in scopes.split(",")]
    token = oauth_token.gen_token()
    if not client.preauthorized:
        audit_log("oauth token issued",
                  "issued oauth token {} to client {}".format(
                      oauth_token.token_partial, client.client_id),
                  user=user)
    if not previous:
        db.session.add(oauth_token)
    db.session.commit()

    return {"token": token, "expires": oauth_token.expires}
Beispiel #11
0
def reset_secret(client_id):
    client = OAuthClient.query.filter(OAuthClient.client_id == client_id).first()
    if not client or client.user_id != current_user.id:
        abort(404)
    secret = client.gen_client_secret()
    session["client_id"] = client.client_id
    session["client_secret"] = secret
    session["client_event"] = "reset-secret"
    audit_log("reset client secret",
            "Reset OAuth client secret for {}".format(client.client_id))
    db.session.commit()
    return redirect("/oauth/registered")
Beispiel #12
0
def security_totp_disable_POST():
    factor = UserAuthFactor.query \
            .filter(UserAuthFactor.user_id == current_user.id)\
            .filter(UserAuthFactor.factor_type == FactorType.totp)\
            .one_or_none()
    if not factor:
        return redirect("/security")
    db.session.delete(factor)
    audit_log("disabled two factor auth", 'Disabled TOTP')
    db.session.commit()
    metrics.meta_totp_disabled.inc()
    return redirect("/security")
Beispiel #13
0
def revoke_token_POST(token_id):
    token = OAuthToken.query.filter(OAuthToken.id == token_id).first()
    if not token or token.user_id != current_user.id:
        abort(404)
    if token.client:
        audit_log("revoked oauth token",
                "revoked access from {}".format(token.client.client_name))
    else:
        audit_log("revoked personal access token",
                "revoked {}...".format(token.token_partial))
    token.expires = datetime.utcnow()
    db.session.commit()
    return redirect("/oauth")
Beispiel #14
0
def totp_challenge_POST():
    user_id = session.get('authorized_user')
    factors = session.get('extra_factors')
    return_to = session.get('return_to') or '/'
    if not user_id or not factors:
        return redirect("/login")
    valid = Validation(request)

    code = valid.require('code')

    if not valid.ok:
        return render_template("totp-challenge.html",
                               return_to=return_to,
                               valid=valid)
    code = code.replace(" ", "")
    try:
        code = int(code)
    except:
        valid.error("This TOTP code is invalid (expected a number)",
                    field="code")
    if not valid.ok:
        return render_template("totp-challenge.html",
                               return_to=return_to,
                               valid=valid)

    factor = UserAuthFactor.query.get(factors[0])
    secret = factor.secret.decode('utf-8')

    valid.expect(totp(secret, code),
                 'The code you entered is incorrect.',
                 field='code')

    if not valid.ok:
        return render_template("totp-challenge.html",
                               valid=valid,
                               return_to=return_to)

    factors = factors[1:]
    if len(factors) != 0:
        return get_challenge(factors[0])

    del session['authorized_user']
    del session['extra_factors']
    del session['return_to']

    user = User.query.get(user_id)
    login_user(user, remember=True)
    audit_log("logged in")
    db.session.commit()
    metrics.meta_logins_success.inc()
    return redirect(return_to)
Beispiel #15
0
def pgp_keys_POST():
    user = User.query.get(current_user.id)
    valid = Validation(request)

    pgp_key = valid.require("pgp-key")
    valid.expect(
        not pgp_key or len(pgp_key) < 32768,
        Markup("Maximum encoded key length is 32768 bytes. "
               "Try <br /><code>gpg --armor --export-options export-minimal "
               "--export &lt;fingerprint&gt;</code><br /> to export a "
               "smaller key."),
        field="pgp-key")
    if valid.ok:
        try:
            key = pgpy.PGPKey()
            key.parse(pgp_key.replace('\r', '').encode('utf-8'))
        except:
            valid.error("This is not a valid PGP key", field="pgp-key")
        valid.expect(any(key.userids),
                     "This key has no user IDs",
                     field="pgp-key")
        try:
            prepare_email("test", user.email, "test", encrypt_key=pgp_key)
        except:
            valid.error(
                "We were unable to encrypt a test message with this key",
                field="pgp-key")
    if valid.ok:
        valid.expect(PGPKey.query\
            .filter(PGPKey.user_id == user.id) \
            .filter(PGPKey.key_id == key.fingerprint)\
            .count() == 0, "This is a duplicate key", field="pgp-key")
    if not valid.ok:
        return render_template("keys.html",
                               current_user=user,
                               pgp_key=pgp_key,
                               valid=valid)

    pgp = PGPKey(user, pgp_key, key.fingerprint, key.userids[0].email)
    db.session.add(pgp)
    audit_log("pgp key added", 'Added PGP key {}'.format(key.fingerprint))
    db.session.commit()
    return redirect("/keys")
Beispiel #16
0
def new_payment_POST():
    valid = Validation(request)
    term = valid.require("term")
    token = valid.require("stripe-token")
    if not valid.ok:
        return "Invalid form submission", 400
    if not current_user.stripe_customer:
        new_customer = True
        try:
            customer = stripe.Customer.create(
                    description="~" + current_user.username,
                    email=current_user.email,
                    card=token)
            current_user.stripe_customer = customer.id
            current_user.payment_due = datetime.utcnow() + timedelta(seconds=-1)
        except stripe.error.CardError as e:
            details = e.json_body["error"]["message"]
            return render_template("new-payment.html",
                    amount=current_user.payment_cents, error=details)
    else:
        new_customer = False
        try:
            customer = stripe.Customer.retrieve(current_user.stripe_customer)
            source = customer.sources.create(source=token)
            customer.default_source = source.stripe_id
            customer.save()
        except stripe.error.CardError as e:
            details = e.json_body["error"]["message"]
            return render_template("new-payment.html",
                    amount=current_user.payment_cents, error=details)
    audit_log("billing", "New payment method handed")
    current_user.payment_interval = PaymentInterval(term)
    success, details = charge_user(current_user)
    if not success:
        return render_template("new-payment.html",
                amount=current_user.payment_cents, error=details)
    db.session.commit()
    if new_customer:
        return redirect(onboarding_redirect)
    session["message"] = "Your payment method was updated."
    return redirect(url_for("billing.billing_GET"))
Beispiel #17
0
def charge_user(user):
    if user.user_type == UserType.active_free:
        return ChargeResult.account_current, "Your account is exempt from payment."
    if user.payment_due >= datetime.utcnow():
        return ChargeResult.account_current, "Your account is current."
    desc = f"{cfg('sr.ht', 'site-name')} {user.payment_interval.value} payment"
    if user.payment_cents == 0:
        # They cancelled their payment and their current term is up
        user.user_type = UserType.active_non_paying
        return ChargeResult.cancelled, "Your paid service has been cancelled."
    try:
        amount = user.payment_cents
        if user.payment_interval == PaymentInterval.yearly:
            amount = amount * 10  # Apply yearly discount
        # TODO: Multiple currencies
        charge = stripe.Charge.create(amount=amount,
                                      currency="usd",
                                      customer=user.stripe_customer,
                                      description=desc)
        audit_log("billing", details="charged ${:.2f}".format(amount / 100))
    except stripe.error.CardError as e:
        details = e.json_body["error"]["message"]
        user.user_type = UserType.active_delinquent
        return ChargeResult.failed, details
    invoice = Invoice()
    invoice.cents = amount
    invoice.user_id = user.id
    try:
        invoice.source = f"{charge.source.brand} ending in {charge.source.last4}"
    except:
        # Not a credit card? dunno how this works
        invoice.source = charge.source.stripe_id
    db.session.add(invoice)
    if user.payment_interval == PaymentInterval.monthly:
        invoice.valid_thru = datetime.utcnow() + timedelta(days=30)
        user.payment_due = invoice.valid_thru
    else:
        invoice.valid_thru = datetime.utcnow() + timedelta(days=365)
        user.payment_due = invoice.valid_thru
    user.user_type = UserType.active_paying
    return ChargeResult.success, "Your card was successfully charged. Thank you!"
Beispiel #18
0
def reset_POST(token):
    user = User.query.filter(User.reset_hash == token).first()
    if not user:
        abort(404)
    if user.reset_expiry < datetime.utcnow():
        abort(404)
    valid = Validation(request)
    password = valid.require("password", friendly_name="Password")
    if not valid.ok:
        return render_template("reset.html", valid=valid)
    valid.expect(8 <= len(password) <= 512,
                 "Password must be between 8 and 512 characters.", "password")
    if not valid.ok:
        return render_template("reset.html", valid=valid)
    user.password = bcrypt.hashpw(password.encode('utf-8'),
                                  salt=bcrypt.gensalt()).decode('utf-8')
    audit_log("password reset", user=user)
    db.session.commit()
    login_user(user, remember=True)
    metrics.meta_pw_resets.inc()
    return redirect("/")
Beispiel #19
0
def privacy_POST():
    valid = Validation(request)

    key_id = valid.require("pgp-key")
    key_id = key_id if key_id != "null" else None
    key = None

    if key_id:
        key = PGPKey.query.get(int(key_id))
        valid.expect(key.user_id == current_user.id, "Invalid PGP key")

    if not valid.ok:
        return redirect("/privacy")

    user = User.query.get(current_user.id)
    user.pgp_key = key
    audit_log("changed pgp key",
            "Set default PGP key to {}".format(key.key_id if key else None))
    db.session.commit()

    return redirect("/privacy")
Beispiel #20
0
def login_POST():
    if current_user:
        return redirect("/")
    valid = Validation(request)

    username = valid.require("username", friendly_name="Username")
    password = valid.require("password", friendly_name="Password")
    return_to = valid.optional("return_to", "/")

    if not valid.ok:
        return render_template("login.html", valid=valid), 400

    user = User.query.filter(User.username == username.lower()).one_or_none()

    valid.expect(user is not None, "Username or password incorrect")

    if valid.ok:
        valid.expect(
            bcrypt.checkpw(password.encode('utf-8'),
                           user.password.encode('utf-8')),
            "Username or password incorrect")

    if not valid.ok:
        metrics.meta_logins_failed.inc()
        return render_template("login.html", username=username, valid=valid)

    factors = UserAuthFactor.query \
        .filter(UserAuthFactor.user_id == user.id).all()

    if any(factors):
        session['extra_factors'] = [f.id for f in factors]
        session['authorized_user'] = user.id
        session['return_to'] = return_to
        return get_challenge(factors[0])

    login_user(user, remember=True)
    audit_log("logged in")
    db.session.commit()
    metrics.meta_logins_success.inc()
    return redirect(return_to)
Beispiel #21
0
def security_totp_enable_POST():
    valid = Validation(request)

    secret = valid.require("secret")
    code = valid.require("code")
    
    if not valid.ok:
        return render_template("totp-enable.html",
            qrcode=totp_get_qrcode(secret),
            otpauth_uri=otpauth_uri(secret),
            secret=secret, valid=valid), 400
    code = code.replace(" ", "")
    try:
        code = int(code)
    except:
        valid.error(
                "This TOTP code is invalid (expected a number)", field="code")
    if not valid.ok:
        return render_template("totp-enable.html",
            qrcode=totp_get_qrcode(secret),
            otpauth_uri=otpauth_uri(secret),
            secret=secret, valid=valid), 400

    valid.expect(totp(secret, code),
            "The code you entered is incorrect.", field="code")

    if not valid.ok:
        return render_template("totp-enable.html",
            qrcode=totp_get_qrcode(secret),
            otpauth_uri=otpauth_uri(secret),
            secret=secret, valid=valid), 400

    factor = UserAuthFactor(current_user, FactorType.totp)
    factor.secret = secret.encode('utf-8')
    db.session.add(factor)
    audit_log("enabled two factor auth", 'Enabled TOTP')
    db.session.commit()
    metrics.meta_totp_enabled.inc()
    return redirect("/security")
Beispiel #22
0
def confirm_account(token):
    if current_user:
        return redirect(onboarding_redirect)
    user = User.query.filter(User.confirmation_hash == token).one_or_none()
    if not user:
        return render_template("already-confirmed.html",
                               redir=onboarding_redirect)
    if user.new_email:
        user.confirmation_hash = None
        audit_log("email updated",
                  "{} became {}".format(user.email, user.new_email))
        user.email = user.new_email
        user.new_email = None
        db.session.commit()
    elif user.user_type == UserType.unconfirmed:
        user.confirmation_hash = None
        user.user_type = UserType.active_non_paying
        audit_log("account created")
        db.session.commit()
        login_user(user, remember=True)
    if cfg("meta.sr.ht::billing", "enabled") == "yes":
        return redirect(url_for("billing.billing_initial_GET"))
    metrics.meta_confirmations.inc()
    return redirect(onboarding_redirect)
Beispiel #23
0
def profile_POST():
    valid = Validation(request)

    user = User.query.filter(User.id == current_user.id).one()

    email = valid.optional("email", user.email)
    email = email.strip()
    url = valid.optional("url", user.url)
    location = valid.optional("location", user.location)
    bio = valid.optional("bio", user.bio)

    valid.expect(not url or 0 <= len(url) <= 256,
                 "URL must fewer than 256 characters.", "url")
    valid.expect(not url or valid_url(url),
                 "URL must be a valid http or https URL", "url")
    valid.expect(not location or 0 <= len(location) <= 256,
                 "Location must fewer than 256 characters.", "location")
    valid.expect(not bio or 0 <= len(bio) <= 4096,
                 "Bio must fewer than 4096 characters.", "bio")

    if not valid.ok:
        return render_template("profile.html",
                               email=email,
                               url=url,
                               location=location,
                               bio=bio,
                               valid=valid), 400

    user.url = url
    user.location = location
    user.bio = bio

    new_email = user.email != email
    if new_email:
        valid.expect(
            len(email) <= 256, "Email must be no more than 256 characters.",
            "email")
        prev = User.query.filter(User.email == email).first()
        valid.expect(not prev, "This email address is already in use.",
                     "email")
        if not valid.ok:
            return render_template("profile.html",
                                   email=email,
                                   url=url,
                                   location=location,
                                   bio=bio,
                                   valid=valid), 400
        user.new_email = email
        user.gen_confirmation_hash()
        send_email('update_email_old',
                   user.email,
                   'Your {} email address is changing'.format(site_name),
                   new_email=email)
        send_email('update_email_new',
                   user.new_email,
                   'Confirm your {} email address change'.format(site_name),
                   new_email=email)

    audit_log("updated profile")
    db.session.commit()
    UserWebhook.deliver(UserWebhook.Events.profile_update, user.to_dict(),
                        UserWebhook.Subscription.user_id == user.id)

    return render_template("profile.html", new_email=new_email)
Beispiel #24
0
def cancel_POST():
    current_user.payment_cents = 0
    db.session.commit()
    audit_log("billing", "Plan cancelled (will not renew)")
    return redirect(url_for("billing.billing_GET"))