Exemplo n.º 1
0
def show(id: int, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 1
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        query = (Query.from_(auth_methods).select(
            auth_methods.human,
            auth_methods.deleted).where(auth_methods.id == Parameter('%s')))
        args = [id]

        if not can_view_others_auth_methods:
            query = query.where(auth_methods.user_id == Parameter('%s'))
            args.append(user_id)

        if not can_view_deleted_auth_methods:
            query = query.where(auth_methods.deleted.eq(False))

        itgs.read_cursor.execute(query.get_sql(), args)
        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        (main, deleted) = row

        authtokens = Table('authtokens')
        itgs.read_cursor.execute(
            Query.from_(authtokens).select(Count(
                Star())).where(authtokens.expires_at < Now()).where(
                    authtokens.source_type == Parameter('%s')).where(
                        authtokens.source_id == Parameter('%s')).get_sql(),
            ('password_authentication', id))
        (active_grants, ) = itgs.read_cursor.fetchone()

        return JSONResponse(status_code=200,
                            content=models.AuthMethod(
                                main=main,
                                deleted=deleted,
                                active_grants=active_grants).dict(),
                            headers={'x-request-cost': str(request_cost)})
Exemplo n.º 2
0
def delete_all_sessions(id: int, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms
        can_modify_others_auth_methods = (
            helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM in perms)

        auth_methods = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.deleted, auth_methods.user_id).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))

        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        (deleted, auth_method_user_id) = row
        if deleted and not can_view_deleted_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_view_others_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_modify_others_auth_methods:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        authtokens = Table('authtokens')
        itgs.write_cursor.execute(
            Query.from_(authtokens).delete().where(
                authtokens.source_type == Parameter('%s')).where(
                    authtokens.source_id == Parameter('%s')).get_sql(),
            ('password_authentication', id))
        itgs.write_conn.commit()
        return Response(status_code=200,
                        headers={'x-request-cost': str(request_cost)})
Exemplo n.º 3
0
def show_user_history_event(req_user_id: int,
                            event_id: int,
                            authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 1
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization,
            (settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION,
             settings_helper.VIEW_SETTING_CHANGE_AUTHORS_PERMISSION,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=404, headers=headers)

        can_see_others_settings = settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION in perms
        can_see_change_authors = settings_helper.VIEW_SETTING_CHANGE_AUTHORS_PERMISSION in perms

        changer_users = Table('users').as_('changer_users')
        events = Table('user_settings_events')
        query = (Query.from_(events).join(changer_users).on(
            changer_users.id == events.changer_user_id).select(
                events.user_id, events.changer_user_id, changer_users.username,
                events.property_name, events.old_value, events.new_value,
                events.created_at).where(events.id == Parameter('%s')))
        args = [event_id]
        if not can_see_others_settings:
            query = query.where(events.user_id == Parameter('%s'))
            args.append(user_id)

        itgs.read_cursor.execute(query.get_sql(), args)

        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404, headers=headers)

        (event_user_id, event_changer_user_id, event_changer_username,
         event_property_name, event_old_value, event_new_value,
         event_created_at) = row

        event = settings_models.UserSettingsEvent(
            name=event_property_name,
            old_value=json.loads(event_old_value),
            new_value=json.loads(event_new_value),
            username=(event_changer_username if
                      (event_changer_user_id == user_id
                       or can_see_change_authors) else None),
            occurred_at=event_created_at.timestamp())

        headers['Cache-Control'] = 'private, max-age=604800, immutable'
        return JSONResponse(status_code=200,
                            content=event.dict(),
                            headers=headers)
Exemplo n.º 4
0
def index(authorization=Header(None)):
    request_cost = 25

    with LazyItgs() as itgs:
        user_id, provided, perms = users.helper.get_permissions_from_header(
            itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS)

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if provided and user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        permissions = Table('permissions')
        itgs.read_cursor.execute(
            Query.from_(permissions).select(permissions.name).get_sql(), [])

        result = []
        row = itgs.read_cursor.fetchone()
        while row is not None:
            result.append(row[0])
            row = itgs.read_cursor.fetchone()

        return JSONResponse(
            status_code=200,
            content=models.PermissionsList(permissions=result).dict(),
            headers={
                'x-request-cost':
                str(request_cost),
                'cache-control':
                'public, max-age=604800, stale-while-revalidate=604800'
            })
