Skip to content

Commit

Permalink
Allow sharing of investigations (#3865)
Browse files Browse the repository at this point in the history
* Revert "Disables the user lookup endpoint"

This reverts commit d827e03.

* Revert "Remove user lookup from investigation sharing screen"

This reverts commit f9c0819.

* Require full email address to share investigation with individual users

* Increase minimum parameter length for suggest endpoint

As suggested by @stchris
  • Loading branch information
tillprochaska authored Sep 18, 2024
1 parent 88c3216 commit 2c03781
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 13 deletions.
103 changes: 103 additions & 0 deletions aleph/tests/test_permissions_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from aleph.tests.util import TestCase
from aleph.logic.roles import create_group


class PermissionsApiTestCase(TestCase):
def setUp(self):
super().setUp()
self.role, self.headers = self.login(
foreign_id="john",
name="John Doe",
email="[email protected]",
)
self.col = self.create_collection(creator=self.role)

def test_update(self):
jane = self.create_user(
foreign_id="jane",
name="Jane Doe",
email="[email protected]",
)

url = f"/api/2/collections/{self.col.id}/permissions"
res = self.client.get(url, headers=self.headers)
assert len(res.json["results"]) == 1
assert res.json["results"][0]["role"]["id"] == str(self.role.id)

# Granting a new user access without providing their full email address is ignored
data = [
{"role_id": str(self.role.id), "read": True, "write": True},
{"role_id": str(jane.id), "read": True, "write": False},
]
res = self.client.put(url, headers=self.headers, json=data)
assert res.status_code == 200
assert len(res.json["results"]) == 1
assert res.json["results"][0]["role"]["id"] == str(self.role.id)

# Granting a new user accces with an incorrect email address is ignored
data = [
{
"role_id": str(self.role.id),
"read": True,
"write": True,
},
{
"role_id": str(jane.id),
"email": "[email protected]",
"read": True,
"write": False,
},
]
res = self.client.put(url, headers=self.headers, json=data)
assert res.status_code == 200
assert len(res.json["results"]) == 1
assert res.json["results"][0]["role"]["id"] == str(self.role.id)

# Granting a new user access updates permissions if full email address is provided
data = [
{
"role_id": str(self.role.id),
"read": True,
"write": False,
},
{
"role_id": str(jane.id),
"email": "[email protected]",
"read": True,
"write": False,
},
]
res = self.client.put(url, headers=self.headers, json=data)
assert res.status_code == 200
assert len(res.json["results"]) == 2
assert res.json["results"][0]["role"]["id"] == str(self.role.id)
assert res.json["results"][1]["role"]["id"] == str(jane.id)
assert res.json["results"][1]["read"] is True
assert res.json["results"][1]["write"] is False

def test_update_groups(self):
group = create_group("group")

url = f"/api/2/collections/{self.col.id}/permissions"
res = self.client.get(url, headers=self.headers)
assert len(res.json["results"]) == 1
assert res.json["results"][0]["role"]["id"] == str(self.role.id)

# Updated permissions for a group the user is not a member of are ignored
data = [
{"role_id": str(self.role.id), "read": True, "write": True},
{"role_id": str(group.id), "read": True, "write": False},
]
res = self.client.put(url, headers=self.headers, json=data)
assert res.status_code == 200
assert len(res.json["results"]) == 1
assert res.json["results"][0]["role"]["id"] == str(self.role.id)

self.role.add_role(group)
res = self.client.put(url, headers=self.headers, json=data)
assert res.status_code == 200
assert len(res.json["results"]) == 2
assert res.json["results"][0]["role"]["id"] == str(self.role.id)
assert res.json["results"][1]["role"]["id"] == str(group.id)
assert res.json["results"][1]["read"] is True
assert res.json["results"][1]["write"] is False
53 changes: 49 additions & 4 deletions aleph/tests/test_roles_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,55 @@ def setUp(self):
def test_suggest(self):
res = self.client.get("/api/2/roles/_suggest")
assert res.status_code == 403, res
_, headers = self.login(is_admin=True)
res = self.client.get("/api/2/roles/_suggest?prefix=user", headers=headers)
assert res.status_code == 200, res
assert res.json["total"] == 0, res.json

_, headers = self.login(foreign_id="jane", email="[email protected]")
john, _ = self.login(foreign_id="john", email="[email protected]")

res = self.client.get(
"/api/2/roles/_suggest",
headers=headers,
)
assert res.status_code == 200
assert res.json["total"] == 0
assert res.json["results"] == []

res = self.client.get(
"/api/2/roles/_suggest",
query_string={"prefix": "john"},
headers=headers,
)
assert res.status_code == 200
assert res.json["total"] == 0
assert res.json["results"] == []

res = self.client.get(
"/api/2/roles/_suggest",
query_string={"prefix": "[email protected]"},
headers=headers,
)
assert res.status_code == 200
assert res.json["total"] == 1
assert len(res.json["results"]) == 1
assert res.json["results"][0]["id"] == str(john.id)

res = self.client.get(
"/api/2/roles/_suggest",
query_string={"prefix": "[email protected]"},
headers=headers,
)
assert res.status_code == 200
assert res.json["total"] == 1
assert len(res.json["results"]) == 1
assert res.json["results"][0]["id"] == str(john.id)

res = self.client.get(
"/api/2/roles/_suggest",
query_string={"prefix": "[email protected]", "exclude:id": john.id},
headers=headers,
)
assert res.status_code == 200
assert res.json["total"] == 0
assert len(res.json["results"]) == 0

def test_view(self):
res = self.client.get("/api/2/roles/%s" % self.rolex)
Expand Down
2 changes: 2 additions & 0 deletions aleph/validation/schema/permission.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ PermissionUpdate:
type: boolean
role_id:
type: string
email:
type: string
role:
$ref: "#/components/schemas/Role"

Expand Down
14 changes: 14 additions & 0 deletions aleph/views/permissions_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,27 @@ def update(collection_id):
- Collection
"""
collection = get_db_collection(collection_id, request.authz.WRITE)
current = Permission.all().where(Permission.collection_id == collection.id)
current = [permission.role_id for permission in current]

for permission in parse_request("PermissionUpdateList"):
role_obj = ensure_dict(permission.get("role"))
role_id = permission.get("role_id", role_obj.get("id"))
role = Role.by_id(role_id)

if not check_visible(role, request.authz):
continue

if (
role.type == Role.USER
and role.id not in current
and (not permission.get("email") or role.email != permission.get("email"))
):
continue

if role.is_public:
permission["write"] = False

if collection.casefile and role.is_public:
permission["read"] = False

Expand All @@ -135,6 +148,7 @@ def update(collection_id):
permission["write"],
editor_id=request.authz.id,
)

collection.updated_at = datetime.utcnow()
update_collection(collection)
db.session.commit()
Expand Down
27 changes: 22 additions & 5 deletions aleph/views/roles_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logging
from banal import ensure_list
from flask_babel import gettext
from flask import Blueprint, request
from itsdangerous import BadSignature
from werkzeug.exceptions import BadRequest
from sqlalchemy import func

from aleph.core import db
from aleph.authz import Authz
from aleph.search import QueryParser, DatabaseQueryResult
from aleph.model import Role
from aleph.logic.roles import challenge_role, update_role, create_user, get_deep_role
from aleph.util import is_auto_admin
Expand Down Expand Up @@ -51,12 +54,26 @@ def suggest():
- Role
"""
require(request.authz.logged_in)
return jsonify(
{
"results": [],
"total": 0,
}
parser = QueryParser(request.args, request.authz, limit=10)
if parser.prefix is None or len(parser.prefix) < 6:
# Do not return 400 because it's a routine event.
return jsonify(
{
"status": "error",
"message": gettext("prefix filter is too short"),
"results": [],
"total": 0,
}
)
# this only returns users, not groups
exclude = ensure_list(parser.excludes.get("id"))
query = (
Role.all_users()
.where(Role.id.not_in(exclude))
.where(func.lower(Role.email) == parser.prefix.lower())
)
result = DatabaseQueryResult(request, query, parser=parser)
return RoleSerializer.jsonify_result(result)


@blueprint.route("/api/2/roles/code", methods=["POST"])
Expand Down
8 changes: 7 additions & 1 deletion ui/src/components/common/Role.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ const messages = defineMessages({
id: 'role.select.user',
defaultMessage: 'Choose a user',
},
placeholder: {
id: 'role.select.placeholder',
defaultMessage: 'Enter email address…',
},
});

class RoleLabel extends PureComponent {
Expand Down Expand Up @@ -94,13 +98,14 @@ class Select extends Component {
const { exclude = [] } = this.props;
const roles = await this.props.suggestRoles(query, exclude);
this.setState({
query,
suggested: roles.results,
});
}

onSelectRole(role, event) {
event.stopPropagation();
this.props.onSelect(role);
this.props.onSelect(role, this.state.query);
}

renderRole = (role, { handleClick, modifiers }) => (
Expand Down Expand Up @@ -130,6 +135,7 @@ class Select extends Component {
}}
inputProps={{
fill: true,
placeholder: intl.formatMessage(messages.placeholder),
}}
activeItem={role}
filterable={!isFixed}
Expand Down
18 changes: 15 additions & 3 deletions ui/src/dialogs/CollectionAccessDialog/CollectionAccessDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { Component, PureComponent } from 'react';
import { Button, Classes, Checkbox, Intent } from '@blueprintjs/core';
import { Button, Classes, Callout, Checkbox, Intent } from '@blueprintjs/core';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { compose } from 'redux';
import { connect } from 'react-redux';
Expand Down Expand Up @@ -88,9 +88,9 @@ class CollectionAccessDialog extends Component {
}
}

onAddRole(role) {
onAddRole(role, query) {
const { permissions } = this.state;
permissions.push({ role, read: true, write: false });
permissions.push({ role, email: query, read: true, write: false });
this.setState({ permissions });
}

Expand Down Expand Up @@ -149,6 +149,7 @@ class CollectionAccessDialog extends Component {
const systemRoles = this.filterPermissions('system');
const groupRoles = this.filterPermissions('group');
const userRoles = this.filterPermissions('user');
const exclude = userRoles.map((perm) => perm.role.id);

return (
<FormDialog
Expand Down Expand Up @@ -222,6 +223,17 @@ class CollectionAccessDialog extends Component {
onToggle={this.onToggle}
/>
))}
<tr key="add">
<td colSpan="3">
<Role.Select onSelect={this.onAddRole} exclude={exclude} />
<Callout intent={Intent.WARNING}>
<FormattedMessage
id="collection.edit.permissions_warning"
defaultMessage="Note: User must already have an Aleph account in order to receive access."
/>
</Callout>
</td>
</tr>
</tbody>
</table>
</div>
Expand Down

0 comments on commit 2c03781

Please sign in to comment.