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 #455 from multinet-app/orm
Browse files Browse the repository at this point in the history
  • Loading branch information
jjnesbitt committed Aug 31, 2020
2 parents 5e7b289 + 7c359a1 commit 8e4962e
Show file tree
Hide file tree
Showing 31 changed files with 1,320 additions and 1,289 deletions.
161 changes: 40 additions & 121 deletions multinet/api.py
Original file line number Diff line number Diff line change
@@ -1,115 +1,54 @@
"""Flask blueprint for Multinet REST API."""
from copy import deepcopy
from dataclasses import asdict
from flasgger import swag_from
from flask import Blueprint, request
from webargs import fields
from webargs.flaskparser import use_kwargs

from typing import Any, Optional, List, Dict, cast
from typing import Any, Optional
from multinet.types import EdgeDirection, TableType
from multinet.auth.util import (
require_login,
require_reader,
is_reader,
require_writer,
require_maintainer,
require_owner,
)
from multinet.validation import ValidationFailure, UndefinedKeys, UndefinedTable
from multinet.types import WorkspacePermissions

from multinet import db, util
from multinet import util
from multinet.errors import (
ValidationFailed,
BadQueryArgument,
MalformedRequestBody,
AlreadyExists,
RequiredParamsMissing,
)
from multinet.user import current_user, find_user_from_id

bp = Blueprint("multinet", __name__)


# Included here due to circular imports
# TODO: Remove once implementing new ORM and permission storage
def _permissions_id_to_user(permissons: WorkspacePermissions) -> Dict:
"""
Transform permission documents to directly contain user info.
Currently, `WorkspacePermissons` only contains references to users through their
`sub` values, stored as a str in the role to which it pertains. The client requires
more information to properly display/use permissions, so this function transforms
the `sub` values to the entire user object.
This fuction will eventually be supplanted by a change in our permission model.
"""

# Cast to a regular dict, since it won't actually be
# a `WorkspacePermissions` after we perform replacement
new_permissions = cast(Dict, deepcopy(permissons))

for role, users in new_permissions.items():
if role == "public":
continue

if role == "owner":
# Since the role is "owner", `users` is a `str`
user = find_user_from_id(users)
if user is not None:
new_permissions["owner"] = asdict(user)
else:
new_users = []
for sub in users:
user = find_user_from_id(sub)
if user is not None:
new_users.append(asdict(user))

new_permissions[role] = new_users

return new_permissions


# Included here due to circular imports
# TODO: Remove once implementing new ORM and permission storage
def _permissions_user_to_id(expanded_user_permissions: Dict) -> WorkspacePermissions:
"""Transform permission documents to only contain the `sub` values of users."""
permissions = deepcopy(expanded_user_permissions)
from multinet.db.models.user import current_user
from multinet.db.models.workspace import Workspace

for role, users in permissions.items():
if role == "public":
continue

if role == "owner":
if users is not None:
permissions["owner"] = users["sub"]
else:
permissions[role] = [user["sub"] for user in users]

return cast(WorkspacePermissions, permissions)
bp = Blueprint("multinet", __name__)


@bp.route("/workspaces", methods=["GET"])
@swag_from("swagger/workspaces.yaml")
def get_workspaces() -> Any:
"""Retrieve list of workspaces."""
"""Return the list of available workspaces, based on the logged in user."""
user = current_user()

# Filter all workspaces based on whether it should be shown to the user who
# is logged in.
stream = util.stream(w["name"] for w in db.get_workspaces() if is_reader(user, w))
return stream
# If the user is logged in, return all workspaces visible to them
if user is not None:
return util.stream(user.available_workspaces())

# Otherwise, return only public workspaces
return util.stream(Workspace.list_public())


@bp.route("/workspaces/<workspace>/permissions", methods=["GET"])
@require_maintainer
@swag_from("swagger/get_workspace_permissions.yaml")
def get_workspace_permissions(workspace: str) -> Any:
"""Retrieve the permissions of a workspace."""
metadata = db.get_workspace_metadata(workspace)

return _permissions_id_to_user(metadata["permissions"])
perms = Workspace(workspace).permissions
return util.expand_user_permissions(perms)