Exemplo n.º 5
0
def show(permission: str, authorization=Header(None)):
    request_cost = 1

    with LazyItgs() as itgs:
        user_id, provided, perms = users.helper.get_permissions_from_header(
            itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS)

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if provided and user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        permissions = Table('permissions')
        itgs.read_cursor.execute(
            Query.from_(permissions).select(permissions.description).where(
                permissions.name == Parameter('%s')).get_sql(), (permission, ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=400,
                            headers={'x-request-cost': str(request_cost)})

        return JSONResponse(
            status_code=200,
            headers={
                'x-request-cost':
                str(request_cost),
                'cache-control':
                'public, max-age=604800, stale-while-revalidate=604800'
            },
            content=models.Permission(description=row[0]).dict())
Exemplo n.º 6
0
def show_detailed(loan_id: int, authorization: str = Header(None)):
    request_cost = 5
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.DELETED_LOANS_PERM, helper.VIEW_ADMIN_EVENT_AUTHORS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        basic = helper.get_basic_loan_info(itgs, loan_id, perms)
        if basic is None:
            return Response(status_code=404, headers=headers)

        events = helper.get_loan_events(itgs, loan_id, perms)

        etag = helper.calculate_etag(itgs, loan_id)
        headers['etag'] = etag
        headers['Cache-Control'] = 'public, max-age=604800'
        return JSONResponse(status_code=200,
                            content=models.DetailedLoanResponse(
                                events=events, basic=basic).dict(),
                            headers=headers)
Exemplo n.º 7
0
def lookup(q: str, authorization=Header(None)):
    """Allows looking up a user id by a username. q is the username to lookup"""
    request_cost = 1

    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS)

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        users = Table('users')
        itgs.read_cursor.execute(
            Query.from_(users).select(
                users.id).where(users.username == Parameter('%s')).get_sql(),
            (q.lower(), ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404, headers=headers)

        headers['Cache-Control'] = 'public, max-age=604800, immutable'
        return JSONResponse(
            status_code=200,
            content=models.UserLookupResponse(id=row[0]).dict(),
            headers=headers)
Exemplo n.º 8
0
def index_permissions(id: int, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 50
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        auth_perms = Table('password_auth_permissions')
        permissions = Table('permissions')

        query = (Query.from_(auth_methods).select(
            permissions.name).join(auth_perms).on(
                auth_perms.password_authentication_id == auth_methods.id).join(
                    permissions).on(
                        permissions.id == auth_perms.permission_id).where(
                            auth_methods.deleted.eq(False)).where(
                                auth_methods.id == Parameter('%s')))
        args = [id]

        if not can_view_others_auth_methods:
            query = query.where(auth_methods.user_id == Parameter('%s'))
            args.append(user_id)

        if not can_view_deleted_auth_methods:
            query = query.where(auth_methods.deleted.eq(False))

        itgs.read_cursor.execute(query.get_sql(), args)

        result = []
        row = itgs.read_cursor.fetchone()
        while row is not None:
            result.append(row[0])
            row = itgs.read_cursor.fetchone()

        return JSONResponse(
            status_code=200,
            headers={
                'x-request-cost': str(request_cost),
                'Cache-Control':
                'private, max-age=60, stale-while-revalidate=540'
            },
            content=models.AuthMethodPermissions(granted=result).dict())
Exemplo n.º 9
0
def show_authentication_methods(req_user_id: int, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 1
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization, (VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
                                  CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
                                  ADD_SELF_AUTHENTICATION_METHODS_PERM,
                                  ADD_OTHERS_AUTHENTICATION_METHODS_PERM,
                                  *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms
        can_add_self_auth_methods = ADD_SELF_AUTHENTICATION_METHODS_PERM in perms
        can_add_others_auth_methods = ADD_OTHERS_AUTHENTICATION_METHODS_PERM in perms

        if not can_view_others_auth_methods and req_user_id != user_id:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_add_more = ((req_user_id == user_id and can_add_self_auth_methods)
                        or can_add_others_auth_methods)
        auth_methods = Table('password_authentications')
        query = (Query.from_(auth_methods).select(
            auth_methods.id).where(auth_methods.user_id == Parameter('%s')))
        args = (req_user_id, )

        if not can_view_deleted_auth_methods:
            query = query.where(auth_methods.deleted.eq(False))
        else:
            query = query.orderby(auth_methods.deleted, order=Order.asc)

        query = query.orderby(auth_methods.id, order=Order.desc)
        itgs.read_cursor.execute(query.get_sql(), args)

        result = itgs.read_cursor.fetchall()
        result = [r[0] for r in result]
        return JSONResponse(
            status_code=200,
            content=settings_models.UserAuthMethodsList(
                authentication_methods=result,
                can_add_more=can_add_more).dict(),
            headers={
                'x-request-cost':
                str(request_cost),
                'cache-control':
                'private, max-age=86400, stale-while-revalidate=86400'
            })
Exemplo n.º 10
0
def show_stats(unit: str,
               frequency: str,
               request: Request,
               authorization=Header(None)):
    """Fetches the most recently calculated statistics using the given unit
    (either count or usd) and frequency (either monthly or quarterly). This
    endpoint normally costs nothing toward the ratelimit quota, however if
    cache-busting is detected (query parameters or via headers) then a cost
    is associated with the request.
    """
    request_cost = 25 if ratelimit_helper.is_cache_bust(request) else 0
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        if request_cost > 0:
            user_id, _, perms = users.helper.get_permissions_from_header(
                itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS)
            if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                    request_cost):
                return Response(status_code=429, headers=headers)

        if unit not in ('count', 'usd'):
            return JSONResponse(status_code=422,
                                headers=headers,
                                content={
                                    'detail': {
                                        'loc': ['unit'],
                                        'msg': 'Must be one of count, usd',
                                        'type': 'value_error'
                                    }
                                })

        if frequency not in ('monthly', 'quarterly'):
            return JSONResponse(status_code=422,
                                headers=headers,
                                content={
                                    'detail': {
                                        'loc': ['frequency'],
                                        'msg':
                                        'Must be one of monthly, quarterly',
                                        'type': 'value_error'
                                    }
                                })

        cache_key = f'stats/loans/{unit}/{frequency}'
        val = itgs.cache.get(cache_key)
        if val is None:
            return Response(status_code=404, headers=headers)

        headers['Cache-Control'] = (
            'public, max-age=86400, stale-if-error=86400, stale-while-revalidate=86400'
        )
        return Response(status_code=200,
                        content=val,
                        headers=headers,
                        media_type="application/json")
Exemplo n.º 11
0
def create_recheck(req: models.RecheckRequest, authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_attempt_cost = 5
    request_success_cost = 145

    headers = {'x-request-cost': str(request_attempt_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization, (helper.RECHECK_PERMISSION,
                                  *ratelimit_helper.RATELIMIT_PERMISSIONS))
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_attempt_cost):
            return Response(status_code=429, headers=headers)

        can_recheck = helper.RECHECK_PERMISSION in perms

        if not can_recheck:
            return Response(status_code=403, headers=headers)

        headers['x-request-cost'] = str(request_attempt_cost +
                                        request_success_cost)
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_success_cost):
            return Response(status_code=429, headers=headers)

        itgs.logger.print(
            Level.DEBUG,
            'User {} queued a request on https://www.reddit.com/comments/{}/lb/{}',
            user_id, req.link_fullname[3:], req.comment_fullname[3:])

        itgs.channel.queue_declare('lbrechecks')
        itgs.channel.basic_publish(
            '', 'lbrechecks',
            json.dumps({
                'link_fullname': req.link_fullname,
                'comment_fullname': req.comment_fullname
            }))
        return Response(status_code=202, headers=headers)
Exemplo n.º 12
0
def update_borrower_req_pm_opt_out(
    req_user_id: int,
    new_value: settings_models.UserSettingBoolChangeRequest,
    authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization,
            (settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION,
             settings_helper.EDIT_OTHERS_STANDARD_SETTINGS_PERMISSION,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=404, headers=headers)

        can_view_others_settings = settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION in perms
        can_edit_others_standard_settings = (
            settings_helper.EDIT_OTHERS_STANDARD_SETTINGS_PERMISSION in perms)

        if user_id != req_user_id:
            if not can_view_others_settings:
                return Response(status_code=404, headers=headers)

            users = Table('users')
            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).get_sql(), (req_user_id, ))

            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

            if not can_edit_others_standard_settings:
                return Response(status_code=403, headers=headers)

        changes = user_settings.set_settings(
            itgs, req_user_id, borrower_req_pm_opt_out=new_value.new_value)
        user_settings.create_settings_events(itgs,
                                             req_user_id,
                                             user_id,
                                             changes,
                                             commit=True)
        return Response(status_code=200, headers=headers)
Exemplo n.º 13
0
def suggest(q: str, limit: int = 3, authorization=Header(None)):
    """Suggest some usernames that partially match the query. q is the partial
    username string"""
    if limit <= 0:
        return JSONResponse(status_code=422,
                            content={
                                'detail': {
                                    'loc': ['limit'],
                                    'msg': 'Must be positive',
                                    'type': 'range_error'
                                }
                            })

    # This is actually moderately expensive since we don't use the correct
    # index for this, but we could switch to a real searching technique to
    # speed it up if this endpoint is problematic, so we'll under-charge here.
    request_cost = 10 + limit

    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS)

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        users = Table('users')
        itgs.read_cursor.execute(
            Query.from_(users).select(users.username).where(
                users.username.like(Parameter('%s'))).limit(limit).get_sql(),
            ('%' + q.lower() + '%', ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=204)
        result = []
        while row is not None:
            result.append(row[0])
            row = itgs.read_cursor.fetchone()

        headers[
            'Cache-Control'] = 'public, max-age=86400, stale-while-revalidate=518400'
        return JSONResponse(
            status_code=200,
            content=models.UserSuggestResponse(suggestions=result).dict(),
            headers=headers)
def get_creation_info(loan_id: int, request: Request):
    """Get the fully qualified url to the comment which spawned the given loan,
    if there is a corresponding url (i.e., the loan was spawned via reddit.)

    GET /api/get_creation_info.php?loan_id=57
    {
        "result_type": "LOAN_REQUEST_THREAD",
        "success": true,
        "request_thread": "https://..."
   }

    Arguments:
    - `loan_id (str)`: A single loan id to get the request thread for
    """
    request_cost = 5
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        auth = find_bearer_token(request)
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, auth, ratelimit_helper.RATELIMIT_PERMISSIONS)
        resp = try_handle_deprecated_call(itgs, request, SLUG, user_id=user_id)

        if resp is not None:
            return resp

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms, request_cost):
            return JSONResponse(
                content=RATELIMIT_RESPONSE.dict(),
                status_code=429,
                headers=headers
            )

        creation_infos = Table('loan_creation_infos')
        loans = Table('loans')
        itgs.read_cursor.execute(
            Query.from_(creation_infos)
            .join(loans)
            .on(loans.id == creation_infos.loan_id)
            .select(
                creation_infos.loan_id,
                creation_infos.type,
                creation_infos.parent_fullname,
                creation_infos.comment_fullname
            )
            .where(loans.deleted_at.isnull())
            .where(creation_infos.loan_id == Parameter('%s'))
            .get_sql(),
            (loan_id,)
        )

        row = itgs.read_cursor.fetchone()
        if row is None:
            return JSONResponse(
                content=PHPErrorResponse(
                    errors=[
                        PHPError(
                            error_type='LOAN_NOT_FOUND',
                            error_message='There is no loan with the specified id!'
                        )
                    ]
                ).dict(),
                status_code=404
            )

        (
            this_loan_id,
            this_type,
            this_parent_fullname,
            this_comment_fullname
        ) = row

        if this_type != 0:
            return JSONResponse(
                content=PHPErrorResponse(
                    errors=[
                        PHPError(
                            error_type='LOAN_EXISTS_NOT_BY_THREAD',
                            error_message=(
                                'The specified loan exists and has creation info, '
                                'but not as a reddit url'
                            )
                        )
                    ]
                ).dict(),
                status_code=404
            )

        headers['Cache-Control'] = 'public, max-age=86400'
        return JSONResponse(
            status_code=200,
            content=ResponseFormat(
                request_thread=(
                    'https://www.reddit.com/comments/{}/redditloans/{}'.format(
                        this_parent_fullname[3:],
                        this_comment_fullname[3:]
                    )
                )
            ).dict(),
            headers=headers
        )
