def post(self): folder_path = request.values.get('folderPath', request.values.get('folder_path', '')) if not folder_path: abort(400) if not folder_path.startswith('/'): abort(400) folder = find_resource(folder_path) if not folder: abort(404) if access_level(folder.query_permissions()) < ACCESS_LEVEL_WRITE: abort(403) if not request.authorization: abort(403) key = find_key(request.authorization.password) if not key: abort(403) message_type = request.values['type'] parameters = json.loads(request.values['parameters']) sender_controller_id = key.access_as_controller_id # None if access as user sender_user_id = key.access_as_user_id # None if access as controller message_queue.add(folder.id, None, message_type, parameters, sender_controller_id=sender_controller_id, sender_user_id=sender_user_id) return {'status': 'ok'}
def post(self): folder_path = request.values.get('folderPath', request.values.get('folder_path', '')) if not folder_path: abort(400) folder = find_resource(folder_path) if not folder: abort(404) if access_level(folder.query_permissions()) < ACCESS_LEVEL_WRITE: abort(403) type = request.values['type'] parameters = json.loads(request.values['parameters']) auth_code = request.values.get('authCode', '') # fix(soon): migrate away from this if auth_code: key = find_key_by_code( auth_code ) # fix(faster): we already looked up key in access_level function elif request.authorization: key = find_key(request.authorization.password) if not key: abort(403) sender_controller_id = key.access_as_controller_id # None if access as user sender_user_id = key.access_as_user_id # None if access as controller message_queue.add(folder.id, type, parameters, sender_controller_id=sender_controller_id, sender_user_id=sender_user_id) return {'status': 'ok'}
def manage_web_socket(ws): ws_conn = WebSocketConnection(ws) # handle key-based authentication if request.authorization: logging.debug('ws connect with auth') auth = request.authorization key = find_key( auth.password) # key is provided as HTTP basic auth password if not key: logging.debug('key not found') return # would be nice to abort(403), but doesn't look like you can do that inside a websocket handler ws_conn.controller_id = key.access_as_controller_id ws_conn.user_id = key.access_as_user_id ws_conn.auth_method = 'key' if ws_conn.controller_id: try: controller_status = ControllerStatus.query.filter( ControllerStatus.id == ws_conn.controller_id).one() controller_status.last_connect_timestamp = datetime.datetime.utcnow( ) controller_status.client_version = auth.username # client should pass version in HTTP basic auth user name db.session.commit() except NoResultFound: logging.debug( 'warning: unable to find controller status record') # handle regular user authentication elif current_user.is_authenticated: logging.debug('ws connect with web browser session') ws_conn.user_id = current_user.id ws_conn.auth_method = 'user' # register this socket to receive outgoing messages socket_sender.register(ws_conn) # process incoming messages while not ws_conn.ws.closed: message = ws_conn.ws.receive() if message: try: message_struct = json.loads(message) except json.JSONDecodeError: break # if client sends bad message; close this connection process_web_socket_message(message_struct, ws_conn) time.sleep(0.05) # sleep to let other stuff run # websocket has been closed ws_conn.log_disconnect() socket_sender.unregister(ws_conn) db.session.close()
def post(self): # create a key for a controller if 'access_as_controller_id' in request.values: try: access_as_controller_id = int(request.values['access_as_controller_id']) except ValueError: abort(400) try: r = Resource.query.filter(Resource.id == access_as_controller_id, not_(Resource.deleted)).one() except NoResultFound: abort(400) if access_level(r.query_permissions()) < ACCESS_LEVEL_WRITE: # require write access to the controller to create a key for it abort(403) organization_id = r.root().id if current_user.is_anonymous: # handle special case of creating a key using a user-associated key (controllers aren't allowed to create keys) key = find_key(request.authorization.password) if key.access_as_user_id: creation_user_id = key.access_as_user_id else: creation_user_id = None abort(403) else: creation_user_id = current_user.id (k, key_text) = create_key(creation_user_id, organization_id, None, access_as_controller_id, key_text=request.values.get('key')) return {'status': 'ok', 'id': k.id, 'secret_key': key_text} # create a key for a users # fix(later): handle case of creating a user-associated key using a user-associated key (see code above) elif 'access_as_user_id' in request.values: access_as_user_id = request.values['access_as_user_id'] try: User.query.filter(User.id == access_as_user_id, not_(User.deleted)).one() except NoResultFound: abort(400) organization_id = request.values['organization_id'] # fix(soon): instead check that (1) access_as_user_id is a member of org and (2) current user has admin access to org; # current check is too strict if current_user.is_anonymous or current_user.role != current_user.SYSTEM_ADMIN: abort(403) (k, key_text) = create_key(current_user.id, organization_id, access_as_user_id, None) return {'status': 'ok', 'id': k.id, 'secret_key': key_text}
def _bootstrap_controller_auth(app_config: Dict[str, str]): """Create an organization, controller folder, and API key.""" org_name = app_config.get('TERRAWARE_ORGANIZATION_NAME') org_folder = app_config.get('TERRAWARE_ORGANIZATION_FOLDER') folder_name = app_config.get('TERRAWARE_CONTROLLER_FOLDER') secret_key = app_config.get('TERRAWARE_CONTROLLER_SECRET_KEY') if org_name and org_folder and folder_name and secret_key: org_resource = Resource.query.filter( Resource.name == org_folder, Resource.type == Resource.ORGANIZATION_FOLDER).one_or_none() if org_resource is not None: org_id = org_resource.id else: logger.info('Creating organization %s', org_name) org_id = create_organization(org_name, org_folder) controller_resource = Resource.query.filter( Resource.name == folder_name, Resource.type == Resource.CONTROLLER_FOLDER).one_or_none() if controller_resource is None: logger.info('Creating controller folder %s', folder_name) controller_resource = Resource(name=folder_name, type=Resource.CONTROLLER_FOLDER, parent_id=org_id) db.session.add(controller_resource) key_resource = find_key(secret_key) if key_resource is None: logger.info('Creating secret key for controller') admin_user_id = db.session.query(User.id).order_by( User.id).limit(1).scalar() (key_resource, _) = create_key(admin_user_id, org_id, None, controller_resource.id, secret_key) db.session.commit()
def access_level(permissions, controller_id=None): # determine current user (if any) # fix(soon): handle API auth with key as user if not handled elsewhere user_id = current_user.id if current_user.is_authenticated else None # handle system admin # fix(soon): require that system admins explicitly add themselves to orgs if user_id and current_user.role == current_user.SYSTEM_ADMIN: return ACCESS_LEVEL_WRITE # determine current API client (if any) if not controller_id: key = None if 'authCode' in request.values: # fix(soon): remove this case auth_code = request.values['authCode'] key = find_key_by_code(auth_code) elif request.authorization: auth = request.authorization key = find_key( auth.password) # key is provided as HTTP basic auth password if key: if key.access_as_controller_id: controller_id = key.access_as_controller_id elif key.access_as_user_id: user_id = key.access_as_user_id # start with no access client_access_level = ACCESS_LEVEL_NONE # take max level of all applicable permissions for permission_record in permissions: (type, id, level) = permission_record # applies to everyone if type == ACCESS_TYPE_PUBLIC: client_access_level = max(client_access_level, level) # applies if current user is contained within the organization given by the permission ID elif type == ACCESS_TYPE_ORG_USERS: if user_id: try: org_user = OrganizationUser.query.filter( OrganizationUser.user_id == user_id, OrganizationUser.organization_id == id).one() client_access_level = max(client_access_level, level) break except NoResultFound: pass # applies if current controller is contained within the organization given by the permission ID elif type == ACCESS_TYPE_ORG_CONTROLLERS: if controller_id: try: controller = Resource.query.filter( Resource.id == controller_id, Resource.deleted == False).one() controller_org_id = controller.organization_id if controller.organization_id else controller.root( ).id # fix(soon): remove this after all resources have org ids if controller_org_id == id: client_access_level = max(client_access_level, level) break except NoResultFound: pass # applies if permission ID is the same as current user ID elif type == ACCESS_TYPE_USER: if user_id and user_id == id: client_access_level = max(client_access_level, level) # applies if permission ID is the same as current controller ID elif type == ACCESS_TYPE_CONTROLLER: if controller_id and controller_id == id: client_access_level = max(client_access_level, level) return client_access_level
def get(self, resource_path): args = request.values result = {} # handle case of controller requesting about self if resource_path == 'self': if 'authCode' in request.values: auth_code = request.values.get('authCode', '') # fix(soon): remove auth codes key = find_key_by_code(auth_code) elif request.authorization: key = find_key(request.authorization.password) else: key = None if key and key.access_as_controller_id: try: r = Resource.query.filter(Resource.id == key.access_as_controller_id).one() except NoResultFound: abort(404) else: abort(403) # look up the resource record else: r = find_resource('/' + resource_path) if not r: abort(404) # fix(later): revisit to avoid leaking file existance if access_level(r.query_permissions()) < ACCESS_LEVEL_READ: abort(403) # if request meta-data if request.values.get('meta', False): result = r.as_dict(extended = True) if request.values.get('include_path', False): result['path'] = r.path() # if request data else: # if folder, return contents list or zip of collection of files if r.type >= 10 and r.type < 20: # multi-file download if 'ids' in args and args.get('download', False): ids = args['ids'].split(',') return batch_download(r, ids) # contents list else: recursive = request.values.get('recursive', False) type_name = request.values.get('type', None) if type_name: type = resource_type_number(type_name) else: type = None filter = request.values.get('filter', None) extended = request.values.get('extended', False) result = resource_list(r.id, recursive, type, filter, extended) # if sequence, return value(s) # fix(later): merge with file case? elif r.type == Resource.SEQUENCE: # get parameters text = request.values.get('text', '') download = request.values.get('download', False) count = int(request.values.get('count', 1)) start_timestamp = request.values.get('start_timestamp', '') end_timestamp = request.values.get('end_timestamp', '') if start_timestamp: try: start_timestamp = parse_json_datetime(start_timestamp) except: abort(400, 'Invalid date/time.') if end_timestamp: try: end_timestamp = parse_json_datetime(end_timestamp) except: abort(400, 'Invalid date/time.') # if filters specified, assume we want a sequence of values if text or start_timestamp or end_timestamp or count > 1: # get summary of values if int(request.values.get('summary', False)): return sequence_value_summary(r.id) # get preliminary set of values resource_revisions = ResourceRevision.query.filter(ResourceRevision.resource_id == r.id) # apply filters (if any) if text: resource_revisions = resource_revisions.filter(text in ResourceRevision.data) if start_timestamp: resource_revisions = resource_revisions.filter(ResourceRevision.timestamp >= start_timestamp) if end_timestamp: resource_revisions = resource_revisions.filter(ResourceRevision.timestamp <= end_timestamp) resource_revisions = resource_revisions.order_by('timestamp') if resource_revisions.count() > count: resource_revisions = resource_revisions[-count:] # fix(later): is there a better/faster way to do this? # return data if download: #timezone = r.root().system_attributes['timezone'] # fix(soon): use this instead of UTC lines = ['utc_timestamp,value\n'] for rr in resource_revisions: lines.append('%s,%s\n' % (rr.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f'), rr.data)) result = make_response(''.join(lines)) result.headers['Content-Type'] = 'application/octet-stream' result.headers['Content-Disposition'] = 'attachment; filename=' + r.name + '.csv' return result else: epoch = datetime.datetime.utcfromtimestamp(0) # fix(clean): merge with similar code for sequence viewer timestamps = [(rr.timestamp.replace(tzinfo = None) - epoch).total_seconds() for rr in resource_revisions] # fix(clean): use some sort of unzip function values = [rr.data for rr in resource_revisions] units = json.loads(r.system_attributes).get('units', None) return {'name': r.name, 'units': units, 'timestamps': timestamps, 'values': values} # if no filter assume just want current value # fix(later): should instead provide all values and have a separate way to get more recent value? else: rev = request.values.get('rev') if rev: rev = int(rev) # fix(soon): save int conversion result = make_response(read_resource(r, revision_id = rev)) data_type = json.loads(r.system_attributes)['data_type'] if data_type == Resource.IMAGE_SEQUENCE: result.headers['Content-Type'] = 'image/jpeg' else: result.headers['Content-Type'] = 'text/plain' # if file, return file data/contents else: data = read_resource(r) if not data: abort(404) name = r.name if request.values.get('convert_to', request.values.get('convertTo', '')) == 'xls' and r.name.endswith('csv'): data = convert_csv_to_xls(data) name = name.replace('csv', 'xls') result = make_response(data) result.headers['Content-Type'] = 'application/octet-stream' if request.values.get('download', False): result.headers['Content-Disposition'] = 'attachment; filename=' + name return result
def put(self): values = json.loads(request.values['values']) if 'timestamp' in request.values: timestamp = parse_json_datetime(request.values['timestamp']) # check for drift delta = datetime.datetime.utcnow() - timestamp drift = delta.total_seconds() # print 'drift', drift if abs(drift) > 30: # get current controller correction # fix(later): support user updates as well? auth = request.authorization key = find_key(auth.password ) # key is provided as HTTP basic auth password if key and key.access_as_controller_id: controller_id = key.access_as_controller_id controller_status = ControllerStatus.query.filter( ControllerStatus.id == controller_id).one() attributes = json.loads(controller_status.attributes) correction = attributes.get('timestamp_correction', 0) # if stored correction is reasonable, use it; otherwise store new correction if abs(correction - drift) > 100: correction = drift attributes['timestamp_correction'] = drift controller_status.attributes = json.dumps(attributes) db.session.commit() # print 'storing new correction (%.2f)' % correction else: pass # print 'applying previous correction (%.2f)' % correction timestamp += datetime.timedelta(seconds=correction) else: timestamp = datetime.datetime.utcnow() # for now, assume all sequences in same folder items = list(values.items()) if items: items = sorted( items ) # sort by keys so we can re-use folder lookup and permission check between items in same folder folder_resource = None folder_name = None for (full_name, value) in items: item_folder_name = full_name.rsplit('/', 1)[0] if item_folder_name != folder_name: # if this folder doesn't match the folder resource record we have folder_name = item_folder_name folder_resource = find_resource(folder_name) if folder_resource and access_level( folder_resource.query_permissions( )) < ACCESS_LEVEL_WRITE: folder_resource = None # don't have write access if folder_resource: seq_name = full_name.rsplit('/', 1)[1] try: resource = (Resource.query.filter( Resource.parent_id == folder_resource.id, Resource.name == seq_name, not_(Resource.deleted)).one()) update_sequence_value( resource, full_name, timestamp, str(value), emit_message=True ) # fix(later): revisit emit_message except NoResultFound: pass db.session.commit()