@bp.route("/workspaces/<workspace>/permissions", methods=["PUT"])
Expand All @@ -126,8 +65,8 @@ def set_workspace_permissions(workspace: str) -> Any:
}:
raise MalformedRequestBody(request.json)

perms = _permissions_user_to_id(request.json)
return _permissions_id_to_user(db.set_workspace_permissions(workspace, perms))
perms = util.contract_user_permissions(request.json)
return Workspace(workspace).set_permissions(perms).__dict__


@bp.route("/workspaces/<workspace>/tables", methods=["GET"])
Expand All @@ -136,7 +75,7 @@ def set_workspace_permissions(workspace: str) -> Any:
@swag_from("swagger/workspace_tables.yaml")
def get_workspace_tables(workspace: str, type: TableType = "all") -> Any: # noqa: A002
"""Retrieve the tables of a single workspace."""
tables = db.workspace_tables(workspace, type)
tables = Workspace(workspace).tables(type)
return util.stream(tables)


Expand All @@ -147,7 +86,7 @@ def get_workspace_tables(workspace: str, type: TableType = "all") -> Any: # noq
def create_aql_table(workspace: str, table: str) -> Any:
"""Create a table from an AQL query."""
aql = request.data.decode()
table = db.create_aql_table(workspace, table, aql)
Workspace(workspace).create_aql_table(table, aql)

return table

Expand All @@ -158,24 +97,25 @@ def create_aql_table(workspace: str, table: str) -> Any:
@swag_from("swagger/table_rows.yaml")
def get_table_rows(workspace: str, table: str, offset: int = 0, limit: int = 30) -> Any:
"""Retrieve the rows and headers of a table."""
return db.workspace_table(workspace, table, offset, limit)
return Workspace(workspace).table(table).rows(offset, limit)


@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."""
graphs = db.workspace_graphs(workspace)
return util.stream(graphs)
return util.stream((g["name"] for g in Workspace(workspace).graphs()))


@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)
node_tables = Workspace(workspace).graph(graph).node_tables()
edge_table = Workspace(workspace).graph(graph).edge_table()
return {"edgeTable": edge_table, "nodeTables": node_tables}


@bp.route("/workspaces/<workspace>/graphs/<graph>/nodes", methods=["GET"])
Expand All @@ -186,7 +126,7 @@ def get_graph_nodes(
workspace: str, graph: str, offset: int = 0, limit: int = 30
) -> Any:
"""Retrieve the nodes of a graph."""
return db.graph_nodes(workspace, graph, offset, limit)
return Workspace(workspace).graph(graph).nodes(offset, limit)


@bp.route(
Expand All @@ -197,7 +137,7 @@ def get_graph_nodes(
@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."""
return db.graph_node(workspace, graph, table, node)
return Workspace(workspace).graph(graph).node_attributes(table, node)