Exemplo n.º 15
0
def index_loans(request: Request,
                id: int = 0,
                after_time: int = 0,
                before_time: int = 0,
                borrower_id: int = 0,
                lender_id: int = 0,
                includes_user_id: int = 0,
                borrower_name: str = '',
                lender_name: str = '',
                includes_user_name: str = '',
                principal_cents: int = 0,
                principal_repayment_cents: int = -1,
                unpaid: int = -1,
                repaid: int = -1,
                format: int = 2,
                limit: int = 10):
    id = _zero_to_none(id)
    after_time = _zero_to_none(after_time)
    before_time = _zero_to_none(before_time)
    borrower_id = _zero_to_none(borrower_id)
    lender_id = _zero_to_none(lender_id)
    includes_user_id = _zero_to_none(includes_user_id)
    borrower_name = _blank_to_none(borrower_name)
    lender_name = _blank_to_none(lender_name)
    includes_user_name = _blank_to_none(includes_user_name)
    principal_cents = _zero_to_none(principal_cents)
    principal_repayment_cents = _neg1_to_none(principal_repayment_cents)
    unpaid = _neg1_to_none(unpaid)
    repaid = _neg1_to_none(repaid)
    limit = _zero_to_none(limit)

    attempt_request_cost = 5
    headers = {'x-request-cost': str(attempt_request_cost)}
    with LazyItgs() as itgs:
        auth = find_bearer_token(request)
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, auth, ratelimit_helper.RATELIMIT_PERMISSIONS)
        resp = try_handle_deprecated_call(itgs, request, SLUG, user_id=user_id)

        if resp is not None:
            return resp

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                attempt_request_cost):
            return JSONResponse(content=RATELIMIT_RESPONSE.dict(),
                                status_code=429,
                                headers=headers)

        if limit is not None and (limit < 0 or limit >= 1000):
            return JSONResponse(content=PHPErrorResponse(errors=[
                PHPError(
                    error_type='INVALID_PARAMETER',
                    error_message=(
                        'Limit must be 0 or a positive integer less than 1000'
                    ))
            ]).dict(),
                                status_code=400)

        if limit is None and user_id is None:
            headers[
                'x-limit-warning'] = 'unauthed requests limit=0 replaced with limit=100'
            limit = 100

        if format not in (0, 1, 2, 3):
            return JSONResponse(content=PHPErrorResponse(errors=[
                PHPError(error_type='INVALID_PARAMETER',
                         error_message=('Format must be 0, 1, 2, or 3'))
            ]).dict(),
                                status_code=400)

        loans = Table('loans')
        if limit is None:
            real_request_cost = 100
        else:
            real_request_cost = min(100, limit)

        if format == 0:
            real_request_cost = math.ceil(math.log(real_request_cost + 1))
        elif format < 3:
            # Cost needs to be greater than loans show
            real_request_cost = 25 + real_request_cost * 2
        else:
            # We need to ensure the cost is greater than using the /users show
            # endpoint for getting usernames
            real_request_cost = 25 + math.ceil(real_request_cost * 4.1)

        headers['x-request-cost'] = str(attempt_request_cost +
                                        real_request_cost)
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                real_request_cost):
            return JSONResponse(content=RATELIMIT_RESPONSE.dict(),
                                status_code=429,
                                headers=headers)

        moneys = Table('moneys')
        principals = moneys.as_('principals')
        principal_repayments = moneys.as_('principal_repayments')

        usrs = Table('users')
        lenders = usrs.as_('lenders')
        borrowers = usrs.as_('borrowers')

        query = (Query.from_(loans).where(loans.deleted_at.isnull()).orderby(
            loans.id, order=Order.desc))
        params = []
        joins = set()

        def _add_param(val):
            params.append(val)
            return Parameter(f'${len(params)}')

        def _ensure_principals():
            nonlocal query
            if 'principals' in joins:
                return
            joins.add('principals')
            query = (query.join(principals).on(
                principals.id == loans.principal_id))

        def _ensure_principal_repayments():
            nonlocal query
            if 'principal_repayments' in joins:
                return
            joins.add('principal_repayments')
            query = (query.join(principal_repayments).on(
                principal_repayments.id == loans.principal_repayment_id))

        def _ensure_lenders():
            nonlocal query
            if 'lenders' in joins:
                return
            joins.add('lenders')
            query = (query.join(lenders).on(lenders.id == loans.lender_id))

        def _ensure_borrowers():
            nonlocal query
            if 'borrowers' in joins:
                return
            joins.add('borrowers')
            query = (query.join(borrowers).on(
                borrowers.id == loans.borrower_id))

        if id is not None:
            query = query.where(loans.id == _add_param(id))

        if after_time is not None:
            query = (query.where(loans.created_at > _add_param(
                datetime.fromtimestamp(after_time / 1000.0))))

        if before_time is not None:
            query = (query.where(loans.created_at < _add_param(
                datetime.fromtimestamp(before_time / 1000.0))))

        if principal_cents is not None:
            _ensure_principals()
            query = (query.where(
                principals.amount_usd_cents == _add_param(principal_cents)))

        if principal_repayment_cents is not None:
            _ensure_principal_repayments()
            query = (query.where(principal_repayments.amount_usd_cents ==
                                 _add_param(principal_repayment_cents)))

        if borrower_id is not None:
            query = (query.where(loans.borrower_id == _add_param(borrower_id)))

        if lender_id is not None:
            query = (query.where(loans.lender_id == _add_param(lender_id)))

        if includes_user_id is not None:
            prm = _add_param(includes_user_id)
            query = (query.where((loans.borrower_id == prm)
                                 | (loans.lender_id == prm)))

        if borrower_name is not None:
            _ensure_borrowers()
            query = (query.where(
                borrowers.username == _add_param(borrower_name.lower())))

        if lender_name is not None:
            _ensure_lenders()
            query = (query.where(
                lenders.username == _add_param(lender_name.lower())))

        if includes_user_name is not None:
            _ensure_lenders()
            _ensure_borrowers()
            prm = _add_param(includes_user_name)
            query = (query.where((lenders.username == prm)
                                 | (borrowers.username == prm)))

        if unpaid is not None:
            if unpaid:
                query = query.where(loans.unpaid_at.notnull())
            else:
                query = query.where(loans.unpaid_at.isnull())

        if repaid is not None:
            if repaid:
                query = query.where(loans.repaid_at.notnull())
            else:
                query = query.where(loans.repaid_at.isnull())

        if limit is not None:
            query = query.limit(limit)

        query = query.select(loans.id)
        if format > 0:
            _ensure_principals()
            _ensure_principal_repayments()
            event_tables = (Table('loan_repayment_events'),
                            Table('loan_unpaid_events'),
                            Table('loan_admin_events'))
            latest_events = Table('latest_events')
            query = (query.with_(
                Query.from_(loans).select(
                    loans.id.as_('loan_id'),
                    Greatest(loans.created_at,
                             *(tbl.created_at for tbl in event_tables
                               )).as_('latest_event_at')).groupby(loans.id),
                'latest_events').left_join(latest_events).on(
                    latest_events.loan_id == loans.id).select(
                        loans.lender_id, loans.borrower_id,
                        principals.amount_usd_cents,
                        principal_repayments.amount_usd_cents, (Case().when(
                            loans.unpaid_at.isnull(), 'false').else_('true')),
                        Cast(
                            Extract('epoch', loans.created_at) * 1000,
                            'bigint'),
                        Cast(
                            Extract('epoch', latest_events.latest_event_at) *
                            1000, 'bigint')))

            if format == 3:
                creation_infos = Table('loan_creation_infos')
                _ensure_borrowers()
                _ensure_lenders()
                query = (query.join(creation_infos).on(
                    creation_infos.loan_id == loans.id).select(
                        Function('SUBSTRING', creation_infos.parent_fullname,
                                 4),
                        Function('SUBSTRING', creation_infos.comment_fullname,
                                 4), lenders.username, borrowers.username))

        sql, args = convert_numbered_args(query.get_sql(), params)
        headers['Cache-Control'] = 'public, max-age=600'
        if format == 0:
            return _UltraCompactResponse((sql, args), headers,
                                         'LOANS_ULTRACOMPACT')
        elif format == 1:
            return _CompactResponse((sql, args), headers, 'LOANS_COMPACT')
        elif format == 2:
            return _StandardResponse((sql, args), headers, 'LOANS_STANDARD')
        else:
            return _ExtendedResponse((sql, args), headers, 'LOANS_EXTENDED')
def get_creation_info(loan_id: str, request: Request):
    """Get the creation information for a loan or multiple loans. The loan ids
    should be a space-separated list of loan ids, and this will return how each
    of the loans were created.

    This has been replaced by the event list on loans (see
    /api/loans/{id}/detailed). The result of this endpoint is just the first
    CreationLoanEvent on the Loan.

    GET /api/get_creation_info.php?loan_id=57+58+73+125
    {
        "result_type": "LOAN_CREATION_INFO",
        "success": true,
        "results": {
            "57": null // this means we found no creation info for the loan id 57
            "58": {
                "type": 0, // this means the loan was created due to an action on reddit
                "thread": "a valid url goes here" // where the action took place
            },
            "73": {
                "type": 1, // this means the loan was created on redditloans
                "user_id": 23 // this is the admin that created the loan
                              // Requires perm 'view_admin_event_authors'

            },
            "125": {
                "type": 2 // this is a loan that was created due to a paid
                          //  summon when the database was  being regenerated
                          // in ~march 2016, but no $loan command was ever found.
            }
        }
   }

    Arguments:
    - `loan_id (str)`: A space separated list of loan ids.
    """
    with LazyItgs() as itgs:
        auth = find_bearer_token(request)
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, auth, ratelimit_helper.RATELIMIT_PERMISSIONS)
        resp = try_handle_deprecated_call(itgs, request, SLUG, user_id=user_id)

        if resp is not None:
            return resp

        try:
            loan_ids = tuple(int(str_id) for str_id in loan_id.split(' '))
        except ValueError:
            return JSONResponse(
                status_code=400,
                content=PHPErrorResponse(errors=[
                    PHPError(
                        error_type='INVALID_ARGUMENT',
                        error_message=(
                            'Cannot parse given loan ids to numbers after '
                            'splitting using a space delimiter!'))
                ]).dict())

        if not loan_ids:
            return JSONResponse(
                status_code=400,
                content=PHPErrorResponse(errors=[
                    PHPError(
                        error_type='INVALID_ARGUMENT',
                        error_message='loan_id is required at this endpoint')
                ]).dict())

        request_cost = len(loan_ids) * 5 + max(
            1, math.ceil(math.log(len(loan_ids))))
        headers = {'x-request-cost': str(request_cost)}
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return JSONResponse(content=RATELIMIT_RESPONSE.dict(),
                                status_code=429,
                                headers=headers)

        creation_infos = Table('loan_creation_infos')
        loans = Table('loans')
        itgs.read_cursor.execute(
            Query.from_(creation_infos).join(loans).on(
                loans.id == creation_infos.loan_id).select(
                    creation_infos.loan_id, creation_infos.type,
                    creation_infos.parent_fullname,
                    creation_infos.comment_fullname).where(
                        loans.deleted_at.isnull()).where(
                            creation_infos.loan_id.isin([
                                Parameter('%s') for _ in loan_ids
                            ])).get_sql(), loan_ids)

        results = dict([lid, None] for lid in loan_ids)
        row = itgs.read_cursor.fetchone()
        while row is not None:
            (this_loan_id, this_type, this_parent_fullname,
             this_comment_fullname) = row

            if this_type == 0:
                results[this_loan_id] = {
                    'type':
                    0,
                    'thread':
                    'https://www.reddit.com/comments/{}/redditloans/{}'.format(
                        this_parent_fullname[3:], this_comment_fullname[3:])
                }
            else:
                results[this_loan_id] = {'type': this_type}

            row = itgs.read_cursor.fetchone()

        headers['Cache-Control'] = 'public, max-age=86400'
        return JSONResponse(status_code=200,
                            content=ResponseFormat(results=results).dict(),
                            headers=headers)
