Skip to content
This repository has been archived by the owner on Feb 23, 2022. It is now read-only.

Commit

Permalink
Merge pull request #402 from multinet-app/reader-decorator
Browse files Browse the repository at this point in the history
Add @require_reader decorator where appropriate
  • Loading branch information
waxlamp committed Jun 26, 2020
2 parents 8566fe0 + 9fbab46 commit 42f937b
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 23 deletions.
15 changes: 13 additions & 2 deletions multinet/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing import Any, Optional, List
from multinet.types import EdgeDirection, TableType
from multinet.auth.util import require_login, is_reader
from multinet.auth.util import require_login, require_reader, is_reader
from multinet.validation import ValidationFailure, UndefinedKeys, UndefinedTable

from multinet import db, util
Expand Down Expand Up @@ -35,13 +35,17 @@ def get_workspaces() -> Any:


@bp.route("/workspaces/<workspace>", methods=["GET"])
@require_reader
@swag_from("swagger/workspace.yaml")
def get_workspace(workspace: str) -> Any:
"""Retrieve a single workspace."""
return db.get_workspace(workspace)
metadata = db.get_workspace_metadata(workspace)

return {"name": metadata["name"], "permissions": metadata["permissions"]}


@bp.route("/workspaces/<workspace>/tables", methods=["GET"])
@require_reader
@use_kwargs({"type": fields.Str()})
@swag_from("swagger/workspace_tables.yaml")
def get_workspace_tables(workspace: str, type: TableType = "all") -> Any: # noqa: A002
Expand All @@ -51,6 +55,7 @@ def get_workspace_tables(workspace: str, type: TableType = "all") -> Any: # noq


@bp.route("/workspaces/<workspace>/tables/<table>", methods=["GET"])
@require_reader
@use_kwargs({"offset": fields.Int(), "limit": fields.Int()})
@swag_from("swagger/table_rows.yaml")
def get_table_rows(workspace: str, table: str, offset: int = 0, limit: int = 30) -> Any:
Expand All @@ -59,6 +64,7 @@ def get_table_rows(workspace: str, table: str, offset: int = 0, limit: int = 30)


@bp.route("/workspaces/<workspace>/graphs", methods=["GET"])
@require_reader
@swag_from("swagger/workspace_graphs.yaml")
def get_workspace_graphs(workspace: str) -> Any:
"""Retrieve the graphs of a single workspace."""
Expand All @@ -67,13 +73,15 @@ def get_workspace_graphs(workspace: str) -> Any:


@bp.route("/workspaces/<workspace>/graphs/<graph>", methods=["GET"])
@require_reader
@swag_from("swagger/workspace_graph.yaml")
def get_workspace_graph(workspace: str, graph: str) -> Any:
"""Retrieve information about a graph."""
return db.workspace_graph(workspace, graph)


@bp.route("/workspaces/<workspace>/graphs/<graph>/nodes", methods=["GET"])
@require_reader
@use_kwargs({"offset": fields.Int(), "limit": fields.Int()})
@swag_from("swagger/graph_nodes.yaml")
def get_graph_nodes(
Expand All @@ -87,6 +95,7 @@ def get_graph_nodes(
"/workspaces/<workspace>/graphs/<graph>/nodes/<table>/<node>/attributes",
methods=["GET"],
)
@require_reader
@swag_from("swagger/node_data.yaml")
def get_node_data(workspace: str, graph: str, table: str, node: str) -> Any:
"""Return the attributes associated with a node."""
Expand All @@ -96,6 +105,7 @@ def get_node_data(workspace: str, graph: str, table: str, node: str) -> Any:
@bp.route(
"/workspaces/<workspace>/graphs/<graph>/nodes/<table>/<node>/edges", methods=["GET"]
)
@require_reader
@use_kwargs({"direction": fields.Str(), "offset": fields.Int(), "limit": fields.Int()})
@swag_from("swagger/node_edges.yaml")
def get_node_edges(
Expand Down Expand Up @@ -132,6 +142,7 @@ def create_workspace(workspace: str) -> Any:


@bp.route("/workspaces/<workspace>/aql", methods=["POST"])
@require_reader
@swag_from("swagger/aql.yaml")
def aql(workspace: str) -> Any:
"""Perform an AQL query in the given workspace."""
Expand Down
18 changes: 18 additions & 0 deletions multinet/auth/util.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"""Utility functions for auth."""

import functools
from typing import Any, Optional, Callable

from multinet import db
from multinet.errors import Unauthorized
from multinet.types import Workspace
from multinet.auth.types import UserInfo
Expand All @@ -14,6 +16,7 @@
def require_login(f: Callable) -> Callable:
"""Decorate an API endpoint to check for a logged in user."""

@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
user = current_user()
if user is None:
Expand Down Expand Up @@ -42,3 +45,18 @@ def is_reader(user: Optional[UserInfo], workspace: Workspace) -> bool:
or sub in perms["maintainers"]
or perms["owner"] == sub
)


def require_reader(f: Any) -> Any:
"""Decorate an API endpoint to require read permission."""

@functools.wraps(f)
def wrapper(workspace: str, *args: Any, **kwargs: Any) -> Any:
user = current_user()
workspace_metadata = db.get_workspace_metadata(workspace)
if not is_reader(user, workspace_metadata):
raise Unauthorized(f"You must be a Reader of workspace {workspace}")

return f(workspace, *args, **kwargs)

return wrapper
23 changes: 13 additions & 10 deletions multinet/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from arango.exceptions import DatabaseCreateError, EdgeDefinitionCreateError
from requests.exceptions import ConnectionError

from typing import Any, List, Dict, Set, Generator, Union
from typing import Any, List, Dict, Set, Generator, Optional
from typing_extensions import TypedDict
from multinet.types import EdgeDirection, TableType, Workspace
from multinet.types import EdgeDirection, TableType, Workspace, WorkspaceDocument
from multinet.auth.types import User
from multinet.errors import InternalServerError
from multinet import util
Expand All @@ -25,14 +25,11 @@
NodeNotFound,
AlreadyExists,
GraphCreationError,
DatabaseCorrupted,
)


