def post(self): post_data = request.get_json() email = post_data.get('email', '') email = email.lower() if email else '' tier = post_data.get('tier') organisation_id = post_data.get('organisation_id', None) if not (email and tier): response_object = {'message': 'No email or tier provided'} return make_response(jsonify(response_object)), 400 if not AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', tier): return make_response( jsonify({ 'message': f'User does not have permission to invite {tier}' })), 400 if organisation_id and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): response_object = { 'message': 'Not Authorised to set organisation ID' } return make_response(jsonify(response_object)), 401 target_organisation_id = organisation_id or g.active_organisation.id if not target_organisation_id: response_object = { 'message': 'Must provide an organisation to bind user to' } return make_response(jsonify(response_object)), 400 organisation = Organisation.query.get(target_organisation_id) if not organisation: response_object = {'message': 'Organisation Not Found'} return make_response(jsonify(response_object)), 404 email_exists = EmailWhitelist.query.filter( func.lower(EmailWhitelist.email) == email).first() if email_exists: response_object = {'message': 'Email already on whitelist.'} return make_response(jsonify(response_object)), 400 invite = EmailWhitelist(email=email, tier=tier, organisation_id=target_organisation_id) db.session.add(invite) send_invite_email(invite, organisation) db.session.commit() response_object = { 'message': 'An invite has been sent!', } return make_response(jsonify(attach_host(response_object))), 201
def put(self, user_id): user = User.query.get(user_id) reset_tfa = request.args.get('reset_tfa', False) == 'true' if user is not None: if reset_tfa and AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): user.reset_TFA() if reset_tfa and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): response_object = { 'status': 'fail', 'message': 'Only Sempo admininstrators may reset TFA codes.', } return make_response(jsonify(response_object)), 403 user.reset_password() response_object = { 'status': 'success', 'message': 'Successfully reset password for user.', 'data': { 'user': user_schema.dump(user).data } } return make_response(jsonify(response_object)), 200 else: response_object = { 'message': 'No user to reset password for', } return make_response(jsonify(response_object)), 400
def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) if initial_disbursement is None: # initial disbursement defaults to None. If initial_disbursement is set then skip this section. # If none, then we want to see if the active_org has a default disbursement amount initial_disbursement = active_org.default_disbursement # Baseline is NOT is_approved, and do NOT auto_resolve self.is_approved = False auto_resolve = False # If admin role is admin or higher, then auto-approval is contingent on being less than or # equal to the default disbursement if (admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'))or ( g.get('auth_type') == 'external' and active_org.auto_approve_externally_created_users ): self.is_approved = True if initial_disbursement <= active_org.default_disbursement: auto_resolve = True # Accounts created by superadmins are all approved, and their disbursements are # auto-resolved no matter how big they are! if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): self.is_approved = True auto_resolve = True if self.is_beneficiary: # Initial disbursement should be pending if the account is not approved disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve=auto_resolve) return disbursement
def sempo_admin_involved(credit_transfer): if credit_transfer.recipient_user and AccessControl.has_sufficient_tier( credit_transfer.recipient_user.roles, 'ADMIN', 'sempoadmin'): return True if credit_transfer.sender_user and AccessControl.has_sufficient_tier( credit_transfer.sender_user.roles, 'ADMIN', 'sempoadmin'): return True return False
def get(self): # HANDLE PARAM : search_stirng - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''.format(order_arg)} order = asc if order_arg.upper()=='ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' final_query = generate_search_query(search_string, filters, order, sort_by_arg) transfer_accounts, total_items, total_pages, _ = paginate_query(final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(transfer_accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(transfer_accounts) return { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'query_time': datetime.datetime.utcnow(), 'data': { 'transfer_accounts': result.data } }
def approve_and_disburse(self, initial_disbursement=None): from server.utils.access_control import AccessControl active_org = getattr(g, 'active_organisation', self.primary_user.default_organisation) admin = getattr(g, 'user', None) auto_resolve = initial_disbursement == active_org.default_disbursement if not self.is_approved and admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'admin'): self.is_approved = True if self.is_beneficiary: # TODO: make this more robust # approve_and_disburse might be called for a second time to disburse # so first check that no credit transfer have already been received if len(self.credit_receives) < 1: # make initial disbursement disbursement = self._make_initial_disbursement(initial_disbursement, auto_resolve) return disbursement elif len(self.credit_receives) == 1: # else likely initial disbursement received, check if DISBURSEMENT and PENDING and resolve if default disbursement = self.credit_receives[0] if disbursement.transfer_subtype == TransferSubTypeEnum.DISBURSEMENT and disbursement.transfer_status == TransferStatusEnum.PENDING and auto_resolve: disbursement.resolve_as_completed() return disbursement
def test_update_admin_user_tier(new_sempo_admin_user): """ GIVEN a User model WHEN a user tier is updated to superadmin THEN check that all lower tiers are True """ new_sempo_admin_user.set_held_role('ADMIN', 'view') assert AccessControl.has_any_tier(new_sempo_admin_user.roles, 'ADMIN') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'subadmin') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'admin') assert not AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'superadmin') # update user tier to super admin new_sempo_admin_user.set_held_role('ADMIN', 'superadmin') assert AccessControl.has_any_tier(new_sempo_admin_user.roles, 'ADMIN') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'subadmin') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'admin') assert AccessControl.has_sufficient_tier(new_sempo_admin_user.roles, 'ADMIN', 'superadmin')
def approve_initial_disbursement(self): from server.utils.access_control import AccessControl admin = getattr(g, 'user', None) active_org = getattr(g, 'active_organisation', Organisation.master_organisation()) initial_disbursement = db.session.query(server.models.credit_transfer.CreditTransfer)\ .filter(server.models.credit_transfer.CreditTransfer.recipient_user == self.primary_user)\ .filter(server.models.credit_transfer.CreditTransfer.is_initial_disbursement == True)\ .first() if initial_disbursement and initial_disbursement.transfer_status == TransferStatusEnum.PENDING: # Must be superadmin to auto-resolve something over default disbursement if initial_disbursement.transfer_amount > active_org.default_disbursement: if admin and AccessControl.has_sufficient_tier(admin.roles, 'ADMIN', 'superadmin'): return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority') else: return False else: return initial_disbursement.resolve_as_complete_and_trigger_blockchain(queue='high-priority')
def check_if_approved(self): if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'sempoadmin'): return True if current_app.config['REQUIRE_MULTIPLE_APPROVALS']: # It always has to be approved by at least two people if len(self.approvers) <= 1: return False # If there's an `ALLOWED_APPROVERS` list, one of the approvers has to be in it if current_app.config['ALLOWED_APPROVERS']: # approve if email in list for user in self.approvers: if user.email in current_app.config['ALLOWED_APPROVERS']: return True # If there's not an `ALLOWED_APPROVERS` list, it just has to be approved by more than one person else: return True else: # Multi-approval is off, so it's approved by default return True
def check_if_fully_approved(self): # Checks if the credit transfer is approved and ready to be resolved as complete if current_app.config[ 'REQUIRE_MULTIPLE_APPROVALS'] and not AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'sempoadmin'): if len(self.approvers) <= 1: return False else: # If there's an `ALLOWED_APPROVERS` list, one of the approvers has to be in it if current_app.config['ALLOWED_APPROVERS']: # approve if email in list for user in self.approvers: if user.email in current_app.config[ 'ALLOWED_APPROVERS']: return True # If there's not an `ALLOWED_APPROVERS` list, it just has to be approved by more than one person else: return True else: return True
def post(self): post_data = request.get_json() email = post_data.get('email', '') email = email.lower() if email else '' tier = post_data.get('tier') organisation_id = post_data.get('organisation_id', None) if not (email and tier): response_object = {'message': 'No email or tier provided'} return make_response(jsonify(response_object)), 400 if not AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', tier): return make_response(jsonify({'message': f'User does not have permission to invite {tier}'})), 400 if organisation_id and not AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'sempoadmin'): response_object = {'message': 'Not Authorised to set organisation ID'} return make_response(jsonify(response_object)), 401 target_organisation_id = organisation_id or g.active_organisation.id if not target_organisation_id: response_object = {'message': 'Must provide an organisation to bind user to'} return make_response(jsonify(response_object)), 400 organisation = Organisation.query.get(target_organisation_id) if not organisation: response_object = {'message': 'Organisation Not Found'} return make_response(jsonify(response_object)), 404 email_exists_for_org = EmailWhitelist.query.filter(func.lower(EmailWhitelist.email)==email).first() if email_exists_for_org: response_object = {'message': 'Email already on organisation whitelist.'} return make_response(jsonify(response_object)), 400 email_exists = EmailWhitelist.query.filter(func.lower(EmailWhitelist.email)==email)\ .execution_options(show_all=True).first() if email_exists and not email_exists.used: response_object = {'message': 'Email already on another organisation whitelist. ' 'Please ask user to create an account first. ' 'Contact support if issue persists.'} return make_response(jsonify(response_object)), 400 user = User.query.filter(func.lower(User.email)==email).execution_options(show_all=True).first() if user: user.add_user_to_organisation(organisation, is_admin=True) send_invite_email_to_existing_user(organisation, user.email) db.session.commit() response_object = { 'message': 'An invite has been sent to an existing user!', } return make_response(jsonify(attach_host(response_object))), 201 invite = EmailWhitelist(email=email, tier=tier, organisation_id=target_organisation_id) db.session.add(invite) send_invite_email(invite, organisation) db.session.commit() response_object = { 'message': 'An invite has been sent!', } return make_response(jsonify(attach_host(response_object))), 201
def get(self, credit_transfer_id): transfer_account_ids = request.args.get('transfer_account_ids') # Handle search parameters # HANDLE PARAM : search_string - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''. format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' if credit_transfer_id: credit_transfer = CreditTransfer.query.get(credit_transfer_id) if credit_transfer is None: return make_response( jsonify({'message': 'Credit transfer not found'})), 404 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = credit_transfer_schema.dump(credit_transfer).data elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfer_schema.dump(credit_transfer).data transfer_stats = [] response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'data': { 'credit_transfer': result, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200 else: if transfer_account_ids: # We're getting a list of transfer accounts - parse try: parsed_transfer_account_ids = list( map(lambda x: int(x), filter(None, transfer_account_ids.split(',')))) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 400 if parsed_transfer_account_ids: try: query = generate_search_query( search_string, filters, order, sort_by_arg, search_type=CREDIT_TRANSFER) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 200 final_query = query.filter( or_( CreditTransfer.recipient_transfer_account_id.in_( parsed_transfer_account_ids), CreditTransfer.sender_transfer_account_id.in_( parsed_transfer_account_ids))) credit_transfers, total_items, total_pages, new_last_fetched = paginate_query( final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'admin'): result = credit_transfers_schema.dump(credit_transfers) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfers_schema.dump( credit_transfers) return { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow(), 'data': { 'credit_transfers': result.data } } else: try: final_query = generate_search_query( search_string, filters, order, sort_by_arg, search_type=CREDIT_TRANSFER) except Exception as e: response_object = {'status': 'fail', 'message': str(e)} return make_response(jsonify(response_object)), 200 credit_transfers, total_items, total_pages, new_last_fetched = paginate_query( final_query, ignore_last_fetched=True) if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = credit_transfers_schema.dump(credit_transfers) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_credit_transfers_schema.dump(credit_transfers) return { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow(), 'data': { 'credit_transfers': result.data } }
def post(self, credit_transfer_id): auto_resolve = AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'superadmin') post_data = request.get_json() uuid = post_data.get('uuid') queue = 'low-priority' transfer_type = post_data.get('transfer_type') transfer_amount = abs( round(Decimal(post_data.get('transfer_amount') or 0), 6)) token_id = post_data.get('token_id') target_balance = post_data.get('target_balance') transfer_use = post_data.get('transfer_use') try: use_ids = transfer_use.split(',') # passed as '3,4' etc. except AttributeError: use_ids = transfer_use sender_user_id = post_data.get('sender_user_id') recipient_user_id = post_data.get('recipient_user_id') # These could be phone numbers, email, nfc serial numbers, card numbers etc sender_public_identifier = post_data.get('sender_public_identifier') recipient_public_identifier = post_data.get( 'recipient_public_identifier') sender_transfer_account_id = post_data.get( 'sender_transfer_account_id') recipient_transfer_account_id = post_data.get( 'recipient_transfer_account_id') recipient_transfer_accounts_ids = post_data.get( 'recipient_transfer_accounts_ids') # invert_recipient_list will send to everyone _except_ for the users in recipient_transfer_accounts_ids invert_recipient_list = post_data.get('invert_recipient_list', False) invert_recipient_list = False if invert_recipient_list == False else True credit_transfers = [] response_list = [] is_bulk = False transfer_card = None if uuid: existing_transfer = CreditTransfer.query.filter_by( uuid=uuid).first() # We return a 201 here so that the client removes the uuid from the cache response_object = { 'message': 'Transfer Successful', 'data': { 'credit_transfer': credit_transfer_schema.dump(existing_transfer).data, } } return make_response(jsonify(response_object)), 201 if transfer_amount <= 0 and not target_balance and not ( transfer_amount == 0 and transfer_type == "BALANCE"): response_object = { 'message': 'Transfer amount must be positive', } return make_response(jsonify(response_object)), 400 if recipient_transfer_accounts_ids: is_bulk = True batch_uuid = str(uuid4()) if transfer_type not in ["DISBURSEMENT", "BALANCE"]: response_object = { 'message': 'Bulk transfer must be either disbursement or balance', } return make_response(jsonify(response_object)), 400 transfer_user_list = [] individual_sender_user = None if invert_recipient_list: all_accounts_query = TransferAccount.query.filter( TransferAccount.is_ghost != True).filter_by( organisation_id=g.active_organisation.id) all_user_accounts_query = (all_accounts_query.filter( TransferAccount.account_type == TransferAccountType.USER)) all_accounts_except_selected_query = all_user_accounts_query.filter( not_( TransferAccount.id.in_( recipient_transfer_accounts_ids))) for individual_recipient_user in all_accounts_except_selected_query.all( ): transfer_user_list.append( (individual_sender_user, individual_recipient_user.primary_user, None)) else: for transfer_account_id in recipient_transfer_accounts_ids: try: individual_recipient_user, transfer_card = find_user_with_transfer_account_from_identifiers( None, None, transfer_account_id) transfer_user_list.append( (individual_sender_user, individual_recipient_user, transfer_card)) except (NoTransferAccountError, UserNotFoundError) as e: response_list.append({ 'status': 400, 'message': str(e) }) else: batch_uuid = None try: individual_sender_user, transfer_card = find_user_with_transfer_account_from_identifiers( sender_user_id, sender_public_identifier, sender_transfer_account_id) individual_recipient_user, _ = find_user_with_transfer_account_from_identifiers( recipient_user_id, recipient_public_identifier, recipient_transfer_account_id) transfer_user_list = [ (individual_sender_user, individual_recipient_user, transfer_card) ] except Exception as e: response_object = { 'message': str(e), } return make_response(jsonify(response_object)), 400 if token_id: token = Token.query.get(token_id) if not token: response_object = {'message': 'Token not found'} return make_response(jsonify(response_object)), 404 else: active_organisation = g.active_organisation if active_organisation is None: response_object = {'message': 'Must provide token_id'} return make_response(jsonify(response_object)), 400 else: token = active_organisation.token for sender_user, recipient_user, transfer_card in transfer_user_list: try: if transfer_type == 'PAYMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, receive_user=recipient_user, transfer_use=transfer_use, transfer_mode=TransferModeEnum.WEB, uuid=uuid, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid, transfer_card=transfer_card) elif transfer_type == 'RECLAMATION': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.RECLAMATION, transfer_mode=TransferModeEnum.WEB, require_recipient_approved=False, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid) elif transfer_type == 'DISBURSEMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=g.user, receive_user=recipient_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, transfer_mode=TransferModeEnum.WEB, automatically_resolve_complete=False, queue=queue, batch_uuid=batch_uuid) elif transfer_type == 'BALANCE': transfer = make_target_balance_transfer( target_balance, recipient_user, uuid=uuid, automatically_resolve_complete=False, transfer_mode=TransferModeEnum.WEB, queue=queue, ) if auto_resolve: transfer.add_approver_and_resolve_as_completed() except (InsufficientBalanceError, AccountNotApprovedError, InvalidTargetBalanceError, BlockchainError, Exception) as e: if is_bulk: response_list.append({'status': 400, 'message': str(e)}) else: db.session.commit() response_object = {'message': str(e)} return make_response(jsonify(response_object)), 400 else: message = 'Transfer Successful' if auto_resolve else 'Transfer Pending. Must be approved.' if is_bulk: credit_transfers.append(transfer) response_list.append({'status': 201, 'message': message}) else: db.session.flush() credit_transfer = credit_transfer_schema.dump( transfer).data response_object = { 'message': message, 'is_create': True, 'data': { 'credit_transfer': credit_transfer, } } return make_response(jsonify(response_object)), 201 db.session.flush() message = 'Bulk Transfer Creation Successful' if auto_resolve else 'Bulk Transfer Pending. Must be approved.' response_object = { 'message': message, 'bulk_responses': response_list, 'data': { 'credit_transfers': credit_transfers_schema.dump(credit_transfers).data } } return make_response(jsonify(response_object)), 201
def get(self, transfer_account_id): account_type_filter = request.args.get('account_type') result = None if transfer_account_id: transfer_account = TransferAccount.query.get(transfer_account_id) if transfer_account is None: response_object = { 'message': 'No such transfer account: {}'.format(transfer_account_id), } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_account_schema.dump(transfer_account) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_account_schema.dump(transfer_account) response_object = { 'message': 'Successfully Loaded.', 'data': { 'transfer_account': result.data, } } return make_response(jsonify(response_object)), 201 else: search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\'' .format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_by_arg = request.args.get('sort_by') or 'rank' base_query = generate_search_query( search_string, filters, order, sort_by_arg).filter(TransferAccount.is_ghost != True) if account_type_filter == 'vendor': transfer_accounts_query = base_query.filter_by(is_vendor=True) elif account_type_filter == 'beneficiary': transfer_accounts_query = base_query.filter_by( is_beneficiary=True) else: pass # Filter Contract, Float and Organisation Transfer Accounts transfer_accounts_query = (base_query.filter( TransferAccount.account_type == TransferAccountType.USER)) transfer_accounts, total_items, total_pages, new_last_fetched = paginate_query( transfer_accounts_query) if transfer_accounts is None: response_object = { 'message': 'No transfer accounts', } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(transfer_accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(transfer_accounts) response_object = { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow().strftime("%m/%d/%Y, %H:%M:%S"), 'data': { 'transfer_accounts': result.data } } return make_response(json.dumps(response_object), 200)
def get(self, credit_transfer_id): transfer_account_ids = request.args.get('transfer_account_ids') transfer_type = request.args.get('transfer_type', 'ALL') get_transfer_stats = request.args.get('get_stats', False) transfer_list = None if transfer_type: transfer_type = transfer_type.upper() if credit_transfer_id: credit_transfer = CreditTransfer.query.get(credit_transfer_id) if credit_transfer is None: return make_response( jsonify({'message': 'Credit transfer not found'})), 404 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump([credit_transfer ]).data elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): transfer_list = view_credit_transfers_schema.dump( [credit_transfer]).data transfer_stats = [] response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'data': { 'credit_transfer': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200 else: query = CreditTransfer.query transfer_list = None if transfer_type != 'ALL': try: transfer_type_enum = TransferTypeEnum[transfer_type] query = query.filter( CreditTransfer.transfer_type == transfer_type_enum) except KeyError: response_object = { 'message': 'Invalid Filter: Transfer Type ', } return make_response(jsonify(response_object)), 400 if transfer_account_ids: # We're getting a list of transfer accounts - parse try: parsed_transfer_account_ids = list( map(lambda x: int(x), filter(None, transfer_account_ids.split(',')))) except ValueError: response_object = { 'message': 'Invalid Filter: Transfer Account IDs ', } return make_response(jsonify(response_object)), 400 if parsed_transfer_account_ids: query = query.filter( or_( CreditTransfer.recipient_transfer_account_id.in_( parsed_transfer_account_ids), CreditTransfer.sender_transfer_account_id.in_( parsed_transfer_account_ids))) transfers, total_items, total_pages = paginate_query( query, CreditTransfer) if get_transfer_stats: transfer_stats = calculate_transfer_stats( total_time_series=True) else: transfer_stats = None if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump(transfers).data elif AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'view'): transfer_list = view_credit_transfers_schema.dump( transfers).data response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'data': { 'credit_transfers': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200
def post(self, credit_transfer_id): auto_resolve = AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'superadmin') post_data = request.get_json() uuid = post_data.get('uuid') transfer_type = post_data.get('transfer_type') transfer_amount = abs( round(float(post_data.get('transfer_amount') or 0), 6)) token_id = post_data.get('token_id') target_balance = post_data.get('target_balance') transfer_use = post_data.get('transfer_use') sender_user_id = post_data.get('sender_user_id') recipient_user_id = post_data.get('recipient_user_id') # These could be phone numbers, email, nfc serial numbers, card numbers etc sender_public_identifier = post_data.get('sender_public_identifier') recipient_public_identifier = post_data.get( 'recipient_public_identifier') sender_transfer_account_id = post_data.get( 'sender_transfer_account_id') recipient_transfer_account_id = post_data.get( 'recipient_transfer_account_id') recipient_transfer_accounts_ids = post_data.get( 'recipient_transfer_accounts_ids') credit_transfers = [] response_list = [] is_bulk = False if uuid: existing_transfer = CreditTransfer.query.filter_by( uuid=uuid).first() # We return a 201 here so that the client removes the uuid from the cache response_object = { 'message': 'Transfer Successful', 'data': { 'credit_transfer': credit_transfer_schema.dump(existing_transfer).data, } } return make_response(jsonify(response_object)), 201 if transfer_amount <= 0 and not target_balance: response_object = { 'message': 'Transfer amount must be positive', } return make_response(jsonify(response_object)), 400 if recipient_transfer_accounts_ids: is_bulk = True if transfer_type not in ["DISBURSEMENT", "BALANCE"]: response_object = { 'message': 'Bulk transfer must be either disbursement or balance', } return make_response(jsonify(response_object)), 400 transfer_user_list = [] for transfer_account_id in recipient_transfer_accounts_ids: try: individual_sender_user = None individual_recipient_user = find_user_with_transfer_account_from_identifiers( None, None, transfer_account_id) transfer_user_list.append( (individual_sender_user, individual_recipient_user)) except (NoTransferAccountError, UserNotFoundError) as e: response_list.append({'status': 400, 'message': str(e)}) else: try: individual_sender_user = find_user_with_transfer_account_from_identifiers( sender_user_id, sender_public_identifier, sender_transfer_account_id) individual_recipient_user = find_user_with_transfer_account_from_identifiers( recipient_user_id, recipient_public_identifier, recipient_transfer_account_id) transfer_user_list = [(individual_sender_user, individual_recipient_user)] except Exception as e: response_object = { 'message': str(e), } return make_response(jsonify(response_object)), 400 if token_id: token = Token.query.get(token_id) if not token: response_object = {'message': 'Token not found'} return make_response(jsonify(response_object)), 404 else: active_organisation = g.active_organisation if active_organisation is None: response_object = {'message': 'Must provide token_id'} return make_response(jsonify(response_object)), 400 else: token = active_organisation.token for sender_user, recipient_user in transfer_user_list: try: if transfer_type == 'PAYMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, receive_user=recipient_user, transfer_use=transfer_use, uuid=uuid, automatically_resolve_complete=auto_resolve) elif transfer_type == 'RECLAMATION': transfer = make_payment_transfer( transfer_amount, token=token, send_user=sender_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.RECLAMATION, require_recipient_approved=False, automatically_resolve_complete=auto_resolve) elif transfer_type == 'DISBURSEMENT': transfer = make_payment_transfer( transfer_amount, token=token, send_user=g.user, receive_user=recipient_user, uuid=uuid, transfer_subtype=TransferSubTypeEnum.DISBURSEMENT, automatically_resolve_complete=auto_resolve) elif transfer_type == 'BALANCE': transfer = make_target_balance_transfer( target_balance, recipient_user, uuid=uuid, automatically_resolve_complete=auto_resolve) except (InsufficientBalanceError, AccountNotApprovedError, InvalidTargetBalanceError, BlockchainError, Exception) as e: if is_bulk: response_list.append({'status': 400, 'message': str(e)}) else: db.session.commit() response_object = {'message': str(e)} return make_response(jsonify(response_object)), 400 else: message = 'Transfer Successful' if auto_resolve else 'Transfer Pending. Must be approved.' if is_bulk: credit_transfers.append(transfer) response_list.append({'status': 201, 'message': message}) else: db.session.flush() credit_transfer = credit_transfer_schema.dump( transfer).data response_object = { 'message': message, 'is_create': True, 'data': { 'credit_transfer': credit_transfer, } } return make_response(jsonify(response_object)), 201 db.session.flush() message = 'Bulk Transfer Creation Successful' if auto_resolve else 'Bulk Transfer Pending. Must be approved.' response_object = { 'message': message, 'bulk_responses': response_list, 'data': { 'credit_transfers': credit_transfers_schema.dump(credit_transfers).data } } return make_response(jsonify(response_object)), 201
def get(self): # HANDLE PARAM : search_stirng - Any search string. An empty string (or None) will just return everything! search_string = request.args.get('search_string') or '' # HANDLE PARAM : params - Standard filter object. Exact same as the ones Metrics uses! encoded_filters = request.args.get('params') filters = process_transfer_filters(encoded_filters) # HANDLE PARAM : order # Valid orders types are: `ASC` and `DESC` # Default: DESC order_arg = request.args.get('order') or 'DESC' if order_arg.upper() not in ['ASC', 'DESC']: return { 'message': 'Invalid order value \'{}\'. Please use \'ASC\' or \'DESC\''. format(order_arg) } order = asc if order_arg.upper() == 'ASC' else desc # HANDLE PARAM: sort_by # Valid orders types are: first_name, last_name, email, date_account_created, rank, balance, status # Default: rank sort_types_to_database_types = { 'first_name': User.first_name, 'last_name': User.last_name, 'email': User.email, 'date_account_created': User.created, 'rank': 'rank', 'balance': TransferAccount._balance_wei, 'status': TransferAccount.is_approved, } sort_by_arg = request.args.get('sort_by') or 'rank' if sort_by_arg not in sort_types_to_database_types: return { 'message': f'Invalid sort_by value {sort_by_arg}. Please use one of the following: {sort_types_to_database_types.keys()}'\ } # To add new searchable column, simply add a new SearchableColumn object! # And don't forget to add a trigram index on that column too-- see migration 33df5e72fca4 for reference user_search_columns = [ SearchableColumn('first_name', User.first_name, rank=1.5), SearchableColumn('last_name', User.last_name, rank=1.5), SearchableColumn('phone', User.phone, rank=2), SearchableColumn('public_serial_number', User.public_serial_number, rank=2), SearchableColumn('location', User.location, rank=1), SearchableColumn('primary_blockchain_address', User.primary_blockchain_address, rank=2), ] sum_search = reduce(lambda x, y: x + y, [ sc.get_similarity_query(search_string) for sc in user_search_columns ]) sort_by = sum_search if sort_by_arg == 'rank' else sort_types_to_database_types[ sort_by_arg] # If there's no search string, the process is the same, just sort by account creation date sort_by = sort_types_to_database_types[ 'date_account_created'] if sort_by == 'rank' and not search_string else sort_by final_query = db.session.query(TransferAccount, User, sum_search)\ .with_entities(TransferAccount, sum_search)\ .outerjoin(TransferAccount, User.default_transfer_account_id == TransferAccount.id)\ .filter(TransferAccount.is_ghost != True)\ .order_by(order(sort_by)) # If there is a search string, we only want to return ranked results! final_query = final_query.filter( sum_search != 0) if search_string else final_query final_query = apply_filters(final_query, filters, User) transfer_accounts, total_items, total_pages, _ = paginate_query( final_query, ignore_last_fetched=True) accounts = [resultTuple[0] for resultTuple in transfer_accounts] if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(accounts) return { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'query_time': datetime.datetime.utcnow(), 'data': { 'transfer_accounts': result.data } }
def put(self, disbursement_id): put_data = request.get_json() action = put_data.get('action', '').upper() notes = put_data.get('notes') or '' if not disbursement_id: return {'message': 'Please provide a disbursement_id'}, 400 if not action: return {'message': 'Please provide an action'}, 400 if action not in ['APPROVE', 'REJECT']: # We might want more actions later! return {'message': f'{action} not a valid action'}, 400 # Ensure it's impossible to have two threads operating on the same disbursement with red.lock(name=f'Disbursemnt{disbursement_id}', timeout=10, blocking_timeout=20): disbursement = Disbursement.query.filter(Disbursement.id == disbursement_id)\ .options(joinedload(Disbursement.credit_transfers))\ .first() disbursement.notes = notes if not disbursement: return { 'message': f'Disbursement with ID \'{disbursement_id}\' not found' }, 400 if disbursement.state in ['APPROVED', 'REJECTED']: return { 'message': f'Disbursement with ID \'{disbursement_id}\' has already been set to {disbursement.state.lower()}!' }, 400 if action == 'APPROVE': disbursement.approve() db.session.commit() auto_resolve = False if current_app.config[ 'REQUIRE_MULTIPLE_APPROVALS'] or AccessControl.has_sufficient_tier( g.user.roles, 'ADMIN', 'superadmin'): auto_resolve = True # A disbursement isn't necessarily approved after approve() is called, since we can require multiple approvers task_uuid = None if disbursement.state == 'APPROVED': task_uuid = add_after_request_checkable_executor_job( make_transfers, kwargs={ 'disbursement_id': disbursement.id, 'auto_resolve': auto_resolve }) data = disbursement_schema.dump(disbursement).data return { 'status': 'success', 'data': { 'disbursement': data }, 'task_uuid': task_uuid }, 200 if action == 'REJECT': disbursement.reject() db.session.commit() data = disbursement_schema.dump(disbursement).data return { 'status': 'success', 'data': { 'disbursement': data }, }, 200
def get(self, transfer_account_id): account_type_filter = request.args.get('account_type') result = None if transfer_account_id: transfer_account = TransferAccount.query.get(transfer_account_id) if transfer_account is None: response_object = { 'message': 'No such transfer account: {}'.format(transfer_account_id), } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_account_schema.dump(transfer_account) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_account_schema.dump(transfer_account) response_object = { 'message': 'Successfully Loaded.', 'data': { 'transfer_account': result.data, } } return make_response(jsonify(response_object)), 201 else: base_query = TransferAccount.query.filter( TransferAccount.is_ghost != True) if account_type_filter == 'vendor': transfer_accounts_query = base_query.filter_by(is_vendor=True) elif account_type_filter == 'beneficiary': transfer_accounts_query = base_query.filter_by( is_beneficiary=True) else: pass # Filter Contract, Float and Organisation Transfer Accounts transfer_accounts_query = (base_query.filter( TransferAccount.account_type == TransferAccountType.USER)) transfer_accounts, total_items, total_pages, new_last_fetched = paginate_query( transfer_accounts_query) if transfer_accounts is None: response_object = { 'message': 'No transfer accounts', } return make_response(jsonify(response_object)), 400 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): result = transfer_accounts_schema.dump(transfer_accounts) elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): result = view_transfer_accounts_schema.dump(transfer_accounts) response_object = { 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'last_fetched': new_last_fetched, 'query_time': datetime.datetime.utcnow().strftime("%m/%d/%Y, %H:%M:%S"), 'data': { 'transfer_accounts': result.data } } return make_response(json.dumps(response_object), 200)
def is_supervendor(self): return AccessControl.has_sufficient_tier(self.roles, 'VENDOR', 'supervendor')
def get(self, credit_transfer_id): transfer_account_ids = request.args.get('transfer_account_ids') transfer_type = request.args.get('transfer_type', 'ALL') get_transfer_stats = False transfer_list = None if transfer_type: transfer_type = transfer_type.upper() if credit_transfer_id: credit_transfer = CreditTransfer.query.get(credit_transfer_id) if credit_transfer is None: return make_response( jsonify({'message': 'Credit transfer not found'})), 404 if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump([credit_transfer ]).data elif AccessControl.has_any_tier(g.user.roles, 'ADMIN'): transfer_list = view_credit_transfers_schema.dump( [credit_transfer]).data transfer_stats = [] response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'data': { 'credit_transfer': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200 else: query = CreditTransfer.query transfer_list = None if transfer_type != 'ALL': try: transfer_type_enum = TransferTypeEnum[transfer_type] query = query.filter( CreditTransfer.transfer_type == transfer_type_enum) except KeyError: response_object = { 'message': 'Invalid Filter: Transfer Type ', } return make_response(jsonify(response_object)), 400 if transfer_account_ids: # We're getting a list of transfer accounts - parse try: parsed_transfer_account_ids = list( map(lambda x: int(x), filter(None, transfer_account_ids.split(',')))) except ValueError: response_object = { 'message': 'Invalid Filter: Transfer Account IDs ', } return make_response(jsonify(response_object)), 400 if parsed_transfer_account_ids: query = query.filter( or_( CreditTransfer.recipient_transfer_account_id.in_( parsed_transfer_account_ids), CreditTransfer.sender_transfer_account_id.in_( parsed_transfer_account_ids))) transfers, total_items, total_pages = paginate_query( query, CreditTransfer) # # if get_transfer_stats: # transfer_stats = calculate_transfer_stats(total_time_series=True) # else: # transfer_stats = None transfer_stats = { 'total_distributed': '15300.0000000000000000', 'total_spent': 0, 'total_exchanged': 0, 'has_transferred_count': 0, 'zero_balance_count': 0, 'total_beneficiaries': 3, 'total_users': 3, 'master_wallet_balance': 0, 'daily_transaction_volume': [{ 'date': '2020-04-29T07:05:01.771621', 'volume': 0 }], 'daily_disbursement_volume': [{ 'date': '2020-04-29T00:00:00', 'volume': ('300.0000000000000000') }, { 'date': '2020-04-28T00:00:00', 'volume': '15000.0000000000000000' }], 'transfer_use_breakdown': [], 'last_day_volume': { 'date': '2020-04-29T07:05:01.771617', 'volume': 0 }, 'filter_active': False } if AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'admin'): transfer_list = credit_transfers_schema.dump(transfers).data elif AccessControl.has_sufficient_tier(g.user.roles, 'ADMIN', 'view'): transfer_list = view_credit_transfers_schema.dump( transfers).data response_object = { 'status': 'success', 'message': 'Successfully Loaded.', 'items': total_items, 'pages': total_pages, 'data': { 'credit_transfers': transfer_list, 'transfer_stats': transfer_stats } } return make_response(jsonify(response_object)), 200