@bp.route(
Expand All @@ -220,23 +160,24 @@ def get_node_edges(
if direction not in allowed:
raise BadQueryArgument("direction", direction, allowed)

return db.node_edges(workspace, graph, table, node, offset, limit, direction)
return (
Workspace(workspace)
.graph(graph)
.node_edges(table, node, direction, offset, limit)
)


@bp.route("/workspaces/<workspace>", methods=["POST"])
@require_login
@swag_from("swagger/create_workspace.yaml")
def create_workspace(workspace: str) -> Any:
"""Create a new workspace."""

# The `require_login()` decorator ensures that a user is logged in by this
# point.
# The `require_login()` decorator ensures that a user is logged in
user = current_user()
assert user is not None

# Perform the actual backend update to create a new workspace owned by the
# logged in user.
return db.create_workspace(workspace, user)
Workspace.create(workspace, user)
return workspace


@bp.route("/workspaces/<workspace>/aql", methods=["POST"])
Expand All @@ -248,7 +189,7 @@ def aql(workspace: str) -> Any:
if not query:
raise MalformedRequestBody(query)

result = db.aql_query(workspace, query)
result = Workspace(workspace).run_query(query)
return util.stream(result)


Expand All @@ -257,7 +198,7 @@ def aql(workspace: str) -> Any:
@swag_from("swagger/delete_workspace.yaml")
def delete_workspace(workspace: str) -> Any:
"""Delete a workspace."""
db.delete_workspace(workspace)
Workspace(workspace).delete()
return workspace


Expand All @@ -267,7 +208,7 @@ def delete_workspace(workspace: str) -> Any:
@swag_from("swagger/rename_workspace.yaml")
def rename_workspace(workspace: str, name: str) -> Any:
"""Delete a workspace."""
db.rename_workspace(workspace, name)
Workspace(workspace).rename(name)
return name


Expand All @@ -277,36 +218,14 @@ def rename_workspace(workspace: str, name: str) -> Any:
@swag_from("swagger/create_graph.yaml")
def create_graph(workspace: str, graph: str, edge_table: Optional[str] = None) -> Any:
"""Create a graph."""

if not edge_table:
raise RequiredParamsMissing(["edge_table"])

loaded_workspace = db.get_workspace_db(workspace)
loaded_workspace = Workspace(workspace)
if loaded_workspace.has_graph(graph):
raise AlreadyExists("Graph", graph)

# Get reference tables with respective referenced keys,
# tables in the _from column and tables in the _to column
edge_table_properties = util.get_edge_table_properties(workspace, edge_table)
referenced_tables = edge_table_properties["table_keys"]
from_tables = edge_table_properties["from_tables"]
to_tables = edge_table_properties["to_tables"]

errors: List[ValidationFailure] = []
for table, keys in referenced_tables.items():
if not loaded_workspace.has_collection(table):
errors.append(UndefinedTable(table=table))
else:
table_keys = set(loaded_workspace.collection(table).keys())
undefined = keys - table_keys

if undefined:
errors.append(UndefinedKeys(table=table, keys=list(undefined)))

if errors:
raise ValidationFailed(errors)

db.create_graph(workspace, graph, edge_table, from_tables, to_tables)
Workspace(workspace).create_graph(graph, edge_table)
return graph


Expand All @@ -315,7 +234,7 @@ def create_graph(workspace: str, graph: str, edge_table: Optional[str] = None) -
@swag_from("swagger/delete_graph.yaml")
def delete_graph(workspace: str, graph: str) -> Any:
"""Delete a graph."""
db.delete_graph(workspace, graph)
Workspace(workspace).delete_graph(graph)
return graph


Expand All @@ -324,5 +243,5 @@ def delete_graph(workspace: str, graph: str) -> Any:
@swag_from("swagger/delete_table.yaml")
def delete_table(workspace: str, table: str) -> Any:
"""Delete a table."""
db.delete_table(workspace, table)
Workspace(workspace).delete_table(table)
return table
23 changes: 8 additions & 15 deletions multinet/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
"""Authorization types."""
from dataclasses import asdict
import json
from flasgger import swag_from
from flask import make_response, session
from flask.blueprints import Blueprint
import json
from werkzeug.wrappers import Response as ResponseWrapper
from webargs import fields
from webargs.flaskparser import use_kwargs

from multinet.user import (
MULTINET_COOKIE,
user_from_cookie,
filtered_user,
delete_user_cookie,
search_user,
)

from multinet.db.models.user import MULTINET_COOKIE, User
from multinet.util import stream
from multinet.auth.util import require_login

Expand All @@ -33,12 +25,12 @@ def user_info() -> ResponseWrapper:
if cookie is None:
return logged_out

user = user_from_cookie(cookie)
user = User.from_session(cookie)
if user is None:
session.pop(MULTINET_COOKIE, None)
return logged_out

return make_response(asdict(filtered_user(user)))
return make_response(user.asdict())


@bp.route("/logout", methods=["GET"])
Expand All @@ -50,9 +42,10 @@ def logout() -> ResponseWrapper:
cookie = session.pop(MULTINET_COOKIE, None)
if cookie is not None:
# Load the user model and invalidate its session.
user = user_from_cookie(cookie)

user = User.from_session(cookie)
if user is not None:
delete_user_cookie(user)
user.delete_session()

return make_response("", 200)

Expand All @@ -63,4 +56,4 @@ def logout() -> ResponseWrapper:
@swag_from("swagger/user/search.yaml")
def search(query: str) -> ResponseWrapper:
"""Search for users given a partial string."""
return stream(search_user(query))
return stream(User.search(query))

0 comments on commit 8e4962e

Please sign in to comment.