# Type definitions.
WorkspaceSpec = TypedDict(
"WorkspaceSpec",
{"name": str, "owner": str, "readers": List[str], "writers": List[str]},
)
GraphSpec = TypedDict("GraphSpec", {"nodeTables": List[str], "edgeTable": str})
GraphNodesSpec = TypedDict("GraphNodesSpec", {"count": int, "nodes": List[str]})
GraphEdgesSpec = TypedDict("GraphEdgesSpec", {"count": int, "edges": List[str]})
Expand Down Expand Up @@ -88,7 +85,7 @@ def workspace_mapping_collection() -> StandardCollection:

# Caches the document that maps an external workspace name to it's internal one
@lru_cache()
def workspace_mapping(name: str) -> Union[Dict[str, str], None]:
def workspace_mapping(name: str) -> Optional[WorkspaceDocument]:
"""
Get the document containing the workspace mapping for :name: (if it exists).
Expand Down Expand Up @@ -195,12 +192,18 @@ def delete_workspace(name: str) -> None:
workspace_mapping.cache_clear()


def get_workspace(name: str) -> WorkspaceSpec:
"""Return a single workspace, if it exists."""
def get_workspace_metadata(name: str) -> Workspace:
"""Return the metadata for a single workspace, if it exists."""
if not workspace_exists(name):
raise WorkspaceNotFound(name)

return {"name": name, "owner": "", "readers": [], "writers": []}
# Find the metadata record for the named workspace. If it's not there,
# something went very wrong, so bail out.
metadata = workspace_mapping(name)
if metadata is None:
raise DatabaseCorrupted()

return metadata


# Caches the reference to the StandardDatabase instance for each workspace
Expand Down
8 changes: 8 additions & 0 deletions multinet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def flask_response(self) -> FlaskTuple:
return ("", "500 Internal Server Error")


class DatabaseCorrupted(ServerError):
"""The database has a consistency issue."""

def flask_response(self) -> FlaskTuple:
"""Generate a 500 level error."""
return ("", "500 Database Corrupted")


class Unauthorized(ServerError):
"""The request did not indicate sufficient permission."""

Expand Down
24 changes: 18 additions & 6 deletions multinet/swagger/template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,47 @@ definitions:
required:
- name
- owner
- readers
- maintainers
- writers
- readers
- public
properties:
name:
description: The name of the workspace
type: string
owner:
description: The owner of the workspace, who has read and write access, and can also delete it
description: The owner of the workspace
type: string
readers:
description: A list of users with read access to the workspace
maintainers:
description: The list of maintainers of the workspace
type: array
items:
type: string
writers:
description: A list of users with write access to the workspace
description: The list of writers of the workspace
type: array
items:
type: string
readers:
description: The list of readers of the workspace
type: array
items:
type: string
public:
description: Whether this workspace is public or not
type: boolean
example:
name: engineering
owner: laforge
readers:
- worf
- data
- troi
writers:
maintainers:
- picard
- riker
writers: []
public: False

graph:
description: A description of a graph, including its constituent tables
Expand Down
18 changes: 15 additions & 3 deletions multinet/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,21 @@
},
)

Workspace = TypedDict(
"Workspace", {"name": str, "internal": str, "permissions": WorkspacePermissions}
)

class Workspace(TypedDict):
"""Workspace metadata."""

name: str
internal: str
permissions: WorkspacePermissions


class WorkspaceDocument(Workspace):
"""Workspace metadata as it appears in ArangoDB."""

_id: str
_key: str
_rev: str


class EdgeTableProperties(TypedDict):
Expand Down
4 changes: 2 additions & 2 deletions mypy_stubs/arango/collection.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from arango.cursor import Cursor # type: ignore
from arango.exceptions import ArangoError # type: ignore
from typing import Any, Optional, Dict, Union, List
from typing import Any, Optional, Dict, Union, List, Mapping

class Collection:
def count(self) -> int: ...
Expand Down Expand Up @@ -40,7 +40,7 @@ class StandardCollection(Collection):
) -> Dict: ...
def update(
self,
document: Union[Dict, str],
document: Union[Mapping[str, Any], str],
check_rev: bool = ...,
merge: bool = ...,
keep_none: bool = ...,
Expand Down

0 comments on commit 42f937b

Please sign in to comment.