Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enforce write only access to folder http endpoint #1109

Merged
merged 11 commits into from
Aug 12, 2024
30 changes: 17 additions & 13 deletions runhouse/resources/folders/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def local_path(self):
else:
return None

def mv(self, system, path: Optional[str] = None) -> None:
def mv(self, system, path: Optional[str] = None, overwrite: bool = True) -> None:
"""Move the folder to a new filesystem or cluster.

Example:
Expand All @@ -155,7 +155,7 @@ def mv(self, system, path: Optional[str] = None) -> None:
)

# Create the destination directory if it doesn't exist
dest_path.parent.mkdir(parents=True, exist_ok=True)
dest_path.parent.mkdir(parents=True, exist_ok=overwrite)

# Move the directory
shutil.move(str(src_path), str(dest_path))
Expand Down Expand Up @@ -467,6 +467,20 @@ def _path_absolute_to_rh_workdir(path):
else str(Path(locate_working_dir()) / path)
)

@staticmethod
def _delete_contents(contents: List, folder_path: Path, recursive: bool):
for content in contents:
content_path = folder_path / content
if content_path.exists():
if content_path.is_file():
content_path.unlink()
elif content_path.is_dir() and recursive:
shutil.rmtree(content_path)
else:
raise ValueError(
f"Path {content_path} is a directory and recursive is set to False"
)

@property
def fsspec_url(self):
"""Generate the FSSpec style URL using the file system and path of the folder"""
Expand Down Expand Up @@ -685,17 +699,7 @@ def rm(self, contents: list = None, recursive: bool = True):
folder_path = Path(self.path).expanduser()

if contents:
for content in contents:
content_path = folder_path / content
if content_path.exists():
if content_path.is_file():
content_path.unlink()
elif content_path.is_dir() and recursive:
shutil.rmtree(content_path)
else:
raise ValueError(
f"Path {content_path} is a directory and recursive is set to False"
)
Folder._delete_contents(contents, folder_path, recursive)
else:
if recursive:
shutil.rmtree(folder_path)
Expand Down
4 changes: 4 additions & 0 deletions runhouse/servers/http/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def clear_cache(self, token: str = None):
async def averify_cluster_access(
cluster_uri: str,
token: str,
access_level_required: ResourceAccess = None,
) -> bool:
"""Checks whether the user has access to the cluster.
Note: A user with write access to the cluster or a cluster owner will have access to all other resources on
Expand All @@ -94,4 +95,7 @@ async def averify_cluster_access(

cluster_access_level = await obj_store.aresource_access_level(token, cluster_uri)

if access_level_required is not None:
return cluster_access_level == access_level_required

return cluster_access_level in [ResourceAccess.WRITE, ResourceAccess.READ]
129 changes: 126 additions & 3 deletions runhouse/servers/http/http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,34 @@
)
from runhouse.globals import configs, obj_store, rns_client
from runhouse.logger import logger
from runhouse.rns.utils.api import resolve_absolute_path
from runhouse.rns.utils.api import resolve_absolute_path, ResourceAccess
from runhouse.rns.utils.names import _generate_default_name
from runhouse.servers.caddy.config import CaddyConfig
from runhouse.servers.http.auth import averify_cluster_access
from runhouse.servers.http.certs import TLSCertConfig
from runhouse.servers.http.http_utils import (
CallParams,
DeleteObjectParams,
folder_exists,
folder_get,
folder_ls,
folder_mkdir,
folder_mv,
folder_put,
folder_rm,
FolderGetParams,
FolderLsParams,
FolderMvParams,
FolderParams,
FolderPutParams,
FolderRmParams,
get_token_from_request,
handle_exception_response,
OutputType,
PutObjectParams,
PutResourceParams,
RenameObjectParams,
resolve_folder_path,
Response,
serialize_data,
ServerSettings,
Expand All @@ -53,7 +67,6 @@
)
from runhouse.utils import sync_function


app = FastAPI(docs_url=None, redoc_url=None)


Expand All @@ -67,6 +80,11 @@ async def wrapper(*args, **kwargs):
is_coro = inspect.iscoroutinefunction(func)

func_call: bool = func.__name__ in ["post_call", "get_call"]

# restrict access for folder specific APIs
access_level_required = (
ResourceAccess.WRITE if func.__name__.startswith("folder") else None
)
token = get_token_from_request(request)

request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
Expand All @@ -82,7 +100,9 @@ async def wrapper(*args, **kwargs):
"provide a valid token in the Authorization header.",
)
cluster_uri = (await obj_store.aget_cluster_config()).get("name")
cluster_access = await averify_cluster_access(cluster_uri, token)
cluster_access = await averify_cluster_access(
cluster_uri, token, access_level_required
)
if not cluster_access:
# Must have cluster access for all the non func calls
# Note: for func calls we handle the auth in the object store
Expand Down Expand Up @@ -612,6 +632,109 @@ async def rename_object(request: Request, params: RenameObjectParams):
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/ls")
@validate_cluster_access
async def folder_ls_cmd(request: Request, ls_params: FolderLsParams):
try:
path = resolve_folder_path(ls_params.path)
return folder_ls(path, full_paths=ls_params.full_paths, sort=ls_params.sort)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/mkdir")
@validate_cluster_access
async def folder_mkdir_cmd(request: Request, folder_params: FolderParams):
try:
path = resolve_folder_path(folder_params.path)
return folder_mkdir(path)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/get")
@validate_cluster_access
async def folder_get_cmd(request: Request, get_params: FolderGetParams):
try:
path = resolve_folder_path(get_params.path)
return folder_get(path, mode=get_params.mode, encoding=get_params.encoding)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/put")
@validate_cluster_access
async def folder_put_cmd(request: Request, put_params: FolderPutParams):
try:
path = resolve_folder_path(put_params.path)
return folder_put(
path,
contents=put_params.contents,
overwrite=put_params.overwrite,
mode=put_params.mode,
serialization=put_params.serialization,
)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/rm")
@validate_cluster_access
async def folder_rm_cmd(request: Request, rm_params: FolderRmParams):
try:
path = resolve_folder_path(rm_params.path)
return folder_rm(
path, contents=rm_params.contents, recursive=rm_params.recursive
)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/mv")
@validate_cluster_access
async def folder_mv_cmd(request: Request, mv_params: FolderMvParams):
try:
path = resolve_folder_path(mv_params.path)
return folder_mv(
src_path=path,
dest_path=mv_params.dest_path,
overwrite=mv_params.overwrite,
)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/folder/method/exists")
@validate_cluster_access
async def folder_exists_cmd(request: Request, folder_params: FolderParams):
try:
path = resolve_folder_path(folder_params.path)
return folder_exists(path=path)

except Exception as e:
return handle_exception_response(
e, traceback.format_exc(), from_http_server=True
)

@staticmethod
@app.post("/delete_object")
@validate_cluster_access
Expand Down
Loading
Loading