Exemplo n.º 17
0
def get_failure_response_or_user_id_and_perms_for_authorization(
        itgs: LazyItgs, authorization: str, check_request_cost: int,
        req_user_id: int, action_self_permission: str,
        action_other_permission: str, additional_permissions_for_result: list):
    """Check that the given authorization header is sufficient for viewing
    the given users demographics and performing the given action. This will
    either return a Response (or JSONResponse) containing the problem with
    the users authorization (check using isinstance(resp, Response)), or it
    will return (user_id, permissions), where user_id is the authorized users
    id and permissions is the subset of permissions on the user that they
    have and we checked.

    Arguments:
    - `itgs (LazyIntegrations)`: The lazy integrations to use for connecting
        to networked services to verify the authtoken.
    - `authorization (str)`: The authorization header passed by the user.
    - `check_requset_cost (int)`: The ratelimiting penalty for even checking if
        they have permission to do the request.
    - `req_user_id (int, None)`: The id of the user whose demographics
        information is being acted on. Should be None for the lookup action.
        If this is set, the view permission is implied.
    - `action_self_permission (str, None)`: The required permission if the
        authorized user is the requested user, e.g.,
        EDIT_SELF_DEMOGRAPHICS_PERMISSION. Should be omitted if `req_user_id`
        is `None`.
    - `action_other_permission (str)`: The required permission if the
        authorized user is not the requested user.
    - `additional_permissions_for_result (iterable[str])`: Permissions which we
        should check for when looking up the authtokens permissions in the
        database, even if this function doesn't use them. Ensures that if the
        token has this permission it will be returned within the permissions
        array in the success response.

    Returns (Failure Response):
    - `resp (fastapi.responses.Response)`: The response that should be
        returned to the user.

    Returns (Success Response):
    - `user_id (int)`: The primary key of the user authorized via the
        authorization header to make this request.
    - `permissions (list[str])`: The subset of permissions which the user has
        and we checked for.
    """
    authtoken_id = None
    user_id = None
    failure_type = None
    headers = {'x-request-cost': str(check_request_cost)}

    authtoken_provided = helper.get_authtoken_from_header(authorization)
    if authtoken_provided is None:
        failure_type = 401
    else:
        max_age = datetime.fromtimestamp(
            time.time() - MAX_AUTHTOKEN_AGE_FOR_DEMOGRAPHICS_SECONDS)
        auths = Table('authtokens')
        password_auths = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auths).select(auths.id,
                                      auths.user_id).join(password_auths).
            on(password_auths.id == auths.source_id).where(
                auths.source_type == Parameter('%s')).where(
                    auths.token == Parameter('%s')).where(
                        auths.created_at > Parameter('%s')).where(
                            password_auths.human.eq(True)).limit(1).get_sql(),
            ('password_authentication', authtoken_provided, max_age))
        row = itgs.read_cursor.fetchone()
        if row is None:
            failure_type = 403
        else:
            (authtoken_id, user_id) = row

            revoke_key = f'auth_token_revoked-{authtoken_id}'
            if itgs.cache.get(revoke_key) is not None:
                failure_type = 403
                authtoken_id = None
                user_id = None

    if failure_type is not None:
        if not ratelimit_helper.check_ratelimit(itgs, None, [],
                                                check_request_cost):
            return Response(status_code=429, headers=headers)
        return Response(status_code=failure_type, headers=headers)

    view_permission = None
    action_permission = None
    if req_user_id == user_id:
        view_permission = VIEW_SELF_DEMOGRAPHICS_PERMISSION
        action_permission = action_self_permission
    else:
        view_permission = VIEW_OTHERS_DEMOGRAPHICS_PERMISSION
        action_permission = action_other_permission

    check_permissions = []
    if view_permission is not None:
        check_permissions.append(view_permission)

    if action_permission is not None:
        check_permissions.append(action_permission)

    for perm in additional_permissions_for_result:
        check_permissions.append(perm)

    for perm in ratelimit_helper.RATELIMIT_PERMISSIONS:
        check_permissions.add(perm)

    authtoken_perms = Table('authtoken_permissions')
    permissions = Table('permissions')
    itgs.read_cursor.execute(
        Query.from_(authtoken_perms).select(
            permissions.name).join(permissions).on(
                permissions.id == authtoken_perms.permission_id).where(
                    authtoken_perms.authtoken_id == Parameter('%s')).where(
                        permissions.name.isin([
                            Parameter('%s') for _ in check_permissions
                        ])).get_sql(), [authtoken_id, *check_permissions])

    permissions = []
    row = itgs.read_cursor.fetchone()
    while row is not None:
        permissions.append(row[0])
        row = itgs.read_cursor.fetchone()

    if not ratelimit_helper.check_ratelimit(itgs, user_id, permissions,
                                            check_request_cost):
        return Response(status_code=429, headers=headers)

    if view_permission is not None and view_permission not in permissions:
        return Response(status_code=404, headers=headers)

    if req_user_id is not None:
        demos = Table('user_demographics')
        itgs.read_cursor.execute(
            Query.from_(demos).select(1).where(
                demos.user_id == Parameter('%s')).where(
                    demos.deleted == Parameter('%s')).limit(1).get_sql(),
            (req_user_id, True))
        if itgs.read_cursor.fetchone() is not None:
            return Response(status_code=451, headers=headers)

    if action_permission is not None and action_permission not in permissions:
        return Response(status_code=403, headers=headers)

    # Enrich logs
    users = Table('users')
    itgs.read_cursor.execute(
        Query.from_(users).select(
            users.username).where(users.id == Parameter('%s')).get_sql(),
        (user_id, ))
    username = itgs.read_cursor.fetchone()[0]

    if req_user_id is not None:
        itgs.read_cursor.execute(
            Query.from_(users).select(
                users.username).where(users.id == Parameter('%s')).get_sql(),
            (req_user_id, ))
        row = itgs.read_cursor.fetchone()
        if row is not None:
            req_username = row[0]
        else:
            req_username = f'<UNKNOWN:id={req_user_id}>'
    else:
        req_username = None

    itgs.logger.print(
        Level.DEBUG if user_id == req_user_id else Level.WARN,
        ('/u/{} is exercising their ability to view user demographics '
         'information {}. (view_permission = {}, action_permission = {})'),
        username, 'via a general lookup' if req_username is None else
        f'on /u/{req_username}', view_permission, action_permission)

    if req_user_id == user_id:
        pm_key = f'demographics_helper/pms/self/{user_id}'
        if itgs.cache.get(pm_key) is None:
            itgs.cache.set(pm_key, b'1', expire=86400)
            url_root = os.environ['ROOT_DOMAIN']
            send_queue = os.environ['AMQP_REDDIT_PROXY_QUEUE']
            itgs.channel.basic_publish(
                exchange='',
                routing_key=send_queue,
                body=json.dumps({
                    'type':
                    'compose',
                    'response_queue':
                    os.environ['AMQP_RESPONSE_QUEUE'],
                    'uuid':
                    secrets.token_urlsafe(47),
                    'version_utc_seconds':
                    float(os.environ['APP_VERSION_NUMBER']),
                    'sent_at':
                    time.time(),
                    'args': {
                        'recipient':
                        username,
                        'subject':
                        'RedditLoans: Demographic Information Viewed',
                        'body':
                        ('You just exercised your ability to view or edit your own '
                         'demographic information (email, first name, last name, street '
                         f'address, city, state, and zip) on {url_root} - if this was '
                         f'not you, immediately visit {url_root} and reset your password.'
                         )
                    }
                }).encode('utf-8'))
    elif req_user_id is not None:
        pm_key = f'demographics_helper/pms/other/{user_id}'
        if itgs.cache.get(pm_key) is None:
            itgs.cache.set(pm_key, b'1', expire=3600)
            url_root = os.environ['ROOT_DOMAIN']
            send_queue = os.environ['AMQP_REDDIT_PROXY_QUEUE']
            itgs.channel.basic_publish(
                exchange='',
                routing_key=send_queue,
                body=json.dumps({
                    'type':
                    'compose',
                    'response_queue':
                    os.environ['AMQP_RESPONSE_QUEUE'],
                    'uuid':
                    secrets.token_urlsafe(47),
                    'version_utc_seconds':
                    float(os.environ['APP_VERSION_NUMBER']),
                    'sent_at':
                    time.time(),
                    'args': {
                        'recipient':
                        username,
                        'subject':
                        'RedditLoans: Demographic Information Viewed',
                        'body':
                        ('You just exercised your ability to view or edit the '
                         'demographic information (email, first name, last name, street '
                         f'address, city, state, and zip) of /u/{req_username} on {url_root} '
                         f'- if this was not you then reset your password on {url_root}, '
                         'contact /u/Tjstretchalot, reset your password on reddit and '
                         f'strip all permissions from your own account on {url_root}.\n\n'
                         f'## Respond to modmail: "Demographic Info Viewed: /u/{req_username}".'
                         )
                    }
                }).encode('utf-8'))
            itgs.channel.basic_publish(
                exchange='',
                routing_key=send_queue,
                body=json.dumps({
                    'type':
                    'compose',
                    'response_queue':
                    os.environ['AMQP_RESPONSE_QUEUE'],
                    'uuid':
                    secrets.token_urlsafe(47),
                    'version_utc_seconds':
                    float(os.environ['APP_VERSION_NUMBER']),
                    'sent_at':
                    time.time(),
                    'args': {
                        'recipient':
                        '/r/borrow',
                        'subject':
                        f'Demographic Info Viewed: /u/{req_username}',
                        'body':
                        (f'/u/{username} exercised his ability to view demographic information '
                         f'on /u/{req_username}. This pm is sent at most once per hour.\n\n '
                         f'/u/{username} should respond here very soon confirming this was him, '
                         'otherwise efforts should be made to contact him. If he cannot be '
                         f'contacted, strip his access on {url_root} by going to Account -> '
                         f'Administrate, type in {username}, fill in a reason, and click '
                         '"Revoke All Permissions". If there are other options under the '
                         '"Authentication Method ID" dropdown, for each one select it and press '
                         '"Revoke All Permissions".\n\n'
                         'Then contact /u/Tjstretchalot if he has not already responded to this '
                         'thread.')
                    }
                }).encode('utf-8'))
            itgs.channel.basic_publish(
                exchange='',
                routing_key=send_queue,
                body=json.dumps({
                    'type':
                    'compose',
                    'response_queue':
                    os.environ['AMQP_RESPONSE_QUEUE'],
                    'uuid':
                    secrets.token_urlsafe(47),
                    'version_utc_seconds':
                    float(os.environ['APP_VERSION_NUMBER']),
                    'sent_at':
                    time.time(),
                    'args': {
                        'recipient':
                        'Tjstretchalot',
                        'subject':
                        'RedditLoans: Demographic Information Viewed',
                        'body':
                        f'Check modmail! Mod user: /u/{username} viewed /u/{req_username}'
                    }
                }).encode('utf-8'))

    return (user_id, permissions)
