Beispiel #1
0
def current_drinks(adapter):
    # optional request paremeter, the name of the machine to get stock information
    machine_name = request.args.get('machine', None)

    # assemble an array of (id, name) tuples
    if machine_name is None:
        machines = adapter.get_machines()
        logger.debug('Fetching contents for machines {}'.format(', '.join(
            [m['name'] for m in machines])))
    else:
        # We're given a machine name
        machine = adapter.get_machine(machine_name)

        if machine is None:
            err = f'The provided machine name \'{machine_name}\' is not a valid machine'
            logger.error(err)
            return bad_params(err)

        logger.debug('Fetching contents for machine {}'.format(machine_name))
        machines = []
        machines.append(machine)

    response = {}

    with Pool(5) as p:
        contents = p.map(query_machine, machines)
        response['machines'] = contents

    response[
        'message'] = 'Successfully retrieved machine contents for {}'.format(
            ', '.join([machine['name'] for machine in machines]))

    return jsonify(response), 200
Beispiel #2
0
        def wrapped_function(*args, **kwargs):

            logger.debug('Begin handling request for {}'.format(request.host))
            unauthorized = {
                "error": "Could not authenticate user",
                "errorCode": 401
            }
            key_unauthorized = {
                "error": "Unable to authenticate trusted client",
                "errorCode": 401
            }
            permissions = {
                "error": "User does not have the correct permissions",
                "errorCode": 401
            }

            # First, could this be a trusted client?
            key = request.headers.get('X-Auth-Token', None)
            if key is not None:
                if key == app.config['MACHINE_API_TOKEN']:
                    return func(*args, **kwargs)
                else:
                    return jsonify(key_unauthorized), 401

            # Otherwise, validate JWT against SSO
            token = request.headers.get('Authorization', None)
            if token is None:
                logger.debug('User unauthorized with no Bearer token')
                return jsonify(unauthorized), 401

            headers = {"Authorization": token}

            endpoint = 'https://sso.csh.rit.edu/auth/realms/csh/protocol/openid-connect/userinfo'

            verify_response = requests.get(endpoint, headers=headers)

            try:
                verify_response.raise_for_status()
            except requests.exceptions.HTTPError:
                logger.debug('Unable to verify Bearer token against provider')
                return jsonify(unauthorized), 401

            verify_body = verify_response.json()

            mock = request.args.get('mock', False)
            if isinstance(mock, str):
                mock = mock.lower().startswith('t')

            if admin_only and not mock:
                if not 'drink' in verify_body['groups']:
                    return jsonify(permissions), 401

            logger.debug('Successfully authenticated user {}'.format(
                verify_body['preferred_username']))
            if return_user_obj:
                return func(*args, user=verify_body, **kwargs)

            return func(*args, **kwargs)
Beispiel #3
0
def _get_machine_status(machine_name):
    """ helper function to query a machine given it's name (should be the actual hostname, will be dropped into a
        {}.csh.rit.edu template), and return slot status information in a more programmatically useful way.

        Realistically, the data should look like this coming back from the mahcine -- this is low hanging fruit for someone
        to fix.

        Warning::

            This doesn't actually validate the machine name. That should be done in what ever route needs to call this function.

        Raises:
            requests.exceptions.HTTPError: in the event the machine responds with a non-2XX
            requests.exceptions.Timeout: if the machine does not respond with any bytes within 5 seconds
            requests.exceptions.ConnectionError: if the machine is not online
    """

    headers = {
        'X-Auth-Token': app.config['MACHINE_API_TOKEN'],
        'Content-Type': 'application/json'
    }

    endpoint = 'https://{}.csh.rit.edu/health'.format(machine_name)
    health_status = requests.get(endpoint, headers=headers, timeout=5)
    health_status.raise_for_status()

    health_results = health_status.json()

    logger.debug('Reached machine {} succesfully'.format(machine_name))

    slots = []

    for idx, slot in enumerate(health_results['slots'], 1):
        slots.append({
            'number': idx,
            'w1id': slot.split('(')[1].split(')')[0],
            'empty': 'empty' in slot
        })

    return slots
