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)})
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)})
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)
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' })
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())
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)
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)
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())
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' })
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")
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)
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)
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 )
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)
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)
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)
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)
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)})
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) })
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)
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)
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) })
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)
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())
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 )
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)})