def show(req_user_id: int, captcha: str, authorization=Header(None)):
    """View the given users demographic information. This endpoint cannot be
    used on non-official frontends as it's guarded by a captcha.
    """
    if authorization is None:
        return Response(status_code=401)

    attempt_request_cost = 5
    success_request_cost = 95
    with LazyItgs(no_read_only=True) as itgs:
        auth = demographics_helper.get_failure_response_or_user_id_and_perms_for_authorization(
            itgs, authorization, attempt_request_cost, req_user_id, None, None,
            [])

        if isinstance(auth, Response):
            return auth

        (user_id, perms) = auth

        headers = {'x-request-cost': str(attempt_request_cost)}
        if not security.verify_captcha(itgs, captcha):
            return Response(status_code=403, headers=headers)

        headers['x-request-cost'] = str(attempt_request_cost +
                                        success_request_cost)
        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                success_request_cost):
            return Response(status_code=429, headers=headers)

        demos = Table('user_demographics')
        itgs.read_cursor.execute(
            Query.from_(demos).select(
                demos.email, demos.name, demos.street_address, demos.city,
                demos.state, demos.zip, demos.country, demos.deleted).where(
                    demos.user_id == Parameter('%s')).get_sql(),
            (req_user_id, ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            (email, name, street_address, city, state, zip_, country,
             deleted) = (None, None, None, None, None, None, None, False)
        else:
            (email, name, street_address, city, state, zip_, country,
             deleted) = row

        if deleted:
            return Response(status_code=451, headers=headers)

        demo_views = Table('user_demographic_views')
        itgs.write_cursor.execute(
            Query.into(demo_views).columns(
                demo_views.user_id, demo_views.admin_user_id,
                demo_views.lookup_id).insert(
                    *[Parameter('%s') for _ in range(3)]).get_sql(),
            (req_user_id, user_id, None))
        itgs.write_conn.commit()

        headers['Cache-Control'] = 'no-store'
        headers['Pragma'] = 'no-cache'
        return JSONResponse(status_code=200,
                            content=demographics_models.UserDemographics(
                                user_id=req_user_id,
                                email=email,
                                name=name,
                                street_address=street_address,
                                city=city,
                                state=state,
                                zip=zip_,
                                country=country).dict(),
                            headers=headers)
Exemplo n.º 19
0
def get_loans_by_thread(thread: str, request: Request):
    """Fetch the loans created from the given comment permalink.

    Result:

    ```json
    {
      "result_type": "LOANS_ULTRACOMPACT"
      "success": true,
      "loans": [loan_id, loan_id, ....]
    }
    ```
    """
    request_cost = 5
    if ratelimit_helper.is_cache_bust(request, params=('thread', )):
        request_cost = 15

    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        auth = find_bearer_token(request)
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, auth, [])
        resp = try_handle_deprecated_call(itgs, request, SLUG, user_id=user_id)

        if resp is not None:
            return resp

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return JSONResponse(content=RATELIMIT_RESPONSE.dict(),
                                status_code=429,
                                headers=headers)

        match = URL_REGEX.match(thread)
        if match is None:
            headers['Cache-Control'] = 'public, max-age=604800'
            return JSONResponse(content=ResponseFormat(loans=[]).dict(),
                                status_code=200,
                                headers=headers)

        matchdict = match.groupdict()
        if 'comment_fullname' not in matchdict:
            headers['Cache-Control'] = 'public, max-age=604800'
            return JSONResponse(content=ResponseFormat(loans=[]).dict(),
                                status_code=200,
                                headers=headers)

        comment_fullname = 't1_' + matchdict['comment_fullname']
        loans = Table('loans')
        creation_infos = Table('loan_creation_infos')
        itgs.read_cursor.execute(
            Query.from_(loans).join(creation_infos).on(
                creation_infos.loan_id == loans.id).select(loans.id).where(
                    creation_infos.comment_fullname == Parameter('%s')).where(
                        loans.deleted_at.isnull()).get_sql(),
            (comment_fullname, ))
        result_loans = [r[0] for r in itgs.read_cursor.fetchall()]

        if result_loans:
            headers['Cache-Control'] = 'public, max-age=604800'
        else:
            headers[
                'Cache-Control'] = 'public, max-age=600, stale-while-revalidate=1200'

        return JSONResponse(content=ResponseFormat(loans=result_loans).dict(),
                            status_code=200,
                            headers=headers)
Exemplo n.º 20
0
def index_user_history(req_user_id: int,
                       limit: int = 25,
                       before_id: int = None,
                       authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    if limit <= 0:
        return JSONResponse(status_code=422,
                            content={
                                'detail': {
                                    'loc': ['limit'],
                                    'msg': 'Must be positive',
                                    'type': 'range_error'
                                }
                            })

    request_cost = max(1, math.log(limit))
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization,
            (settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=404, headers=headers)

        can_see_others_settings = settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION in perms

        users = Table('users')
        if req_user_id != user_id:
            if not can_see_others_settings:
                return Response(status_code=404, headers=headers)

            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).get_sql(), (req_user_id, ))
            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

        events = Table('user_settings_events')
        query = (Query.from_(events).select(
            events.id).where(events.user_id == Parameter('$1')).limit(
                Parameter('$2')).orderby(events.id, order=Order.desc))
        args = [req_user_id, limit + 1]

        if before_id is not None:
            query = query.where(events.id < Parameter('$3'))
            args.append(before_id)

        itgs.read_cursor.execute(*convert_numbered_args(query.get_sql(), args))
        result = []
        have_more = False
        row = itgs.read_cursor.fetchone()
        while row is not None:
            if len(result) < limit:
                result.append(row[0])
            else:
                have_more = True
            row = itgs.read_cursor.fetchone()

        # Not cacheable; inserting an item at the front breaks all the pages
        headers['Cache-Control'] = 'no-store'

        return JSONResponse(status_code=200,
                            content=settings_models.UserSettingsHistory(
                                before_id=(min(result) if result else None)
                                if have_more else None,
                                history=result).dict(),
                            headers=headers)
Exemplo n.º 21
0
def grant_permission(id: int, perm: str, authorization=Header(None)):
    """This does not apply to existing auth tokens."""
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM, perm,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if (user_id is None) or (perm not in perms):
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_modify_others_auth_methods = (
            helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM in perms)
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')

        if (not can_modify_others_auth_methods) or (
                not can_view_others_auth_methods):
            itgs.read_cursor.execute(
                Query.from_(auth_methods).select(
                    auth_methods.user_id, auth_methods.deleted).where(
                        auth_methods.id == Parameter('%s')).get_sql(), (id, ))
            row = itgs.read_cursor.fetchone()

            if row is None:
                return Response(status_code=404,
                                headers={'x-request-cost': str(request_cost)})

            (auth_method_user_id, deleted) = row
            if auth_method_user_id != user_id:
                if not can_view_others_auth_methods:
                    return Response(
                        status_code=404,
                        headers={'x-request-cost': str(request_cost)})

                return Response(status_code=403,
                                headers={'x-request-cost': str(request_cost)})

            if deleted:
                if not can_view_deleted_auth_methods:
                    return Response(
                        status_code=404,
                        headers={'x-request-cost': str(request_cost)})

                return Response(status_code=403,
                                headers={'x-request-cost': str(request_cost)})

        itgs.write_cursor.execute(
            '''
            INSERT INTO password_auth_permissions (password_authentication_id, permission_id)
            SELECT %s, permissions.id
            FROM permissions
            WHERE
                permissions.name = %s AND
                NOT EXISTS (
                    SELECT FROM password_auth_permissions
                    JOIN permissions ON permissions.id = password_auth_permissions.permission_id
                    WHERE
                        password_authentication_id = %s AND
                        permissions.name = %s
                )
            LIMIT 1
            RETURNING 1 AS one
            ''', (id, perm.lower(), id, perm.lower()))
        inserted_any = itgs.read_cursor.fetchone() is not None
        if inserted_any:
            events = Table('password_authentication_events')
            permissions = Table('permissions')
            itgs.write_cursor.execute(
                Query.into(events).columns(
                    events.password_authentication_id, events.type,
                    events.reason, events.user_id,
                    events.permission_id).from_(permissions).select(
                        Parameter('%s'), Parameter('%s'), Parameter('%s'),
                        Parameter('%s'),
                        permissions.id).where(permissions.name == Parameter(
                            '%s')).limit(1).get_sql(),
                (id, 'permission-granted',
                 'No reason provided; performed manually', user_id,
                 perm.lower()))

        itgs.write_conn.commit()

        if not inserted_any:
            return Response(status_code=409,
                            headers={'x-request-cost': str(request_cost)})

        return Response(status_code=200,
                        headers={'x-request-cost': str(request_cost)})