Beispiel #4
0
def query_machine(machine):
    logger.debug('Querying machine details for {}'.format(machine['name']))
    machine_slots = adapter.get_slots_in_machine(machine['name'])

    is_online = True

    try:
        slot_status = _get_machine_status(machine['name'])
    except requests.exceptions.ConnectionError:
        # We couldn't connect to the machine
        logger.debug('Machine {} is unreachable, reporting as offline'.format(
            machine['name']))
        slot_status = [{'empty': True} for n in range(len(machine_slots))]
        is_online = False  # seems a useful feature
    except requests.exceptions.Timeout:
        # We hit a timeout waiting for the machine to respond
        logger.debug(
            'Machine {} was reachable, but did not respond within a reasonable amount of time'
            .format(machine['name']))
        slot_status = [{'empty': True} for n in range(len(machine_slots))]
        is_online = False

    machine_contents = {
        'id': machine['id'],
        'name': machine['name'],
        'display_name': machine['display_name'],
        'is_online': is_online,
        'slots': []
    }

    for slot in machine_slots:
        slot['empty'] = slot_status[slot['number'] - 1]['empty']
        machine_contents['slots'].append(slot)

    logger.debug('Fetched all available details for {}'.format(
        machine['name']))
    return machine_contents
Beispiel #5
0
def update_slot_status():
    if request.headers.get('Content-Type') != 'application/json':
        return bad_headers_content_type()

    body = request.json

    logger.debug('Handling slot update')

    unprovided = []
    if 'machine' not in body:
        unprovided.append('machine')
    if 'slot' not in body:
        unprovided.append('slot')

    if len(unprovided) > 0:
        return bad_params(
            'The following required parameters were not provided: {}'.format(
                ', '.join(unprovided)))

    if 'active' not in body and 'item_id' not in body:
        return bad_params(
            'Either the state or item within a slot must be provided for an update.'
        )

    updates = {}

    if 'active' in body:
        if not isinstance(body['active'], bool):
            return bad_params('The active parameter must be a boolean value')

        updates['active'] = body['active']

    if 'item_id' in body:
        try:
            item_id = int(body['item_id'])
            if item_id <= 0:
                raise ValueError()
        except ValueError:
            return bad_params('The item ID value must be a positive integer')

        updates['item'] = item_id

        item = db.session.query(Item).filter(Item.id == item_id).first()

        if item is None:
            return bad_params(
                'No item with ID {} is present in the system'.format(item_id))

    try:
        slot_num = int(body['slot'])

        if slot_num <= 0:
            raise ValueError()
    except ValueError:
        return bad_params('The slot number must be a positive integer')

    machine = db.session.query(Machine).filter(
        Machine.name == body['machine']).first()
    if machine is None:
        return bad_params('The machine \'{}\' is not a valid machine'.format(
            body['machine']))

    slot = db.session.query(Slot).filter(Slot.number == slot_num,
                                         Slot.machine == machine.id).first()
    if slot is None:
        return bad_params(
            'The machine \'{}\' does not have a slot number {}'.format(
                body['machine'],
                body['slot'],
            ))

    logger.debug('Slot update details validated')

    slot = db.session.query(Slot).filter(Slot.number == body['slot'], Slot.machine == machine.id).\
            update(updates, synchronize_session=False)

    if slot < 1:
        return jsonify({
            'error': 'Could not update slot',
            'errorCode': 500,
            'message': 'Contact a drink admin'
        }), 500

    db.session.commit()

    slot = db.session.query(Slot).filter(Slot.number == slot_num,
                                         Slot.machine == machine.id).first()

    success = {
        'message':
        'Successfully updated slot {} in {}'.format(slot.number,
                                                    body['machine']),
        'slot': {
            'machine': body['machine'],
            'number': slot.number,
            'active': slot.active,
            'item_id': slot.item,
        },
    }

    return jsonify(success), 200