Exemplo n.º 22
0
def change_password(id: int,
                    args: models.ChangePasswordParams,
                    authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    # In reality this is probably closer to ~10000, but we want to make sure
    # everyone could actually save enough tokens to do this. To avoid user error,
    # we will split the ratelimit cost into a pre- and post- part
    check_request_cost = 5
    perform_request_cost = 295
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                check_request_cost):
            return Response(
                status_code=429,
                headers={'x-request-cost': str(check_request_cost)})

        if user_id is None:
            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.user_id, auth_methods.deleted).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))
        row = itgs.read_cursor.fetchone()

        if row is None:
            return Response(
                status_code=404,
                headers={'x-request-cost': str(check_request_cost)})

        (auth_method_user_id, deleted) = row
        if auth_method_user_id != user_id:
            if not can_view_others_auth_methods:
                return Response(
                    status_code=404,
                    headers={'x-request-cost': str(check_request_cost)})

            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        if deleted:
            if not can_view_deleted_auth_methods:
                return Response(
                    status_code=404,
                    headers={'x-request-cost': str(check_request_cost)})

            return Response(
                status_code=403,
                headers={'x-request-cost': str(check_request_cost)})

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                perform_request_cost):
            return Response(status_code=429,
                            headers={
                                'x-request-cost':
                                str(check_request_cost + perform_request_cost)
                            })

        salt = secrets.token_urlsafe(23)  # 31 chars
        block_size = int(os.environ.get('NONHUMAN_PASSWORD_BLOCK_SIZE', '8'))
        dklen = int(os.environ.get('NONHUMAN_PASSWORD_DKLEN', '64'))

        # final number is MiB of RAM for the default
        iterations = int(
            os.environ.get('NONHUMAN_PASSWORD_ITERATIONS', str(
                (1024 * 8) * 64)))
        hash_name = f'scrypt-{block_size}-{dklen}'

        password_digest = b64encode(
            scrypt(
                args.password.encode('utf-8'),
                salt=salt.encode('utf-8'),
                n=iterations,
                r=block_size,
                p=1,
                maxmem=128 * iterations * block_size +
                1024 * 64,  # padding not necessary?
                dklen=dklen)).decode('ascii')

        itgs.write_cursor.execute(
            Query.update(auth_methods).set(
                auth_methods.hash_name,
                Parameter('%s')).set(auth_methods.hash, Parameter('%s')).set(
                    auth_methods.salt, Parameter('%s')).set(
                        auth_methods.iterations, Parameter('%s')).where(
                            auth_methods.id == Parameter('%s')).get_sql(),
            (hash_name, password_digest, salt, iterations, id))

        auth_events = Table('password_authentication_events')
        itgs.write_cursor.execute(
            Query.into(auth_events).columns(
                auth_events.password_authentication_id, auth_events.type,
                auth_events.reason, auth_events.permission_id,
                auth_events.user_id).insert(
                    *[Parameter('%s') for _ in range(5)]).get_sql(),
            (id, 'password-changed', args.reason, None, user_id))
        itgs.write_conn.commit()
        return Response(status_code=200,
                        headers={
                            'x-request-cost':
                            str(check_request_cost + perform_request_cost)
                        })
Exemplo n.º 23
0
def delete_all_permissions(id: int,
                           reason_wrapped: models.Reason,
                           authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms
        can_modify_others_auth_methods = (
            helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM in perms)

        auth_methods = Table('password_authentications')
        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.deleted, auth_methods.user_id).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))

        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        (deleted, auth_method_user_id) = row
        if deleted and not can_view_deleted_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_view_others_auth_methods:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id and not can_modify_others_auth_methods:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        if deleted:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        authtoken = users.helper.get_authtoken_from_header(authorization)
        info = users.helper.get_auth_info_from_token_auth(
            itgs, models.TokenAuthentication(token=authtoken))
        auth_id = info[0]

        # You can only delete permissions from someone that you yourself have!
        itgs.write_cursor.execute(
            '''
            DELETE FROM password_auth_permissions AS outer
            WHERE
                password_authentication_id = %s AND
                EXISTS (
                    SELECT FROM password_auth_permissions
                    WHERE
                        password_authentication_id = %s AND
                        permission_id = outer.permission_id
                )
            RETURNING outer.permission_id
            ''', (id, auth_id))

        rows = itgs.write_cursor.fetchall()
        if rows:
            auth_events = Table('password_authentication_events')
            authtokens = Table('authtokens')
            itgs.write_cursor.execute(
                Query.into(auth_events).columns(
                    auth_events.password_authentication_id, auth_events.type,
                    auth_events.reason, auth_events.user_id,
                    auth_events.permission_id).insert(
                        *[[Parameter('%s') for _ in range(5)]
                          for _ in rows]).get_sql(),
                tuple(
                    itertools.chain.from_iterable(
                        (id, 'permission-revoked', reason_wrapped.reason,
                         user_id, row[0]) for row in rows)))
            itgs.write_cursor.execute(
                Query.from_(authtokens).delete().where(
                    authtokens.source_type == Parameter('%s')).where(
                        authtokens.source_id == Parameter('%s')).get_sql(),
                ('password_authentication', id))
        itgs.write_conn.commit()
        return Response(status_code=200)
Exemplo n.º 24
0
def show_setting(req_user_id: int,
                 setting_name: str,
                 authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    if '_' in setting_name:
        setting_name_fixed = setting_name.replace('_', '-')
        return Response(
            status_code=301,
            headers={
                'Location':
                f'/api/users/{req_user_id}/settings/{setting_name_fixed}'
            })

    known_settings = frozenset(
        ('non-req-response-opt-out', 'borrower-req-pm-opt-out', 'ratelimit'))
    if setting_name not in known_settings:
        return Response(status_code=404)

    request_cost = 1
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization,
            (settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION,
             settings_helper.EDIT_OTHERS_STANDARD_SETTINGS_PERMISSION,
             settings_helper.EDIT_RATELIMIT_SETTINGS_PERMISSION,
             settings_helper.EDIT_OTHERS_RATELIMIT_SETTINGS_PERMISSION,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=404, headers=headers)

        can_view_others_settings = settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION in perms
        can_edit_others_standard_settings = (
            settings_helper.EDIT_OTHERS_STANDARD_SETTINGS_PERMISSION in perms)
        can_edit_self_ratelimit_settings = (
            settings_helper.EDIT_RATELIMIT_SETTINGS_PERMISSION in perms)
        can_edit_others_ratelimit_settings = (
            settings_helper.EDIT_OTHERS_RATELIMIT_SETTINGS_PERMISSION in perms)

        users = Table('users')
        if user_id != req_user_id:
            if not can_view_others_settings:
                return Response(status_code=404, headers=headers)

            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).get_sql(), (req_user_id, ))
            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

        settings = user_settings.get_settings(itgs, req_user_id)

        if setting_name == 'non-req-response-opt-out':
            setting = settings_models.UserSetting(
                can_modify=(req_user_id == user_id
                            or can_edit_others_standard_settings),
                value=settings.non_req_response_opt_out)
        elif setting_name == 'borrower-req-pm-opt-out':
            setting = settings_models.UserSetting(
                can_modify=(req_user_id == user_id
                            or can_edit_others_standard_settings),
                value=settings.borrower_req_pm_opt_out)
        elif setting_name == 'ratelimit':
            setting = settings_models.UserSetting(
                can_modify=(can_edit_self_ratelimit_settings
                            if req_user_id == user_id else
                            can_edit_others_ratelimit_settings),
                value={
                    'global_applies':
                    settings.global_ratelimit_applies,
                    'user_specific':
                    settings.user_specific_ratelimit,
                    'max_tokens':
                    (settings.ratelimit_max_tokens
                     or ratelimit_helper.USER_RATELIMITS.max_tokens),
                    'refill_amount':
                    (settings.ratelimit_refill_amount
                     or ratelimit_helper.USER_RATELIMITS.refill_amount),
                    'refill_time_ms':
                    (settings.ratelimit_refill_time_ms
                     or ratelimit_helper.USER_RATELIMITS.refill_time_ms),
                    'strict':
                    (settings.ratelimit_strict if settings.ratelimit_strict
                     is not None else ratelimit_helper.USER_RATELIMITS.strict)
                })

        headers['Cache-Control'] = 'no-store'
        return JSONResponse(status_code=200,
                            content=setting.dict(),
                            headers=headers)
Exemplo n.º 25
0
def index_history(id: int,
                  after_id: int = None,
                  limit: int = None,
                  authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    if limit is None:
        limit = 25

    if limit <= 0:
        return JSONResponse(status_code=422,
                            content={
                                'detail': [{
                                    'loc': ['query', 'limit'],
                                    'msg': 'Must be non-negative',
                                    'type': 'value_error'
                                }]
                            })

    request_cost = max(5 * math.ceil(math.log(limit)), 5)

    request_cost = 5
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_OTHERS_AUTH_EDIT_NOTES_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_view_others_edit_notes = helper.CAN_VIEW_OTHERS_AUTH_EDIT_NOTES_PERM in perms
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        query = (Query.from_(auth_methods).select(1).where(
            auth_methods.id == Parameter('%s')))
        args = [id]
        if not can_view_others_auth_methods:
            query = query.where(auth_methods.user_id == Parameter('%s'))
            args.append(user_id)

        if not can_view_deleted_auth_methods:
            query = query.where(auth_methods.deleted.eq(False))

        itgs.read_cursor.execute(query.get_sql(), args)
        if itgs.read_cursor.fetchone() is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        events = Table('password_authentication_events')
        permissions = Table('permissions')
        usrs = Table('users')
        query = (
            Query.from_(events).join(usrs).on(
                usrs.id == events.user_id).left_join(permissions).
            on(permissions.id == events.permission_id).where(
                events.password_authentication_id == Parameter('%s')).select(
                    events.id, events.type, permissions.name, events.reason,
                    events.user_id, usrs.username, events.reason,
                    events.created_at).orderby(
                        events.id,
                        order=Order.asc)  # Need ascending order for caching
        )
        args = [id]

        if after_id is not None:
            query = query.where(events.id > Parameter('%s'))
            args.append(after_id)

        query = query.limit(Parameter('%s'))
        args.append(limit +
                    1)  # add 1 to check if theres more, slightly better UX

        itgs.read_cursor.execute(query.get_sql(), args)

        result = []
        next_id = None
        have_more = False
        row = itgs.read_cursor.fetchone()
        while row is not None:
            (this_id, event_type, permission_name, event_reason, event_user_id,
             event_username, event_reason, event_created_at) = row

            if len(result) >= limit:
                have_more = True
                itgs.read_cursor.fetchall()
                break

            next_id = this_id

            if (not can_view_others_edit_notes and
                    event_user_id is not None  # system events, deleted users
                    and event_user_id != user_id):
                event_user_id = None
                event_username = None
                event_reason = None

            result.append(
                models.AuthMethodHistoryItem(
                    event_type=event_type,
                    reason=event_reason,
                    username=event_username,
                    permission=permission_name,
                    occurred_at=event_created_at.timestamp()))
            row = itgs.read_cursor.fetchone()

        if not result:
            return Response(status_code=204,
                            headers={'x-request-cost': str(request_cost)})

        if have_more:
            cache_control = 'private, max-age=604800'
        else:
            cache_control = 'no-store'
            next_id = None

        return JSONResponse(status_code=200,
                            content=models.AuthMethodHistory(
                                next_id=next_id, history=result).dict(),
                            headers={
                                'cache-control': cache_control,
                                'x-request-cost': str(request_cost)
                            })
Exemplo n.º 26
0
def update_ratelimit(
    req_user_id: int,
    new_value: settings_models.UserSettingRatelimitChangeRequest,
    authorization=Header(None)):
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization,
            (settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION,
             settings_helper.EDIT_RATELIMIT_SETTINGS_PERMISSION,
             settings_helper.EDIT_OTHERS_RATELIMIT_SETTINGS_PERMISSION,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=404, headers=headers)

        can_view_others_settings = settings_helper.VIEW_OTHERS_SETTINGS_PERMISSION in perms
        can_edit_self_ratelimit_settings = (
            settings_helper.EDIT_RATELIMIT_SETTINGS_PERMISSION in perms)
        can_edit_others_ratelimit_settings = (
            settings_helper.EDIT_OTHERS_RATELIMIT_SETTINGS_PERMISSION in perms)

        if user_id != req_user_id:
            if not can_view_others_settings:
                return Response(status_code=404, headers=headers)

            users = Table('users')
            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).get_sql(), (req_user_id, ))

            if itgs.read_cursor.fetchone() is None:
                return Response(status_code=404, headers=headers)

            if not can_edit_others_ratelimit_settings:
                return Response(status_code=403, headers=headers)
        elif not can_edit_self_ratelimit_settings:
            return Response(status_code=403, headers=headers)

        changes = user_settings.set_settings(
            itgs,
            req_user_id,
            global_ratelimit_applies=new_value.new_value.global_applies,
            user_specific_ratelimit=new_value.new_value.user_specific,
            ratelimit_max_tokens=new_value.new_value.max_tokens,
            ratelimit_refill_amount=new_value.new_value.refill_amount,
            ratelimit_refill_time_ms=new_value.new_value.refill_time_ms,
            ratelimit_strict=new_value.new_value.strict)
        user_settings.create_settings_events(itgs,
                                             req_user_id,
                                             user_id,
                                             changes,
                                             commit=True)
        return Response(status_code=200, headers=headers)
def get_promotion_blacklist(request: Request,
                            username: str = None,
                            min_id: int = None,
                            max_id: int = None,
                            limit: int = 10):
    """Get up to the given limit number of users which match the given criteria
    and are barred from being promoted.

    This requires the `view-others-trust` permission to include results that
    aren't the authenticated user and the `view-self-trust` reason to include
    the authenticated user in the response. The mod username will always be
    replaced with `LoansBot`. Permissions are not consistently checked and this
    information is not considered secure, however these permissions are used to
    avoid this endpoint being used in browser extensions as it's not intended for
    that purpose.

    The reason is replaced with users trust status, so for example "unknown" or
    "bad". Users with the trust status "good" are not returned in this endpoint.
    This attempts to emulate the behavior of the promotion blacklist within the
    new improved trust system.

    Arguments:
    - `username (str, None)`: If specified only users which match the given username
      with an ILIKE query will be returned.
    - `min_id (int, None)`: If specified only TRUSTS which have an id of the given value
      or higher will be returned. This can be used to walk trusts.
    - `max_id (int, None)`: If specified only TRUSTS which have an id of the given value
      or lower will be returned. This can be used to walk trusts.
    - `limit (int)`: The maximum number of results to return. For users for which the
      global ratelimit is applied this is restricted to 3 or fewer. For other users this
      has no explicit limit but does linearly increase the request cost.

    Result:
    ```json
    {
      "result_type": "PROMOTION_BLACKLIST"
      "success": true,
      "list": [
          {
              id: 1,
              username: "******",
              mod_username: "******",
              reason: "some text here",
              added_at: <utc milliseconds>
          },
          ...
      ]
    }
    ```
    """
    with LazyItgs() as itgs:
        auth = find_bearer_token(request)
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, auth, (trusts.helper.VIEW_OTHERS_TRUST_PERMISSION,
                         trusts.helper.VIEW_SELF_TRUST_PERMISSION,
                         *ratelimit_helper.RATELIMIT_PERMISSIONS))
        resp = try_handle_deprecated_call(itgs, request, SLUG, user_id=user_id)

        if resp is not None:
            return resp

        if limit <= 0:
            limit = 10

        settings = get_settings(itgs, user_id) if user_id is not None else None
        if limit > 3 and (settings is None
                          or settings.global_ratelimit_applies):
            # Avoid accidentally blowing through the global ratelimit
            limit = 3

        request_cost = 7 * limit
        headers = {'x-request-cost': str(request_cost)}
        if not ratelimit_helper.check_ratelimit(
                itgs, user_id, perms, request_cost, settings=settings):
            return JSONResponse(content=RATELIMIT_RESPONSE.dict(),
                                status_code=429,
                                headers=headers)

        can_view_others_trust = trusts.helper.VIEW_OTHERS_TRUST_PERMISSION in perms
        can_view_self_trust = trusts.helper.VIEW_SELF_TRUST_PERMISSION in perms

        if not can_view_others_trust and not can_view_self_trust:
            headers['Cache-Control'] = 'no-store'
            headers['Pragma'] = 'no-cache'
            return JSONResponse(content=ResponseFormat(list=[]).dict(),
                                status_code=200,
                                headers=headers)

        if can_view_others_trust and can_view_self_trust:
            headers['Cache-Control'] = 'public, max-age=600'
        else:
            headers['Cache-Control'] = 'no-store'
            headers['Pragma'] = 'no-cache'

        headers['x-can-view-others-trust'] = str(can_view_others_trust)
        headers['x-can-view-self-trust'] = str(can_view_self_trust)

        usrs = Table('users')
        trsts = Table('trusts')
        query = (Query.from_(trsts).select(
            trsts.id, usrs.username, trsts.status,
            trsts.created_at).join(usrs).on(usrs.id == trsts.user_id).where(
                trsts.status != Parameter('$1')).limit('$2'))
        args = ['good', limit]

        if username is not None:
            query = query.where(
                usrs.username.ilike(Parameter(f'${len(args) + 1}')))
            args.append(username)

        if min_id is not None:
            query = query.where(trsts.id >= Parameter(f'${len(args) + 1}'))
            args.append(min_id)

        if max_id is not None:
            query = query.where(trsts.id <= Parameter(f'${len(args) + 1}'))
            args.append(max_id)

        if not can_view_self_trust:
            query = query.where(usrs.id != Parameter(f'${len(args) + 1}'))
            args.append(user_id)

        if not can_view_others_trust:
            query = query.where(usrs.id == Parameter(f'${len(args) + 1}'))
            args.append(user_id)

        itgs.read_cursor.execute(*convert_numbered_args(query.get_sql(), args))

        denylist = []
        row = itgs.read_cursor.fetchone()
        while row is not None:
            (trust_id, username, status, trust_created_at) = row
            denylist.append(
                ResponseEntry(id=trust_id,
                              username=username,
                              reason=status,
                              added_at=(trust_created_at.timestamp() * 1000)))
            row = itgs.read_cursor.fetchone()

        return JSONResponse(content=ResponseFormat(list=denylist).dict(),
                            status_code=200,
                            headers=headers)