Beispiel #6
0
def current_drinks(adapter):
    # optional request paremeter, the name of the machine to get stock information
    machine_name = request.args.get('machine', None)

    # assemble an array of (id, name) tuples
    if machine_name is None:
        machines = adapter.get_machines()
        logger.debug('Fetching contents for machines {}'.format(', '.join(
            [m['name'] for m in machines])))
    else:
        # We're given a machine name
        machine = adapter.get_machine(machine_name)

        if machine is None:
            return bad_params(
                'The provided machine name \'{}\' is not a valid machine'.
                format(machine_name))

        logger.debug('Fetching contents for machine {}'.format(machine_name))
        machines = []
        machines.append(machine)

    response = {"machines": []}

    for machine in machines:
        logger.debug('Querying machine details for {}'.format(machine['name']))
        machine_slots = adapter.get_slots_in_machine(machine['name'])

        is_online = True

        try:
            slot_status = _get_machine_status(machine['name'])
        except requests.exceptions.ConnectionError:
            # We couldn't connect to the machine
            logger.debug(
                'Machine {} is unreachable, reporting as offline'.format(
                    machine['name']))
            slot_status = [{'empty': True} for n in range(len(machine_slots))]
            is_online = False  # seems a useful feature
        except requests.exceptions.Timeout:
            # We hit a timeout waiting for the machine to respond
            logger.debug(
                'Machine {} was reachable, but did not respond within a reasonable amount of time'
                .format(machine['name']))
            slot_status = [{'empty': True} for n in range(len(machine_slots))]
            is_online = False

        machine_contents = {
            'id': machine['id'],
            'name': machine['name'],
            'display_name': machine['display_name'],
            'is_online': is_online,
            'slots': []
        }

        for slot in machine_slots:
            slot_item = adapter.get_item(slot['item'])
            machine_contents['slots'].append({
                "number":
                slot['number'],
                "active":
                slot['active'],
                "count":
                slot['count'],
                "empty":
                slot_status[slot['number'] - 1]['empty'],
                "item": {
                    "name": slot_item['name'],
                    "price": slot_item['price'],
                    "id": slot_item['id'],
                },
            })
        response['machines'].append(machine_contents)
        logger.debug('Fetched all available details for {}'.format(
            machine['name']))

    response[
        'message'] = 'Successfully retrieved machine contents for {}'.format(
            ', '.join([machine['name'] for machine in machines]))

    return jsonify(response), 200
Beispiel #7
0
def drop_drink(adapter, user=None):
    if request.headers.get('Content-Type') != 'application/json':
        return bad_headers_content_type()

    logger.debug('Handing request to drop drink')

    bal_before = _get_credits(user['preferred_username'])

    body = request.json

    unprovided = []
    if 'machine' not in body:
        unprovided.append('machine')
    if 'slot' not in body:
        unprovided.append('slot')

    if len(unprovided) > 0:
        return bad_params(
            'The following required parameters were not provided: {}'.format(
                ', '.join(unprovided)))

    machine = db.session.query(Machine).filter(
        Machine.name == body['machine']).first()
    if machine is None:
        return bad_params(
            'The machine name \'{}\' is not a valid machine'.format(
                body['machine']))

    slot = db.session.query(Slot).filter(Slot.number == body['slot'],
                                         Slot.machine == machine.id).first()
    if slot is None:
        return bad_params(
            'The machine \'{}\' does not have a slot with id \'{}\''.format(
                body['machine'], body['slot']))

    logger.debug('Drop request is valid')

    slot_status = _get_machine_status(body['machine'])
    if (body['machine'] == 'snack' and slot.count < 1
        ) or slot_status[body['slot'] - 1]['empty']:  # slots are 1 indexed
        return jsonify({
            "error": "The requested slot is empty!",
            "errorCode": 400
        }), 400

    item = db.session.query(Item).filter(Item.id == slot.item).first()

    if bal_before < item.price:
        response = {
            "error": "The user \'{}\' does not have a sufficient drinkBalance",
            "errorCode": 402
        }
        return jsonify(response), 402

    logger.debug('User has sufficient balance')

    machine_hostname = '{}.csh.rit.edu'.format(machine.name)
    request_endpoint = 'https://{}/drop'.format(machine_hostname)

    body = {"slot": slot.number}

    headers = {
        'X-Auth-Token': app.config['MACHINE_API_TOKEN'],
        'Content-Type': 'application/json'
    }

    # Do the thing
    try:
        response = requests.post(request_endpoint,
                                 json=body,
                                 headers=headers,
                                 timeout=5)
    except requests.exceptions.ConnectionError:
        return jsonify({
            "error": "Could not contact drink machine for drop!",
            "errorCode": 500
        }), 500
    except requests.exceptions.Timeout:
        return jsonify({
            "error": "Connection to the drink machine timed out!",
            "errorCode": 500
        }), 500

    try:
        response.raise_for_status()
    except requests.exceptions.HTTPError:
        return jsonify({"error": "Could not access slot for drop!",
                        "message": response.json()['error'],
                        "errorCode": response.status_code}),\
                       response.status_code

    logger.debug('Dropped drink - adjusting user credits')
    new_balance = bal_before - item.price
    _manage_credits(user['preferred_username'], new_balance, adapter)
    logger.debug('Credits for {} updated'.format(user['preferred_username']))

    if machine.name == 'snack':
        slot.count = Slot.count - 1  # Decrement stock count, set inactive if empty
        if slot.count == 0:
            slot.active = False
        db.session.commit()

    return jsonify({
        "message": "Drop successful!",
        "drinkBalance": new_balance
    }), response.status_code