Exemplo n.º 28
0
def create_authentication_method(req_user_id: int, authorization=Header(None)):
    """Create an authentication method with a randomly assigned password and
    no permissions. The password should be changed before adding permissions."""
    if authorization is None:
        return Response(status_code=401)

    request_cost = 50
    headers = {'x-request-cost': str(request_cost)}
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = helper.get_permissions_from_header(
            itgs, authorization, (VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
                                  ADD_SELF_AUTHENTICATION_METHODS_PERM,
                                  ADD_OTHERS_AUTHENTICATION_METHODS_PERM,
                                  *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=403, headers=headers)

        can_view_others_auth_methods = VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_add_self_auth_methods = ADD_SELF_AUTHENTICATION_METHODS_PERM in perms
        can_add_others_auth_methods = ADD_OTHERS_AUTHENTICATION_METHODS_PERM in perms

        if req_user_id != user_id:
            if not can_view_others_auth_methods:
                return Response(status_code=404, headers=headers)

            users = Table('users')
            itgs.read_cursor.execute(
                Query.from_(users).select(1).where(
                    users.id == Parameter('%s')).get_sql(), (req_user_id, ))
            if itgs.read_cursor.fetchone() is not None:
                return Response(status_code=404, headers=headers)

        can_add = ((user_id == req_user_id and can_add_self_auth_methods)
                   or can_add_others_auth_methods)
        if not can_add:
            return Response(status_code=403, headers=headers)

        hash_name = 'sha512'
        passwd = secrets.token_urlsafe(23)
        salt = secrets.token_urlsafe(23)  # 31 chars
        iterations = int(
            os.environ.get('INITIAL_NONHUMAN_PASSWORD_ITERS', '10000'))

        passwd_digest = b64encode(
            pbkdf2_hmac(hash_name, passwd.encode('utf-8'),
                        salt.encode('utf-8'), iterations)).decode('ascii')

        auth_methods = Table('password_authentications')
        itgs.write_cursor.execute(
            Query.into(auth_methods).columns(
                auth_methods.user_id, auth_methods.human,
                auth_methods.hash_name, auth_methods.hash, auth_methods.salt,
                auth_methods.iterations).insert([
                    Parameter('%s') for _ in range(6)
                ]).returning(auth_methods.id).get_sql(),
            (req_user_id, False, hash_name, passwd_digest, salt, iterations))
        (row_id, ) = itgs.write_cursor.fetchone()
        itgs.write_conn.commit()

        return JSONResponse(
            status_code=201,
            headers=headers,
            content=settings_models.AuthMethodCreateResponse(id=row_id).dict())
Exemplo n.º 29
0
def get_csv_dump(alt_authorization: str = None, authorization=Header(None)):
    """Get a csv of all loans where the columns are

    id, lender_id, borrower_id, currency, principal_minor, principal_cents,
    principal_repayment_minor, principal_repayment_cents, created_at,
    last_repayment_at, repaid_at, unpaid_at

    This endpoint is _very_ expensive for us. Without a users ratelimit being
    increased they will almost certainly not even be able to use this endpoint
    once. We charge 5 * rows * log(rows) toward the quota and do not allow users
    which contribute to the global ratelimit. It is NOT cheaper to use this
    endpoint compared to just walking the index endpoint.

    This mainly exists for users which are willing to pay for a csv dump. You
    may use a query parameter for authorization instead of a header.
    """
    if authorization is None and alt_authorization is None:
        return Response(status_code=401)

    if authorization is None:
        authorization = f'bearer {alt_authorization}'

    attempt_request_cost = 1
    check_request_cost_cost = 10
    headers = {'x-request-cost': str(attempt_request_cost)}
    with LazyItgs() as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization, ratelimit_helper.RATELIMIT_PERMISSIONS
        )

        settings = (
            ratelimit_helper.USER_RATELIMITS
            if user_id is None
            else get_settings(itgs, user_id)
        )
        if not ratelimit_helper.check_ratelimit(
                itgs, user_id, perms, attempt_request_cost,
                settings=settings):
            return Response(status_code=429, headers=headers)

        if user_id is None:
            return Response(status_code=403, headers=headers)

        if settings.global_ratelimit_applies:
            return Response(status_code=403, headers=headers)

        headers['x-request-cost'] = (
            str(attempt_request_cost + check_request_cost_cost)
        )
        if not ratelimit_helper.check_ratelimit(
                itgs, user_id, perms, check_request_cost_cost,
                settings=settings):
            return Response(status_code=429, headers=headers)

        loans = Table('loans')
        itgs.read_cursor.execute(
            Query.from_(loans).select(Count(Star())).get_sql()
        )
        (cnt_loans,) = itgs.read_cursor.fetchone()

        real_request_cost = (
            5 * cnt_loans * max(1, math.ceil(math.log(cnt_loans)))
        )

        headers['x-request-cost'] = (
            str(
                attempt_request_cost
                + check_request_cost_cost
                + real_request_cost
            )
        )
        if not ratelimit_helper.check_ratelimit(
                itgs, user_id, perms, real_request_cost,
                settings=settings):
            return Response(status_code=429, headers=headers)

        moneys = Table('moneys')
        currencies = Table('currencies')
        principals = moneys.as_('principals')
        principal_repayments = moneys.as_('principal_repayments')
        repayment_events = Table('loan_repayment_events')
        query = (
            Query.from_(loans)
            .select(
                loans.id,
                loans.lender_id,
                loans.borrower_id,
                currencies.code,
                principals.amount,
                principals.amount_usd_cents,
                principal_repayments.amount,
                principal_repayments.amount_usd_cents,
                loans.created_at,
                Max(repayment_events.created_at),
                loans.repaid_at,
                loans.unpaid_at
            )
            .join(principals)
            .on(principals.id == loans.principal_id)
            .join(currencies)
            .on(currencies.id == principals.currency_id)
            .join(principal_repayments)
            .on(principal_repayments.id == loans.principal_repayment_id)
            .left_join(repayment_events)
            .on(repayment_events.loan_id == loans.id)
            .groupby(
                loans.id,
                currencies.id,
                principals.id,
                principal_repayments.id
            )
        )

        headers['Content-Type'] = 'text/csv'
        headers['Content-Disposition'] = 'attachment; filename="loans.csv"'
        headers['Cache-Control'] = 'public, max-age=86400'
        return StreamingResponse(
            query_generator(
                query.get_sql(),
                ','.join((
                    'id', 'lender_id', 'borrower_id', 'currency', 'principal_minor',
                    'principal_cents', 'principal_repayment_minor', 'principal_repayment_cents',
                    'created_at', 'last_repayment_at', 'repaid_at', 'unpaid_at'
                ))
            ),
            status_code=200,
            headers=headers
        )
Exemplo n.º 30
0
def revoke_permission(id: int, perm: str, authorization=Header(None)):
    """This immediately takes effect on all corresponding auth tokens."""
    if authorization is None:
        return Response(status_code=401)

    request_cost = 5
    with LazyItgs(no_read_only=True) as itgs:
        user_id, _, perms = users.helper.get_permissions_from_header(
            itgs, authorization,
            (helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM,
             helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM, perm,
             *ratelimit_helper.RATELIMIT_PERMISSIONS))

        if not ratelimit_helper.check_ratelimit(itgs, user_id, perms,
                                                request_cost):
            return Response(status_code=429,
                            headers={'x-request-cost': str(request_cost)})

        if user_id is None or perm not in perms:
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        can_view_others_auth_methods = helper.VIEW_OTHERS_AUTHENTICATION_METHODS_PERM in perms
        can_modify_others_auth_methods = (
            helper.CAN_MODIFY_OTHERS_AUTHENTICATION_METHODS_PERM in perms)
        can_view_deleted_auth_methods = helper.CAN_VIEW_DELETED_AUTHENTICATION_METHODS_PERM in perms

        auth_methods = Table('password_authentications')
        authtokens = Table('authtokens')
        authtoken_perms = Table('authtoken_permissions')
        permissions = Table('permissions')

        itgs.read_cursor.execute(
            Query.from_(auth_methods).select(
                auth_methods.user_id, auth_methods.deleted).where(
                    auth_methods.id == Parameter('%s')).get_sql(), (id, ))
        row = itgs.read_cursor.fetchone()
        if row is None:
            return Response(status_code=404,
                            headers={'x-request-cost': str(request_cost)})

        (auth_method_user_id, deleted) = row

        if deleted:
            if not can_view_deleted_auth_methods:
                return Response(status_code=404,
                                headers={'x-request-cost': str(request_cost)})
            return Response(status_code=403,
                            headers={'x-request-cost': str(request_cost)})

        if auth_method_user_id != user_id:
            if not can_view_others_auth_methods:
                return Response(status_code=404,
                                headers={'x-request-cost': str(request_cost)})

            if not can_modify_others_auth_methods:
                return Response(status_code=403,
                                headers={'x-request-cost': str(request_cost)})

        auth_perms = Table('password_auth_permissions')
        outer_auth_perms = auth_perms.as_('outer_perms')
        inner_auth_perms = auth_perms.as_('inner_perms')
        itgs.write_cursor.execute(
            Query.from_(outer_auth_perms).delete().where(
                exists(
                    Query.from_(inner_auth_perms).where(
                        inner_auth_perms.id == outer_auth_perms.id).join(
                            auth_methods).on(
                                auth_methods.id == inner_auth_perms.
                                password_authentication_id).join(permissions).
                    on(permissions.id == inner_auth_perms.permission_id).where(
                        auth_methods.id == Parameter('%s')).where(
                            permissions.name == Parameter('%s')))).returning(
                                outer_auth_perms.id).get_sql(),
            (id, perm.lower()))
        found_any = not not itgs.write_cursor.fetchall()

        outer_perms = authtoken_perms.as_('outer_perms')
        inner_perms = authtoken_perms.as_('inner_perms')
        itgs.write_cursor.execute(
            Query.from_(outer_perms).delete().where(
                exists(
                    Query.from_(inner_perms).where(
                        inner_perms.id == outer_perms.id).join(authtokens).on(
                            authtokens.id ==
                            inner_perms.authtoken_id).join(permissions).on(
                                permissions.id == inner_perms.permission_id).
                    where(authtokens.source_type == Parameter('%s')).where(
                        authtokens.source_id == Parameter('%s')).where(
                            permissions.name == Parameter('%s')))).get_sql(),
            ('password_authentication', id, perm.lower()))

        if found_any:
            events = Table('password_authentication_events')
            itgs.write_cursor.execute(
                Query.into(events).columns(
                    events.password_authentication_id, events.type,
                    events.reason, events.user_id,
                    events.permission_id).from_(permissions).select(
                        Parameter('%s'), Parameter('%s'), Parameter('%s'),
                        Parameter('%s'),
                        permissions.id).where(permissions.name == Parameter(
                            '%s')).limit(1).get_sql(),
                (id, 'permission-revoked',
                 'No reason provided; performed manually', user_id,
                 perm.lower()))
        itgs.write_conn.commit()

        if not found_any:
            return Response(status_code=409,
                            headers={'x-request-cost': str(request_cost)})

        return Response(status_code=200,
                        headers={'x-request-cost': str(request_cost)})