Moderate severityNVD Advisory· Published Oct 11, 2023· Updated Sep 17, 2024
vantage6 Improper Access Control vulnerability
CVE-2023-41882
Description
vantage6 is privacy preserving federated learning infrastructure. The endpoint /api/collaboration/{id}/task is used to collect all tasks from a certain collaboration. To get such tasks, a user should have permission to view the collaboration and to view the tasks in it. However, prior to version 4.0.0, it is only checked if the user has permission to view the collaboration. Version 4.0.0 contains a patch. There are no known workarounds.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
vantage6PyPI | < 4.0.0 | 4.0.0 |
Affected products
1Patches
186564e103cbaMerge pull request #711 from vantage6/feature/collaboration-scope
15 files changed · +1786 −317
vantage6-server/tests_server/test_resources.py+935 −65 modified@@ -220,6 +220,8 @@ def paginated_list( The url of the list endpoint headers: dict The headers to use for the request + kwargs: dict + Additional arguments to pass to the request Returns ------- @@ -229,13 +231,16 @@ def paginated_list( result = self.app.get(url, headers=headers) links = result.json.get('links') page = 1 - json_data = result.json['data'] + json_data = result.json.get('data') + if json_data is None: + json_data = [] while links and links.get('next'): page += 1 new_response = self.app.get( links.get('next'), headers=headers ) - json_data += new_response.json.get('data') + json_data += new_response.json.get('data') \ + if new_response.json.get('data') else [] links = new_response.json.get('links') return result, json_data @@ -411,17 +416,13 @@ def test_run_without_id(self): result1 = self.app.get("/api/run", headers=headers) self.assertEqual(result1.status_code, 200) - result2 = self.app.get("/api/run?state=open&&node_id=1", + result2 = self.app.get("/api/run?state=open", headers=headers) self.assertEqual(result2.status_code, 200) result3 = self.app.get("/api/run?task_id=1", headers=headers) self.assertEqual(result3.status_code, 200) - result4 = self.app.get("/api/run?task_id=1&&node_id=1", - headers=headers) - self.assertEqual(result4.status_code, 200) - def test_stats(self): headers = self.login("root") result = self.app.get("/api/run", headers=headers) @@ -620,6 +621,100 @@ def test_view_roles(self): for field in expected_fields: self.assertIn(field, body[0]) + def test_view_role_permissions(self): + org = Organization() + org.save() + other_org = Organization() + other_org.save() + col = Collaboration(organizations=[org, other_org]) + col.save() + org_outside_collab = Organization() + org_outside_collab.save() + + # non-existing role + headers = self.login('root') + result = self.app.get("/api/role/9999", headers=headers) + self.assertEqual(result.status_code, HTTPStatus.NOT_FOUND) + + # root user can view all roles + result, json_data = self.paginated_list('/api/role', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + self.assertEqual(len(json_data), len(Role.get())) + + role = Role(organization=org) + role.save() + + # without permissions should allow you to view your own roles, which + # in this case is an empty list + headers = self.create_user_and_login() + result, json_data = self.paginated_list('/api/role', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + self.assertEqual(len(json_data), 0) + + # view roles of your organization + rule = Rule.get_by_("role", Scope.ORGANIZATION, Operation.VIEW) + headers = self.create_user_and_login(org, rules=[rule]) + result, json_data = self.paginated_list('/api/role', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # +3 for the root, container and node roles (other default roles are + # not generated for unit tests) + self.assertEqual(len(json_data), len(org.roles) + 3) + + # view a single role of your organization + result = self.app.get(f'/api/role/{role.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # check that user of other organization cannot view roles with + # organization scope + headers = self.create_user_and_login(other_org, rules=[rule]) + result = self.app.get( + '/api/role', headers=headers, + query_string={'organization_id': org.id} + ) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # user can view their own roles. This should always be possible + user = self.create_user(rules=[]) + headers = self.login(user.username) + result = self.app.get('/api/role', headers=headers, query_string={ + 'user_id': user.id + }) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # collaboration permission - in same collaboration with id + rule = Rule.get_by_("role", Scope.COLLABORATION, Operation.VIEW) + headers = self.create_user_and_login(other_org, rules=[rule]) + result = self.app.get(f"/api/role/{role.id}", headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # collaboration permission - in same collaboration without id + result, json_data = self.paginated_list('/api/role', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + # +3 for the root, container and node roles (other default roles are + # not generated for unit tests) + self.assertEqual(len(json_data), len([ + role_ for org in col.organizations for role_ in org.roles + ]) + 3) + + # collaboration permission - in different collaboration with id + headers = self.create_user_and_login(org_outside_collab, rules=[rule]) + result = self.app.get(f"/api/role/{role.id}", headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # collaboration permission - in different collaboration without id + result = self.app.get('/api/role', headers=headers, + query_string={'collaboration_id': col.id}) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + org.delete() + other_org.delete() + org_outside_collab.delete() + col.delete() + role.delete() + user.delete() + def test_create_role_as_root(self): headers = self.login("root") @@ -685,12 +780,11 @@ def test_create_role_permissions(self): body = { "name": "some-role-name", - "description": "Testing if we can create a rol for another org", + "description": "Testing if we can create a role for another org", "rules": [rule.id for rule in all_rules], } result = self.app.post("/api/role", headers=headers, json=body) self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) - # check that user with a missing rule cannot create a role with that # missing rule headers = self.create_user_and_login(rules=(all_rules[:-2])) @@ -719,6 +813,28 @@ def test_create_role_permissions(self): result = self.app.post("/api/role", headers=headers, json=body) self.assertEqual(result.status_code, HTTPStatus.NOT_FOUND) + # check creating role inside the collaboration + org1 = Organization() + org1.save() + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org1, org2]) + col.save() + rule = Rule.get_by_("role", scope=Scope.COLLABORATION, + operation=Operation.CREATE) + headers = self.create_user_and_login(organization=org1, rules=[rule]) + body["rules"] = [rule.id] + body["organization_id"] = org2.id + result = self.app.post("/api/role", headers=headers, json=body) + self.assertEqual(result.status_code, HTTPStatus.CREATED) + + # check creating role outside the collaboration fails + org3 = Organization() + org3.save() + body["organization_id"] = org3.id + result = self.app.post("/api/role", headers=headers, json=body) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + def test_edit_role(self): headers = self.login('root') @@ -773,6 +889,29 @@ def test_edit_role(self): }) self.assertEqual(result.status_code, HTTPStatus.OK) + # test editing role inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule = Rule.get_by_("role", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.patch(f"/api/role/{role.id}", headers=headers, json={ + "name": "new-role-name", + }) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # check editing role outside the collaboration fails + org3 = Organization() + org3.save() + role = Role(name="some-role-name", organization=org3) + role.save() + result = self.app.patch(f"/api/role/{role.id}", headers=headers, json={ + "name": "this-will-not-be-updated" + }) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + def test_remove_role(self): org = Organization() @@ -805,6 +944,32 @@ def test_remove_role(self): result = self.app.delete(f'/api/role/{role.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # check removing role outside the collaboration fails + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + role = Role(organization=org) # because we removed it... + role.save() + + org3 = Organization() + org3.save() + rule = Rule.get_by_("role", Scope.COLLABORATION, Operation.DELETE) + headers = self.create_user_and_login(organization=org3, rules=[rule]) + result = self.app.delete(f"/api/role/{role.id}", headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # test removing role inside the collaboration + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.delete(f"/api/role/{role.id}", headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # cleanup + org3.delete() + org2.delete() + org.delete() + col.delete() + def test_rules_from_role(self): headers = self.login('root') role = Role.get()[0] @@ -881,7 +1046,9 @@ def test_remove_single_rule_from_role(self): def test_view_permission_rules(self): rule = Rule.get_by_("role", Scope.ORGANIZATION, Operation.VIEW) - role = Role(name="some-role", organization=Organization()) + org = Organization() + org.save() + role = Role(name="some-role", organization=org) role.save() # user does not belong to organization @@ -901,11 +1068,14 @@ def test_view_permission_rules(self): result = self.app.get(f'/api/role/{role.id}/rule', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # cleanup role.delete() + org.delete() def test_add_rule_to_role_permission(self): - - role = Role(name="new-role", organization=Organization()) + org = Organization() + org.save() + role = Role(name="new-role", organization=org) role.save() rule = Rule.get_by_("role", Scope.ORGANIZATION, Operation.EDIT) @@ -935,14 +1105,42 @@ def test_add_rule_to_role_permission(self): headers=headers) self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + # test inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule = Rule.get_by_("role", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.post(f'/api/role/{role.id}/rule/{rule.id}', + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.CREATED) + + # check outside the collaboration fails + org3 = Organization() + org3.save() + role2 = Role(name="some-role-name", organization=org3) + role2.save() + result = self.app.post(f'/api/role/{role2.id}/rule/{rule.id}', + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup role.delete() + role2.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_remove_rule_from_role_permissions(self): - - role = Role(name="new-role", organization=Organization()) + org = Organization() + org.save() + role = Role(name="new-role", organization=org) role.save() rule = Rule.get_by_("role", Scope.ORGANIZATION, - Operation.DELETE) + Operation.EDIT) # try removing without any permissions headers = self.create_user_and_login() @@ -958,8 +1156,7 @@ def test_remove_rule_from_role_permissions(self): self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) # try removing rule which is not in the role - headers = self.create_user_and_login(organization=role.organization, - rules=[rule]) + headers = self.create_user_and_login(organization=org, rules=[rule]) result = self.app.delete(f'/api/role/{role.id}/rule/{rule.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.NOT_FOUND) @@ -979,13 +1176,43 @@ def test_remove_rule_from_role_permissions(self): # power users can edit other organization rules power_rule = Rule.get_by_("role", Scope.GLOBAL, - Operation.DELETE) + Operation.EDIT) headers = self.create_user_and_login(rules=[power_rule, rule]) result = self.app.delete(f'/api/role/{role.id}/rule/{rule.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # test inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule = Rule.get_by_("role", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + role.rules.append(rule) + role.save() + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.delete(f'/api/role/{role.id}/rule/{rule.id}', + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # check outside the collaboration fails + org3 = Organization() + org3.save() + role2 = Role(name="some-role-name", organization=org3) + role2.rules.append(rule) + role2.save() + result = self.app.delete(f'/api/role/{role2.id}/rule/{rule.id}', + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup role.delete() + role2.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_view_permission_user(self): @@ -1007,7 +1234,8 @@ def test_view_permission_user(self): # view users of your organization rule = Rule.get_by_("user", Scope.ORGANIZATION, Operation.VIEW) - org = Organization.get(1) + org = Organization() + org.save() headers = self.create_user_and_login(org, rules=[rule]) result, json_data = self.paginated_list('/api/user', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) @@ -1024,6 +1252,48 @@ def test_view_permission_user(self): result = self.app.get(f'/api/user/{user.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # collaboration permission - view single user + org2 = Organization() + org2.save() + org3 = Organization() + org3.save() + col = Collaboration(organizations=[org2, org3]) + col.save() + user = self.create_user(organization=org2, rules=[]) + rule = Rule.get_by_("user", scope=Scope.COLLABORATION, + operation=Operation.VIEW) + headers = self.create_user_and_login(organization=org3, rules=[rule]) + result = self.app.get(f'/api/user/{user.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # collaboration permission - view list of users + result = self.app.get('/api/user', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + # expecting 2 users: 1 in org2 and the 1 in org3 which is logged in now + self.assertEqual(len(result.json['data']), 2) + + # collaboration permission - viewing outside collaboration should fail + org_outside_col = Organization() + org_outside_col.save() + headers = self.create_user_and_login(organization=org_outside_col, + rules=[rule]) + result = self.app.get(f'/api/user/{user.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # collaboration permission - viewing other collaborations should fail + result = self.app.get('/api/user', headers=headers, query_string={ + 'collaboration_id': col.id + }) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + org.delete() + org2.delete() + org3.delete() + org_outside_col.delete() + col.delete() + user.delete() + def test_bounce_existing_username_and_email(self): headers = self.create_user_and_login() User(username="something", email="mail@me.org").save() @@ -1073,7 +1343,31 @@ def test_new_permission_user(self): result = self.app.post('/api/user', headers=headers, json=userdata) self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) - # you can only assign roles in which you have all rules + # test inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule = Rule.get_by_("user", scope=Scope.COLLABORATION, + operation=Operation.CREATE) + headers = self.create_user_and_login(organization=org, rules=[rule]) + userdata['username'] = 'smarty4' + userdata['email'] = 'mail4@me.org' + userdata['organization_id'] = org2.id + userdata['rules'] = [rule.id] + result = self.app.post('/api/user', headers=headers, json=userdata) + self.assertEqual(result.status_code, HTTPStatus.CREATED) + + # check outside the collaboration fails + org3 = Organization() + org3.save() + userdata['username'] = 'smarty5' + userdata['email'] = 'mail5@me.org' + userdata['organization_id'] = org3.id + result = self.app.post('/api/user', headers=headers, json=userdata) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # you can only create users for in which you have all rules rule_view_roles = Rule.get_by_( "role", Scope.ORGANIZATION, Operation.VIEW) headers = self.create_user_and_login( @@ -1092,6 +1386,13 @@ def test_new_permission_user(self): query_string={'user_id': result.json['id']}) self.assertEqual(len(result.json['data']), 1) + # cleanup + org.delete() + org2.delete() + org3.delete() + col.delete() + role.delete() + def test_patch_user_permissions(self): org = Organization() @@ -1174,6 +1475,29 @@ def test_patch_user_permissions(self): self.assertEqual("again", user.firstname) self.assertEqual("and again", user.lastname) + # test editing user inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule2 = Rule.get_by_("user", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org2, rules=[rule2]) + result = self.app.patch(f'/api/user/{user.id}', headers=headers, json={ + 'firstname': 'something', + 'lastname': 'everything', + }) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # check editing outside the collaboration fails + org3 = Organization() + org3.save() + headers = self.create_user_and_login(organization=org3, rules=[rule2]) + result = self.app.patch(f'/api/user/{user.id}', headers=headers, json={ + 'firstname': 'will-not-work', + }) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + # test that you cannot assign rules that you not own not_owning_rule = Rule.get_by_("user", Scope.OWN, Operation.DELETE) @@ -1270,12 +1594,16 @@ def test_patch_user_permissions(self): user.delete() role.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_delete_user_permissions(self): - + org = Organization() user = User(firstname="Firstname", lastname="Lastname", username="Username", password="Password", email="a@b.c", - organization=Organization()) + organization=org) user.save() self.credentials[user.username] = {'username': user.username, 'password': "Password"} @@ -1338,6 +1666,34 @@ def test_delete_user_permissions(self): self.assertEqual(result.status_code, HTTPStatus.OK) # user is deleted by endpoint! user.delete() + # check delete outside the collaboration fails + user = User(firstname="Firstname", lastname="Lastname", + username="Username", password="Password", email="a@b.c", + organization=org) + user.save() + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + org3 = Organization() + org3.save() + rule = Rule.get_by_("user", Scope.COLLABORATION, + Operation.DELETE) + headers = self.create_user_and_login(organization=org3, rules=[rule]) + result = self.app.delete(f'/api/user/{user.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # test delete inside the collaboration + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.delete(f'/api/user/{user.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # cleanup + org.delete() + org2.delete() + org3.delete() + col.delete() + def test_view_organization_as_user_permissions(self): # view without any permissions @@ -1362,18 +1718,40 @@ def test_view_organization_as_user_permissions(self): self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) # Missing organization with global view - rule = Rule.get_by_("organization", Scope.GLOBAL, - Operation.VIEW) + rule = Rule.get_by_("organization", Scope.GLOBAL, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) result = self.app.get('/api/organization/9999', headers=headers) self.assertEqual(result.status_code, HTTPStatus.NOT_FOUND) # test global view - result = self.app.get(f'/api/organization/{org.id}', - headers=headers) + result = self.app.get(f'/api/organization/{org.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # test view inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule = Rule.get_by_("organization", scope=Scope.COLLABORATION, + operation=Operation.VIEW) + headers = self.create_user_and_login(organization=org2, rules=[rule]) + result = self.app.get(f'/api/organization/{org.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # check view outside the collaboration fails + org3 = Organization() + org3.save() + headers = self.create_user_and_login(organization=org3, rules=[rule]) + result = self.app.get(f'/api/organization/{org.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + org.delete() + org2.delete() + org3.delete() + col.delete() + def test_view_organization_as_node_permission(self): node, api_key = self.create_node() headers = self.login_node(api_key) @@ -1464,10 +1842,9 @@ def test_patch_organization_permissions(self): rule = Rule.get_by_("organization", Scope.ORGANIZATION, Operation.EDIT) headers = self.create_user_and_login(organization=org, rules=[rule]) - results = self.app.patch(f'/api/organization/{org.id}', - headers=headers, json={ - "name": "third-name" - }) + results = self.app.patch( + f'/api/organization/{org.id}', headers=headers, + json={"name": "third-name"}) self.assertEqual(results.status_code, HTTPStatus.OK) self.assertEqual(results.json['name'], "third-name") @@ -1481,6 +1858,28 @@ def test_patch_organization_permissions(self): }) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + # test editing organization inside the collaboration + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) + col.save() + rule2 = Rule.get_by_("organization", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org2, rules=[rule2]) + results = self.app.patch( + f'/api/organization/{org.id}', headers=headers, + json={"name": "fourth-name"}) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # check editing outside the collaboration fails + org3 = Organization() + org3.save() + headers = self.create_user_and_login(organization=org3, rules=[rule2]) + results = self.app.patch( + f'/api/organization/{org.id}', headers=headers, + json={"name": "not-going-to-happen"}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + def test_organization_view_nodes(self): # create organization, collaboration and node @@ -1650,6 +2049,35 @@ def test_edit_collaboration_permissions(self): }) self.assertEqual(results.status_code, HTTPStatus.OK) self.assertEqual(results.json["name"], "this-is-gonna-fly") + col.delete() + + # test editing collaboration from within the collaboration + org = Organization() + org.save() + col = Collaboration(organizations=[org]) + col.save() + rule = Rule.get_by_("collaboration", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.patch( + f'/api/collaboration/{col.id}', headers=headers, + json={"name": "some-name"}) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # check editing collaboration outside the collaboration fails without + # root access + org2 = Organization() + org2.save() + headers = self.create_user_and_login(organization=org2, rules=[rule]) + results = self.app.patch( + f'/api/collaboration/{col.id}', headers=headers, + json={"name": "not-going-to-happen"}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + org.delete() + org2.delete() + col.delete() def test_delete_collaboration_permissions(self): @@ -1674,6 +2102,33 @@ def test_delete_collaboration_permissions(self): headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) + # check deleting with collaboration permission outside the + # collaboration fails + org = Organization() + org.save() + col = Collaboration(organizations=[org]) + col.save() + org_not_member = Organization() + org_not_member.save() + rule = Rule.get_by_("collaboration", Scope.COLLABORATION, + Operation.DELETE) + headers = self.create_user_and_login(organization=org_not_member, + rules=[rule]) + result = self.app.delete(f"/api/collaboration/{col.id}", + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # check deleting with collaboration permission inside the collaboration + # succeeds + headers = self.create_user_and_login(organization=org, rules=[rule]) + result = self.app.delete(f"/api/collaboration/{col.id}", + headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # cleanup + org.delete() + org_not_member.delete() + def test_view_collaboration_organization_permissions_as_user(self): headers = self.create_user_and_login() @@ -1778,11 +2233,40 @@ def test_edit_collaboration_organization_permissions(self): self.assertEqual(results.status_code, HTTPStatus.OK) self.assertEqual(len(results.json), 2) - def test_delete_collaboration_organization_pesmissions(self): + # test adding new organization to collaboration from within the + # collaboration + org3 = Organization() + org3.save() + rule = Rule.get_by_("collaboration", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.post(f"/api/collaboration/{col.id}/organization", + headers=headers, json={'id': org3.id}) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # adding new organization to collaboration from outside the + # collaboration should fail with collaboration permission + org4 = Organization() + org4.save() + headers = self.create_user_and_login(organization=org4, rules=[rule]) + results = self.app.post(f"/api/collaboration/{col.id}/organization", + headers=headers, json={'id': org4.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + org.delete() + org2.delete() + org3.delete() + org4.delete() + col.delete() + + def test_delete_collaboration_organization_permissions(self): org = Organization() org.save() - col = Collaboration(organizations=[org]) + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) col.save() # try to do it without permission @@ -1791,13 +2275,41 @@ def test_delete_collaboration_organization_pesmissions(self): headers=headers, json={'id': org.id}) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) - # delete it! - rule = Rule.get_by_("collaboration", Scope.GLOBAL, Operation.DELETE) + # delete first organization + rule = Rule.get_by_("collaboration", Scope.GLOBAL, Operation.EDIT) headers = self.create_user_and_login(rules=[rule]) results = self.app.delete(f"/api/collaboration/{col.id}/organization", headers=headers, json={'id': org.id}) self.assertEqual(results.status_code, HTTPStatus.OK) - self.assertEqual(results.json, []) + self.assertEqual(len(results.json), 1) # one organization left + + # add back first organization + col.organizations.append(org) + col.save() + + # removing organization from collaboration from outside the + # collaboration should fail with collaboration permission + org3 = Organization() + org3.save() + rule = Rule.get_by_("collaboration", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org3, rules=[rule]) + results = self.app.delete(f"/api/collaboration/{col.id}/organization", + headers=headers, json={'id': org2.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test removing organization from collaboration from within the + # collaboration + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.delete(f"/api/collaboration/{col.id}/organization", + headers=headers, json={'id': org2.id}) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # cleanup + org.delete() + org2.delete() + org3.delete() + col.delete() def test_view_collaboration_node_permissions(self): @@ -1848,10 +2360,19 @@ def test_add_collaboration_node_permissions(self): org = Organization() org.save() - col = Collaboration(organizations=[org]) + org2 = Organization() + org2.save() + col = Collaboration(organizations=[org, org2]) col.save() node = Node(organization=org) node.save() + node2 = Node(organization=org2) + node2.save() + + org3 = Organization() + org3.save() + node3 = Node(organization=org3) + node3.save() # try non-existant collaboration headers = self.create_user_and_login() @@ -1883,13 +2404,41 @@ def test_add_collaboration_node_permissions(self): headers=headers, json={'id': node.id}) self.assertEqual(results.status_code, HTTPStatus.BAD_REQUEST) + # adding new node to collaboration from an organization that is not + # part of the collaboration should fail + results = self.app.post(f'/api/collaboration/{col.id}/node', + headers=headers, json={'id': node3.id}) + self.assertEqual(results.status_code, HTTPStatus.BAD_REQUEST) + + # test new node to collaboration from within the collaboration + rule = Rule.get_by_("collaboration", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.post(f'/api/collaboration/{col.id}/node', + headers=headers, json={'id': node2.id}) + self.assertEqual(results.status_code, HTTPStatus.CREATED) + + # adding new node to collaboration from outside collaboration should + # fail with collaboration-scope permission + headers = self.create_user_and_login(organization=org3, rules=[rule]) + results = self.app.post(f'/api/collaboration/{col.id}/node', + headers=headers, json={'id': node3.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + # cleanup node.delete() + node2.delete() + node3.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_delete_collaboration_node_permissions(self): org = Organization() - col = Collaboration(organizations=[org]) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) node = Node(organization=org, collaboration=col) node.save() @@ -1917,15 +2466,40 @@ def test_delete_collaboration_node_permissions(self): results = self.app.delete(f'/api/collaboration/{col.id}/node', headers=headers, json={'id': node2.id}) self.assertEqual(results.status_code, HTTPStatus.BAD_REQUEST) + node2.delete() - # delete a node! + # delete node from collaboration! results = self.app.delete(f'/api/collaboration/{col.id}/node', headers=headers, json={'id': node.id}) self.assertEqual(results.status_code, HTTPStatus.OK) + # removing node from collaboration from outside the + # collaboration should fail with collaboration permission + node2 = Node(organization=org2, collaboration=col) + node2.save() + org3 = Organization() + org3.save() + rule = Rule.get_by_("collaboration", scope=Scope.COLLABORATION, + operation=Operation.EDIT) + headers = self.create_user_and_login(organization=org3, rules=[rule]) + results = self.app.delete(f"/api/collaboration/{col.id}/node", + headers=headers, json={'id': node2.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test removing organization from collaboration from within the + # collaboration + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.delete(f"/api/collaboration/{col.id}/node", + headers=headers, json={'id': node2.id}) + self.assertEqual(results.status_code, HTTPStatus.OK) + # cleanup node.delete() node2.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_view_collaboration_task_permissions_as_user(self): @@ -1946,8 +2520,7 @@ def test_view_collaboration_task_permissions_as_user(self): self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) # view from another organization - rule = Rule.get_by_("task", Scope.ORGANIZATION, - Operation.VIEW) + rule = Rule.get_by_("task", Scope.COLLABORATION, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) results = self.app.get(f'/api/collaboration/{col.id}/task', headers=headers) @@ -1987,9 +2560,12 @@ def test_view_collaboration_task_permissions_as_node(self): def test_view_node_permissions_as_user(self): org = Organization() - col = Collaboration(organizations=[org]) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) node = Node(organization=org, collaboration=col) node.save() + node2 = Node(organization=org2, collaboration=col) + node2.save() # view non existing node headers = self.create_user_and_login() @@ -2021,16 +2597,45 @@ def test_view_node_permissions_as_user(self): headers = self.create_user_and_login(organization=org, rules=[rule1]) results = self.app.get('/api/node', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) - self.assertEqual(len(results.json['data']), len(col.nodes)) + self.assertEqual(len(results.json['data']), 1) # collab has 1 node # list global permissions headers = self.create_user_and_login(rules=[rule2]) results, json_data = self.paginated_list('/api/node', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) self.assertEqual(len(json_data), len(Node.get())) + # collaboration permission inside the collaboration + rule = Rule.get_by_("node", scope=Scope.COLLABORATION, + operation=Operation.VIEW) + headers = self.create_user_and_login(organization=org2, rules=[rule]) + results = self.app.get(f'/api/node/{node.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # list collaboration permissions - in collaboration + results = self.app.get('/api/node', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + self.assertEqual(len(results.json['data']), len(col.nodes)) + + # collaboration permission outside the collaboration should fail + org3 = Organization() + org3.save() + headers = self.create_user_and_login(organization=org3, rules=[rule]) + results = self.app.get(f'/api/node/{node.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # list collaboration permissions - other collaboration + results = self.app.get('/api/node', headers=headers, + query_string={'collaboration_id': col.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + # cleanup node.delete() + node2.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() def test_view_node_permissions_as_node(self): @@ -2089,9 +2694,10 @@ def test_create_node_permissions(self): }) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) - # test adding a node to an collaboration from an organization witch + # test adding a node to an collaboration from an organization which # does not belong to the collaboration - headers = self.create_user_and_login(organization=org2, rules=[rule]) + rule2 = Rule.get_by_("node", Scope.GLOBAL, Operation.CREATE) + headers = self.create_user_and_login(organization=org2, rules=[rule2]) results = self.app.post('/api/node', headers=headers, json={ 'collaboration_id': col.id }) @@ -2123,6 +2729,41 @@ def test_create_node_permissions(self): }) self.assertEqual(results.status_code, HTTPStatus.CREATED) + # test collaboration permissions + org3 = Organization() + org3.save() + col.organizations.append(org3) + col.save() + rule = Rule.get_by_("node", scope=Scope.COLLABORATION, + operation=Operation.CREATE) + headers = self.create_user_and_login(organization=org, rules=[rule]) + result = self.app.post('/api/node', headers=headers, json={ + 'collaboration_id': col.id, + 'organization_id': org3.id + }) + self.assertEqual(result.status_code, HTTPStatus.CREATED) + + # test collaboration permissions - outside of collaboration should fail + org4 = Organization() + org4.save() + col.organizations.append(org4) + col.save() + headers = self.create_user_and_login(organization=Organization(), + rules=[rule]) + result = self.app.post('/api/node', headers=headers, json={ + 'collaboration_id': col.id, + 'organization_id': org4.id + }) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + node.delete() + org.delete() + org2.delete() + org3.delete() + org4.delete() + col.delete() + def test_delete_node_permissions(self): org = Organization(name=str(uuid.uuid1())) @@ -2156,6 +2797,34 @@ def test_delete_node_permissions(self): results = self.app.delete(f'/api/node/{node2.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) + # collaboration permission - removing node from outside collaboration + # should fail + org3 = Organization() + node3 = Node(organization=org3, collaboration=col) + node3.save() + col.organizations.append(org3) + col.save() + org_not_in_collab = Organization() + org_not_in_collab.save() + rule = Rule.get_by_("node", scope=Scope.COLLABORATION, + operation=Operation.DELETE) + headers = self.create_user_and_login(organization=org_not_in_collab, + rules=[rule]) + results = self.app.delete(f'/api/node/{node3.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # collaboration permission - now within collaboration + headers = self.create_user_and_login(organization=org, rules=[rule]) + results = self.app.delete(f'/api/node/{node3.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # cleanup + org.delete() + org2.delete() + org3.delete() + org_not_in_collab.delete() + col.delete() + def test_patch_node_permissions_as_user(self): # test patching non-existant node headers = self.create_user_and_login() @@ -2164,7 +2833,8 @@ def test_patch_node_permissions_as_user(self): # test user without any permissions org = Organization() - col = Collaboration(organizations=[org]) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) node = Node(organization=org, collaboration=col) node.save() @@ -2230,8 +2900,28 @@ def test_patch_node_permissions_as_user(self): json={'organization_id': 9999}) self.assertEqual(results.status_code, HTTPStatus.NOT_FOUND) + # collaboration permission - inside the collaboration + rule = Rule.get_by_("node", Scope.COLLABORATION, Operation.EDIT) + headers = self.create_user_and_login(organization=org2, rules=[rule]) + results = self.app.patch(f"/api/node/{node.id}", headers=headers, + json={"name": "A"}) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # collaboration permission - outside the collaboration + org3 = Organization() + org3.save() + headers = self.create_user_and_login(organization=org3, rules=[rule]) + results = self.app.patch(f"/api/node/{node.id}", headers=headers, + json={"name": "A"}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + # cleanup node.delete() + org.delete() + org2.delete() + org3.delete() + col.delete() + col2.delete() def test_view_task_permissions_as_user(self): # non existing task @@ -2241,20 +2931,29 @@ def test_view_task_permissions_as_user(self): # test user without any permissions and id org = Organization() - col = Collaboration(organizations=[org]) - task = Task(name="unit", collaboration=col) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) + task = Task(name="unit", collaboration=col, init_org=org) task.save() results = self.app.get(f'/api/task/{task.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) - # test user with org permissions with id - rule = Rule.get_by_("task", Scope.ORGANIZATION, Operation.VIEW) + # test user with col permissions with id + rule = Rule.get_by_("task", Scope.COLLABORATION, Operation.VIEW) headers = self.create_user_and_login(org, rules=[rule]) results = self.app.get(f'/api/task/{task.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) self.assertEqual(results.json['name'], 'unit') + # collaboration permission outside the collaboration should fail + org_not_in_collab = Organization() + org_not_in_collab.save() + headers = self.create_user_and_login(organization=org_not_in_collab, + rules=[rule]) + results = self.app.get(f'/api/task/{task.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + # test user with org permissions with id from another org headers = self.create_user_and_login(rules=[rule]) results = self.app.get(f'/api/task/{task.id}', headers=headers) @@ -2265,23 +2964,111 @@ def test_view_task_permissions_as_user(self): results = self.app.get('/api/task', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) + # test that user is not allowed to view task results without id + results = self.app.get('/api/task', headers=headers, + query_string={'include': 'results'}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test that user is allowed to view task results if they have the rule + # to view results + rule_view_results = Rule.get_by_("run", Scope.GLOBAL, Operation.VIEW) + headers = self.create_user_and_login(org, + rules=[rule, rule_view_results]) + results = self.app.get('/api/task', headers=headers, + query_string={'include': 'results'}) + self.assertEqual(results.status_code, HTTPStatus.OK) + # test user with global permissions and id rule = Rule.get_by_("task", Scope.GLOBAL, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) results = self.app.get(f'/api/task/{task.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) + # test that user is not allowed to view task results with id + results = self.app.get(f'/api/task/{task.id}', headers=headers, + query_string={'include': 'results'}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test that user is allowed to view task results if they have the rule + # to view results + headers = self.create_user_and_login(org, + rules=[rule, rule_view_results]) + results = self.app.get(f'/api/task/{task.id}', headers=headers, + query_string={'include': 'results'}) + self.assertEqual(results.status_code, HTTPStatus.OK) + # test user with global permissions without id results = self.app.get('/api/task', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) + # list collaboration permissions - in collaboration + rule = Rule.get_by_("task", Scope.COLLABORATION, Operation.VIEW) + headers = self.create_user_and_login(org, rules=[rule]) + results = self.app.get('/api/task', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + self.assertEqual(len(results.json['data']), len(col.tasks)) + + # list collaboration permissions - other collaboration + headers = self.create_user_and_login(org_not_in_collab, rules=[rule]) + results = self.app.get('/api/task', headers=headers, + query_string={'collaboration_id': col.id}) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # list own organization permissions - same organization + rule = Rule.get_by_("task", Scope.ORGANIZATION, Operation.VIEW) + headers = self.create_user_and_login(org, rules=[rule]) + results = self.app.get('/api/task', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + self.assertEqual(len(results.json['data']), len(col.tasks)) + + # list own organization permissions - other organization + headers = self.create_user_and_login(org2, rules=[rule]) + results = self.app.get('/api/task', headers=headers, query_string={ + 'init_org_id': org.id + }) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # list own user's task permissions - same user without id + rule = Rule.get_by_("task", Scope.OWN, Operation.VIEW) + user = self.create_user(rules=[rule], organization=org) + headers = self.login(user.username) + task2 = Task(name="unit", collaboration=col, init_org=org, + init_user=user) + task2.save() + results = self.app.get('/api/task', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + self.assertEqual(len(results.json['data']), 1) + + # list own user's task permissions - same user with id + results = self.app.get(f'/api/task/{task2.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # list own user's task permissions - other user without id + headers = self.create_user_and_login(org, rules=[rule]) + results = self.app.get('/api/task', headers=headers, query_string={ + 'init_user_id': user.id + }) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # list own user's task permissions - other user with id + results = self.app.get(f'/api/task/{task2.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # cleanup + task.delete() + task2.delete() + user.delete() + org.delete() + org2.delete() + col.delete() + def test_view_task_permissions_as_node_and_container(self): # test node with id org = Organization() org.save() col = Collaboration(organizations=[org]) col.save() - task = Task(collaboration=col, image="some-image") + task = Task(collaboration=col, image="some-image", init_org=org) task.save() res = Run(task=task, status=TaskStatus.PENDING) res.save() @@ -2345,7 +3132,7 @@ def test_create_task_permission_as_user(self): self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) # user with organization permissions for other organization - rule = Rule.get_by_("task", Scope.ORGANIZATION, Operation.CREATE) + rule = Rule.get_by_("task", Scope.COLLABORATION, Operation.CREATE) headers = self.create_user_and_login(rules=[rule]) results = self.app.post('/api/task', headers=headers, json=task_json) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) @@ -2440,16 +3227,18 @@ def test_delete_task_permissions(self): # test with organization permissions from other organization org = Organization() - col = Collaboration(organizations=[org]) - task = Task(collaboration=col) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) + task = Task(collaboration=col, init_org=org) task.save() - rule = Rule.get_by_("task", Scope.ORGANIZATION, Operation.DELETE) + # test with user who is not member of collaboration + rule = Rule.get_by_("task", Scope.COLLABORATION, Operation.DELETE) headers = self.create_user_and_login(rules=[rule]) results = self.app.delete(f'/api/task/{task.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) - # test with organization permissions + # test with collaboration permissions headers = self.create_user_and_login(org, [rule]) results = self.app.delete(f'/api/task/{task.id}', headers=headers) self.assertEqual(results.status_code, HTTPStatus.OK) @@ -2471,6 +3260,41 @@ def test_delete_task_permissions(self): self.assertEqual(results.status_code, HTTPStatus.OK) self.assertIsNone(Task.get(run_id)) + # test permission to delete tasks of own organization - other + # organization should fail + task = Task(collaboration=col, init_org=org) + task.save() + rule = Rule.get_by_("task", Scope.ORGANIZATION, Operation.DELETE) + headers = self.create_user_and_login(rules=[rule], organization=org2) + results = self.app.delete(f'/api/task/{task.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test permission to delete tasks of own organization - should work + headers = self.create_user_and_login(rules=[rule], organization=org) + results = self.app.delete(f'/api/task/{task.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # test permission to delete own tasks - other user of organization + # should fail + rule = Rule.get_by_("task", Scope.OWN, Operation.DELETE) + user = self.create_user(rules=[rule], organization=org) + task = Task(collaboration=col, init_org=org, init_user=user) + task.save() + headers = self.create_user_and_login(rules=[rule], organization=org) + results = self.app.delete(f'/api/task/{task.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.UNAUTHORIZED) + + # test permission to delete own tasks with same user + headers = self.login(user.username) + results = self.app.delete(f'/api/task/{task.id}', headers=headers) + self.assertEqual(results.status_code, HTTPStatus.OK) + + # cleanup + user.delete() + org.delete() + org2.delete() + col.delete() + def test_view_task_result_permissions_as_user(self): # non-existing task @@ -2480,19 +3304,22 @@ def test_view_task_result_permissions_as_user(self): # test with organization permissions from other organization org = Organization() - col = Collaboration(organizations=[org]) - task = Task(collaboration=col) + org2 = Organization() + col = Collaboration(organizations=[org, org2]) + col.save() + task = Task(collaboration=col, init_org=org) # NB: node is used implicitly in task/{id}/result schema node = Node(organization=org, collaboration=col) res = Run(task=task, organization=org) res.save() - rule = Rule.get_by_("run", Scope.ORGANIZATION, Operation.VIEW) + # Test with permissions of someone who is not in the collaboration + rule = Rule.get_by_("run", Scope.COLLABORATION, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) result = self.app.get(f'/api/run?task_id={task.id}', headers=headers) - self.assertEqual(len(result.json['data']), 0) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) - # test with organization permission + # test with collaboration permission headers = self.create_user_and_login(org, [rule]) result = self.app.get(f'/api/run?task_id={task.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) @@ -2504,33 +3331,76 @@ def test_view_task_result_permissions_as_user(self): self.assertEqual(result.status_code, HTTPStatus.OK) # test also result endpoint - rule = Rule.get_by_("run", Scope.ORGANIZATION, Operation.VIEW) + rule = Rule.get_by_("run", Scope.COLLABORATION, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) result = self.app.get( f'/api/result?task_id={task.id}', headers=headers) - self.assertEqual(len(result.json['data']), 0) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) - # test with organization permission + # test result endpoint with organization permission headers = self.create_user_and_login(org, [rule]) result = self.app.get( f'/api/result?task_id={task.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) - # test with global permission + # test result endpoint with global permission rule = Rule.get_by_("run", Scope.GLOBAL, Operation.VIEW) headers = self.create_user_and_login(rules=[rule]) result = self.app.get( f'/api/result?task_id={task.id}', headers=headers) self.assertEqual(result.status_code, HTTPStatus.OK) + # test with organization permission + rule = Rule.get_by_("run", Scope.ORGANIZATION, Operation.VIEW) + headers = self.create_user_and_login(org, [rule]) + result = self.app.get(f'/api/run?task_id={task.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + result = self.app.get(f'/api/run/{res.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # test with organization permission - other organization should fail + headers = self.create_user_and_login(org2, [rule]) + result = self.app.get( + f'/api/run?task_id={task.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + result = self.app.get(f'/api/run/{res.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + + # test with permission to view own runs + rule = Rule.get_by_("run", Scope.OWN, Operation.VIEW) + user = self.create_user(rules=[rule], organization=org) + headers = self.login(user.username) + task2 = Task(collaboration=col, init_org=org, init_user=user) + task2.save() + res2 = Run(task=task2, organization=org) + res2.save() + result = self.app.get(f'/api/run?task_id={task2.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + result = self.app.get(f'/api/run/{res2.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.OK) + + # test with permission to view own runs - other user should fail + headers = self.create_user_and_login(rules=[rule], organization=org) + result = self.app.get(f'/api/run?task_id={task2.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + result = self.app.get(f'/api/run/{res2.id}', headers=headers) + self.assertEqual(result.status_code, HTTPStatus.UNAUTHORIZED) + # cleanup node.delete() + task.delete() + task2.delete() + res.delete() + res2.delete() + org.delete() + org2.delete() + col.delete() def test_view_task_run_permissions_as_container(self): # test if container can org = Organization() col = Collaboration(organizations=[org]) - task = Task(collaboration=col, image="some-image") + task = Task(collaboration=col, image="some-image", init_org=org) task.save() res = Run(task=task, organization=org, status=TaskStatus.PENDING) res.save()
vantage6-server/vantage6/server/default_roles.py+18 −13 modified@@ -58,8 +58,8 @@ def get_default_roles(db) -> list[dict]: db.Rule.get_by_('collaboration', Scope.ORGANIZATION, Operation.VIEW), db.Rule.get_by_('role', Scope.ORGANIZATION, Operation.VIEW), db.Rule.get_by_('node', Scope.ORGANIZATION, Operation.VIEW), - db.Rule.get_by_('task', Scope.ORGANIZATION, Operation.VIEW), - db.Rule.get_by_('run', Scope.ORGANIZATION, Operation.VIEW), + db.Rule.get_by_('task', Scope.COLLABORATION, Operation.VIEW), + db.Rule.get_by_('run', Scope.COLLABORATION, Operation.VIEW), db.Rule.get_by_('port', Scope.ORGANIZATION, Operation.VIEW), db.Rule.get_by_('event', Scope.ORGANIZATION, Operation.RECEIVE), ] @@ -71,7 +71,7 @@ def get_default_roles(db) -> list[dict]: } # 3. Researcher role RESEARCHER_RULES = VIEWER_RULES + [ - db.Rule.get_by_('task', Scope.ORGANIZATION, Operation.CREATE), + db.Rule.get_by_('task', Scope.COLLABORATION, Operation.CREATE), db.Rule.get_by_('task', Scope.ORGANIZATION, Operation.DELETE), ] RESEARCHER_ROLE = { @@ -102,17 +102,22 @@ def get_default_roles(db) -> list[dict]: } # 4. Collaboration administrator role COLLAB_ADMIN_RULES = ORG_ADMIN_RULES + [ - db.Rule.get_by_('user', Scope.GLOBAL, Operation.VIEW), - db.Rule.get_by_('user', Scope.GLOBAL, Operation.CREATE), - db.Rule.get_by_('user', Scope.GLOBAL, Operation.EDIT), + db.Rule.get_by_('user', Scope.COLLABORATION, Operation.VIEW), + db.Rule.get_by_('user', Scope.COLLABORATION, Operation.CREATE), + db.Rule.get_by_('user', Scope.COLLABORATION, Operation.EDIT), + # The following rule is given so that a collaboration admin can + # view which organizations they may add to their collaboration db.Rule.get_by_('organization', Scope.GLOBAL, Operation.VIEW), - db.Rule.get_by_('organization', Scope.GLOBAL, Operation.EDIT), - db.Rule.get_by_('collaboration', Scope.GLOBAL, Operation.VIEW), - db.Rule.get_by_('collaboration', Scope.GLOBAL, Operation.EDIT), - db.Rule.get_by_('role', Scope.GLOBAL, Operation.VIEW), - db.Rule.get_by_('node', Scope.GLOBAL, Operation.CREATE), - db.Rule.get_by_('node', Scope.GLOBAL, Operation.VIEW), - db.Rule.get_by_('node', Scope.GLOBAL, Operation.DELETE), + db.Rule.get_by_('organization', Scope.COLLABORATION, Operation.EDIT), + db.Rule.get_by_('collaboration', Scope.COLLABORATION, Operation.VIEW), + db.Rule.get_by_('collaboration', Scope.COLLABORATION, Operation.EDIT), + # The following rule is given so that a collaboration admin can + # create a new collaboration + db.Rule.get_by_('collaboration', Scope.GLOBAL, Operation.CREATE), + db.Rule.get_by_('role', Scope.COLLABORATION, Operation.VIEW), + db.Rule.get_by_('node', Scope.COLLABORATION, Operation.CREATE), + db.Rule.get_by_('node', Scope.COLLABORATION, Operation.VIEW), + db.Rule.get_by_('node', Scope.COLLABORATION, Operation.DELETE), db.Rule.get_by_('event', Scope.COLLABORATION, Operation.RECEIVE), db.Rule.get_by_('event', Scope.COLLABORATION, Operation.SEND), ]
vantage6-server/vantage6/server/__init__.py+0 −2 modified@@ -13,14 +13,12 @@ import importlib import logging -import os import uuid import json import time import datetime as dt import traceback -from typing import Any from http import HTTPStatus from werkzeug.exceptions import HTTPException from flasgger import Swagger
vantage6-server/vantage6/server/model/rule.py+2 −2 modified@@ -7,7 +7,7 @@ from vantage6.server.model.base import Base, DatabaseSessionManager -class Operation(Enumerate): +class Operation(str, Enumerate): """ Enumerator of all available operations """ VIEW = "v" EDIT = "e" @@ -17,7 +17,7 @@ class Operation(Enumerate): RECEIVE = "r" -class Scope(Enumerate): +class Scope(str, Enumerate): """ Enumerator of all available scopes """ OWN = "own" ORGANIZATION = "org"
vantage6-server/vantage6/server/model/task.py+1 −1 modified@@ -32,7 +32,7 @@ class Task(Base): Id of the parent task (if any) database : str Name of the database that needs to be used for this task - initiator_id : int + init_org_id : int Id of the organization that created this task init_user_id : int Id of the user that created this task
vantage6-server/vantage6/server/permission.py+175 −2 modified@@ -6,9 +6,13 @@ from vantage6.server.globals import RESOURCES from vantage6.server.default_roles import DefaultRole +from vantage6.server.model.base import Base from vantage6.server.model.role import Role from vantage6.server.model.rule import Rule, Operation, Scope from vantage6.server.model.base import DatabaseSessionManager +from vantage6.server.utils import ( + obtain_auth_collaborations, obtain_auth_organization +) from vantage6.common import logger_name module_name = logger_name(__name__) @@ -17,7 +21,7 @@ RuleNeed = namedtuple("RuleNeed", ["name", "scope", "operation"]) -class RuleCollection: +class RuleCollection(dict): """ Class that tracks a set of all rules for a certain resource name @@ -42,7 +46,176 @@ def add(self, scope: Scope, operation: Operation) -> None: What operation the rule applies to """ permission = Permission(RuleNeed(self.name, scope, operation)) - self.__setattr__(f'{operation.value}_{scope.value}', permission) + self.__setattr__(f'{operation}_{scope}', permission) + + def can_for_org(self, operation: Operation, subject_org_id: int) -> bool: + """ + Check if an operation is allowed on a certain organization + + Parameters + ---------- + operation: Operation + Operation to check if allowed + subject_org_id: int + Organization id on which the operation should be allowed + + Returns + ------- + bool + True if the operation is allowed on the organization, False + otherwise + """ + auth_org = obtain_auth_organization() + + # check if the entity has global permission + global_perm = getattr(self, f'{operation}_{Scope.GLOBAL}') + if global_perm and global_perm.can(): + return True + + # check if the entity has organization permission and organization is + # the same as the subject organization + org_perm = getattr(self, f'{operation}_{Scope.ORGANIZATION}') + if auth_org.id == subject_org_id and org_perm and org_perm.can(): + return True + + # check if the entity has collaboration permission and the subject + # organization is in the collaboration of the own organization + col_perm = getattr(self, f'{operation}_{Scope.COLLABORATION}') + if col_perm and col_perm.can(): + for col in auth_org.collaborations: + if subject_org_id in [org.id for org in col.organizations]: + return True + + # no permission found + return False + + def can_for_col(self, operation: Operation, collaboration_id: int) -> bool: + """ + Check if the user or node can perform the operation on a certain + collaboration + + Parameters + ---------- + operation: Operation + Operation to check if allowed + collaboration_id: int + Collaboration id on which the operation should be allowed + """ + auth_collabs = obtain_auth_collaborations() + + # check if the entity has global permission + global_perm = getattr(self, f'{operation}_{Scope.GLOBAL}') + if global_perm and global_perm.can(): + return True + + # check if the entity has collaboration permission and the subject + # collaboration is in the collaborations of the user/node + col_perm = getattr(self, f'{operation}_{Scope.COLLABORATION}') + if col_perm and col_perm.can() and \ + self._id_in_list(collaboration_id, auth_collabs): + return True + + # no permission found + return False + + def get_max_scope(self, operation: Operation) -> Scope | None: + """ + Get the highest scope that the entity has for a certain operation + + Parameters + ---------- + operation: Operation + Operation to check + + Returns + ------- + Scope | None + Highest scope that the entity has for the operation. None if the + entity has no permission for the operation + """ + if getattr(self, f'{operation}_{Scope.GLOBAL}'): + return Scope.GLOBAL + elif getattr(self, f'{operation}_{Scope.COLLABORATION}'): + return Scope.COLLABORATION + elif getattr(self, f'{operation}_{Scope.ORGANIZATION}'): + return Scope.ORGANIZATION + elif getattr(self, f'{operation}_{Scope.OWN}'): + return Scope.OWN + else: + return None + + def has_at_least_scope(self, scope: Scope, operation: Operation) -> bool: + """ + Check if the entity has at least a certain scope for a certain + operation + + Parameters + ---------- + scope: Scope + Scope to check if the entity has at least + operation: Operation + Operation to check + + Returns + ------- + bool + True if the entity has at least the scope, False otherwise + """ + scopes: list[Scope] = self._get_scopes_from(scope) + for s in scopes: + perm = getattr(self, f'{operation}_{s}') + if perm and perm.can(): + return True + return False + + def _id_in_list(self, id_: int, resource_list: list[Base]) -> bool: + """ + Check if resource list contains a resource with a certain ID + + Parameters + ---------- + id_ : int + ID of the resource + resource_list : list[db.Base] + List of resources + + Returns + ------- + bool + True if resource is in list, False otherwise + """ + return any(r.id == id_ for r in resource_list) + + def _get_scopes_from(self, minimal_scope: Scope) -> list[Scope]: + """ + Get scopes that are at least equal to a certain scope + + Parameters + ---------- + minimal_scope: Scope + Minimal scope + + Returns + ------- + list[Scope] + List of scopes that are at least equal to the minimal scope + + Raises + ------ + ValueError + If the minimal scope is not known + """ + if minimal_scope == Scope.ORGANIZATION: + return [Scope.ORGANIZATION, Scope.COLLABORATION, Scope.GLOBAL] + elif minimal_scope == Scope.COLLABORATION: + return [Scope.COLLABORATION, Scope.GLOBAL] + elif minimal_scope == Scope.GLOBAL: + return [Scope.GLOBAL] + elif minimal_scope == Scope.OWN: + return [Scope.OWN, Scope.ORGANIZATION, Scope.COLLABORATION, + Scope.GLOBAL] + else: + raise ValueError(f"Unknown scope '{minimal_scope}'") class PermissionManager:
vantage6-server/vantage6/server/resource/collaboration.py+40 −13 modified@@ -13,6 +13,7 @@ CollaborationInputSchema ) from vantage6.server.permission import ( + RuleCollection, Scope as S, Operation as P, PermissionManager @@ -122,12 +123,18 @@ def permissions(permissions: PermissionManager) -> None: add(scope=S.GLOBAL, operation=P.EDIT, description="edit any collaboration") + add(scope=S.COLLABORATION, operation=P.EDIT, + description="edit any collaboration that your organization " + "participates in") add(scope=S.GLOBAL, operation=P.CREATE, description="create a new collaboration") add(scope=S.GLOBAL, operation=P.DELETE, description="delete a collaboration") + add(scope=S.COLLABORATION, operation=P.DELETE, + description="delete any collaboration that your organization " + "participates in") # ------------------------------------------------------------------------------ @@ -137,7 +144,7 @@ class CollaborationBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class Collaborations(CollaborationBase): @@ -408,6 +415,8 @@ def patch(self, id): Description|\n |--|--|--|--|--|--|\n |Collaboration|Global|Edit|❌|❌|Update a collaboration|\n\n + |Collaboration|Collaboration|Edit|❌|❌|Update a collaboration that + you are already a member of|\n\n Accessible to users. @@ -459,7 +468,7 @@ def patch(self, id): "can not be found"}, HTTPStatus.NOT_FOUND # 404 # verify permissions - if not self.r.e_glo.can(): + if not self.r.can_for_col(P.EDIT, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -505,6 +514,8 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |Collaboration|Global|Delete|❌|❌|Remove collaboration|\n\n + |Collaboration|Collaboration|Delete|❌|❌|Remove collaborations + that you are part of yourself|\n\n Accessible to users. @@ -542,7 +553,7 @@ def delete(self, id): HTTPStatus.NOT_FOUND # verify permissions - if not self.r.d_glo.can(): + if not self.r.can_for_col(P.DELETE, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -574,7 +585,7 @@ class CollaborationOrganization(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) @only_for(("node", "user", "container")) def get(self, id): @@ -663,6 +674,8 @@ def post(self, id): |--|--|--|--|--|--|\n |Collaboration|Global|Edit|❌|❌|Add organization to a collaboration|\n\n + |Collaboration|Collaboration|Edit|❌|❌|Add organization to a + collaboration that your organization is already a member of|\n\n Accessible to users. @@ -703,7 +716,7 @@ def post(self, id): "not be found"}, HTTPStatus.NOT_FOUND # verify permissions - if not self.r.e_glo.can(): + if not self.r.can_for_col(P.EDIT, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -739,6 +752,8 @@ def delete(self, id): |--|--|--|--|--|--|\n |Collaboration|Global|Edit|❌|❌|Remove an organization from an existing collaboration|\n\n + |Collaboration|Collaboration|Edit|❌|❌|Remove an organization from + an existing collaboration that your organization is a member of|\n\n Accessible to users. @@ -772,17 +787,17 @@ def delete(self, id): # get collaboration from which organization should be removed collaboration = db.Collaboration.get(id) if not collaboration: - return {"msg": f"collaboration having collaboration_id={id} can " + return {"msg": f"Collaboration with collaboration_id={id} can " "not be found"}, HTTPStatus.NOT_FOUND # get organization which should be deleted data = request.get_json() organization = db.Organization.get(data['id']) if not organization: - return {"msg": f"organization with id={id} is not found"}, \ + return {"msg": f"Organization with id={id} is not found"}, \ HTTPStatus.NOT_FOUND - if not self.r.d_glo.can(): + if not self.r.can_for_col(P.EDIT, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -798,7 +813,7 @@ class CollaborationNode(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) @with_user def get(self, id): @@ -883,7 +898,9 @@ def post(self, id): |Rule name|Scope|Operation|Assigned to node|Assigned to container| Description|\n |--|--|--|--|--|--|\n - |Collaboration|Global|Create|❌|❌|Add node to collaboration|\n + |Collaboration|Global|Edit|❌|❌|Add node to collaboration|\n + |Collaboration|Collaboration|Edit|❌|❌|Add node to collaboration + that your organization is a member of|\n Accessible to users. @@ -924,7 +941,7 @@ def post(self, id): return {"msg": f"collaboration having collaboration_id={id} can " "not be found"}, HTTPStatus.NOT_FOUND - if not self.r.e_glo.can(): + if not self.r.can_for_col(P.EDIT, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -939,9 +956,16 @@ def post(self, id): if not node: return {"msg": f"node id={data['id']} not found"}, \ HTTPStatus.NOT_FOUND + if node in collaboration.nodes: return {"msg": f"node id={data['id']} is already in collaboration " f"id={id}"}, HTTPStatus.BAD_REQUEST + elif node.organization not in collaboration.organizations: + return { + "msg": f"Node id={data['id']} belongs to an organization that " + f"is not part of collaboration id={id}. Please add the " + "organization to the collaboration first" + }, HTTPStatus.BAD_REQUEST collaboration.nodes.append(node) collaboration.save() @@ -960,6 +984,8 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |Collaboration|Global|Edit|❌|❌|Remove node from collaboration|\n + |Collaboration|Collaboration|Edit|❌|❌|Remove node from + collaboration that your organization is a member of|\n Accessible to users. @@ -997,14 +1023,15 @@ def delete(self, id): return {"msg": f"collaboration having collaboration_id={id} can " "not be found"}, HTTPStatus.NOT_FOUND - if not self.r.e_glo.can(): + if not self.r.can_for_col(P.EDIT, collaboration.id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED data = request.get_json() node = db.Node.get(data['id']) if not node: return {"msg": f"node id={id} not found"}, HTTPStatus.NOT_FOUND + if node not in collaboration.nodes: return {"msg": f"node id={data['id']} is not part of " f"collaboration id={id}"}, HTTPStatus.BAD_REQUEST @@ -1087,7 +1114,7 @@ def get(self, id): auth_org = self.obtain_auth_organization() if not self.r.v_glo.can(): - if not (self.r.v_org.can() and auth_org in col.organizations): + if not (self.r.v_col.can() and auth_org in col.organizations): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED
vantage6-server/vantage6/server/resource/__init__.py+51 −1 modified@@ -16,6 +16,10 @@ from vantage6.common import logger_name from vantage6.server import db +from vantage6.server.utils import ( + obtain_auth_collaborations, obtain_auth_organization +) +from vantage6.server.model.authenticatable import Authenticatable from vantage6.server.resource.common.output_schema import HATEOASModelSchema from vantage6.server.permission import PermissionManager from vantage6.server.resource.common.pagination import Page @@ -149,7 +153,19 @@ def obtain_auth_organization(cls) -> db.Organization: db.Organization Organization model """ - return db.Organization.get(cls.obtain_organization_id()) + return obtain_auth_organization() + + @staticmethod + def obtain_auth_collaborations() -> list[db.Collaboration]: + """ + Obtain the collaborations that the auth is part of. + + Returns + ------- + list[db.Collaboration] + List of collaborations + """ + return obtain_auth_collaborations() # ------------------------------------------------------------------------------ @@ -267,3 +283,37 @@ def parse_datetime(dt: str = None, default: datetime = None) -> datetime: if dt: return datetime.datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S.%f') return default + + +def get_org_ids_from_collabs(auth: Authenticatable, + collab_id: int = None) -> list[int]: + """ + Get all organization ids from the collaborations the user or node is in. + + Parameters + ---------- + auth : Authenticatable + User or node + collab_id : int, optional + Collaboration id. If given, only return the organization ids of this + collaboration. If not given, return all organization ids of all + collaborations the user or node is in. + + Returns + ------- + list[int] + List of organization ids + """ + if collab_id: + return [ + org.id + for col in auth.organization.collaborations + for org in col.organizations + if col.id == collab_id + ] + else: + return [ + org.id + for col in auth.organization.collaborations + for org in col.organizations + ] \ No newline at end of file
vantage6-server/vantage6/server/resource/node.py+81 −46 modified@@ -9,8 +9,9 @@ from vantage6.server.resource import with_user_or_node, with_user from vantage6.server.resource import ServicesResources from vantage6.server.resource.common.pagination import Pagination -from vantage6.server.permission import (Scope as S, - Operation as P, PermissionManager) +from vantage6.server.permission import ( + RuleCollection, Scope as S, Operation as P, PermissionManager +) from vantage6.server import db from vantage6.server.resource.common.output_schema import NodeSchema from vantage6.server.resource.common.input_schema import NodeInputSchema @@ -67,20 +68,28 @@ def permissions(permissions: PermissionManager) -> None: add = permissions.appender(module_name) add(scope=S.GLOBAL, operation=P.VIEW, description="view any node") + add(scope=S.COLLABORATION, operation=P.VIEW, + description="view any node in your collaborations") add(scope=S.ORGANIZATION, operation=P.VIEW, assign_to_container=True, description="view your own node info", assign_to_node=True) add(scope=S.GLOBAL, operation=P.EDIT, description="edit any node") + add(scope=S.COLLABORATION, operation=P.EDIT, + description="edit any node in your collaborations") add(scope=S.ORGANIZATION, operation=P.EDIT, description="edit node that is part of your organization", assign_to_node=True) add(scope=S.GLOBAL, operation=P.CREATE, description="create node for any organization") + add(scope=S.COLLABORATION, operation=P.CREATE, + description="create node for any organization in your collaborations") add(scope=S.ORGANIZATION, operation=P.CREATE, description="create new node for your organization") add(scope=S.GLOBAL, operation=P.DELETE, description="delete any node") + add(scope=S.COLLABORATION, operation=P.DELETE, + description="delete any node in your collaborations") add(scope=S.ORGANIZATION, operation=P.DELETE, description="delete node that is part of your organization") @@ -96,7 +105,7 @@ class NodeBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class Nodes(NodeBase): @@ -115,6 +124,8 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |Node|Global|View|❌|❌|View any node information|\n + |Node|Collaboration|View|❌|❌|View any node information for nodes + in your collaborations|\n |Node|Organization|View|✅|✅|View node information for nodes that belong to your organization|\n @@ -196,7 +207,23 @@ def get(self): auth_org_id = self.obtain_organization_id() args = request.args - for param in ['organization_id', 'collaboration_id', 'status', 'ip']: + if 'organization_id' in args: + if not self.r.can_for_org(P.VIEW, args['organization_id']): + return { + 'msg': 'You lack the permission view nodes from the ' + f'organization with id {args["organization_id"]}!' + }, HTTPStatus.UNAUTHORIZED + q = q.filter(db.Node.organization_id == args['organization_id']) + + if 'collaboration_id' in args: + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return { + 'msg': 'You lack the permission view nodes from the ' + f'collaboration with id {args["collaboration_id"]}!' + }, HTTPStatus.UNAUTHORIZED + q = q.filter(db.Node.collaboration_id == args['collaboration_id']) + + for param in ['status', 'ip']: if param in args: q = q.filter(getattr(db.Node, param) == args[param]) if 'name' in args: @@ -208,7 +235,11 @@ def get(self): q = q.filter(db.Node.last_seen >= args['last_seen_from']) if not self.r.v_glo.can(): - if self.r.v_org.can(): + if self.r.v_col.can(): + q = q.filter(db.Node.collaboration_id.in_( + [col.id for col in self.obtain_auth_collaborations()] + )) + elif self.r.v_org.can(): # only the results of the user's organization are returned q = q.filter(db.Node.organization_id == auth_org_id) else: @@ -231,18 +262,20 @@ def post(self): """Create node --- description: >- - Creates a new node-account belonging to a specific collaboration - which is specified in the POST body.\n + Creates a new node-account belonging to a specific organization and + collaboration which is specified in the POST body.\n The organization of the user needs to be within the collaboration.\n ### Permission Table\n |Rule name|Scope|Operation|Assigned to node|Assigned to container| Description|\n |--|--|--|--|--|--|\n |Node|Global|Create|❌|❌|Create a new node account belonging to a - specific collaboration|\n + specific organization in any collaboration|\n + |Node|Collaboration|Create|❌|❌|Create a new node account belonging + to a specific organization in your collaborations|\n |Node|Organization|Create|❌|❌|Create a new node account belonging - to a specific organization which is also part of the collaboration|\n + to your organization|\n Accessible to users. @@ -256,12 +289,14 @@ def post(self): description: Collaboration id organization_id: type: integer - description: Organization id. If not provided, the user's - organization is used + description: Organization id. If not provided, this + defaults to the organization of the user creating the + node. name: type: string description: Human-readable name. If not provided a name - is generated + is generated based on organization and collaboration + name. responses: 201: @@ -293,17 +328,20 @@ def post(self): return {"msg": f"collaboration id={data['collaboration_id']} " "does not exist"}, HTTPStatus.NOT_FOUND # 404 + org_id = data["organization_id"] \ + if data.get("organization_id") is not None \ + else g.user.organization_id + organization = db.Organization.get(org_id) + + # check that the organization exists + if not organization: + return {"msg": f"organization id={org_id} does not exist"}, \ + HTTPStatus.NOT_FOUND + # check permissions - org_id = data.get("organization_id", None) - user_org_id = g.user.organization.id - if not self.r.c_glo.can(): - own = not org_id or org_id == user_org_id - if not (self.r.c_org.can() and own): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED - else: - org_id = g.user.organization.id - organization = db.Organization.get(org_id or user_org_id) + if not self.r.can_for_org(P.CREATE, org_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED # we need to check that the organization belongs to the # collaboration @@ -359,14 +397,16 @@ def get(self, id): Description|\n |--|--|--|--|--|--|\n |Node|Global|View|❌|❌|View any node information|\n + |Node|Collaboration|View|❌|❌|View any node information for nodes + within your collaborations|\n |Node|Organization|View|✅|✅|View node information for nodes that belong to your organization|\n Accessible to users. parameters: - in: path - name: id + name: id_ schema: type: integer minimum: 1 @@ -388,17 +428,13 @@ def get(self, id): """ node = db.Node.get(id) if not node: - return {'msg': f'Node id={id} is not found!'}, HTTPStatus.NOT_FOUND - - # obtain authenticated model - auth = self.obtain_auth() + return {'msg': f'Node id={id} is not found!'}, \ + HTTPStatus.NOT_FOUND # check permissions - if not self.r.v_glo.can(): - same_org = auth.organization.id == node.organization.id - if not (self.r.v_org.can() and same_org): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.VIEW, node.organization_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED return node_schema.dump(node, many=False), HTTPStatus.OK @@ -416,14 +452,16 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |Node|Global|Delete|❌|❌|Delete a node|\n + |Node|Collaboration|Delete|❌|❌|Delete a node that belongs to + one of the organizations in your collaborations|\n |Node|Organization|Delete|❌|❌|Delete a node that belongs to your organization|\n Accessible to users. parameters: - in: path - name: id + name: id_ schema: type: integer minimum: 1 @@ -447,11 +485,9 @@ def delete(self, id): if not node: return {"msg": f"Node id={id} not found"}, HTTPStatus.NOT_FOUND - if not self.r.d_glo.can(): - own = node.organization == g.user.organization - if not (self.r.d_org.can() and own): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.DELETE, node.organization_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED node.delete() return {"msg": f"Successfully deleted node id={id}"}, HTTPStatus.OK @@ -470,14 +506,16 @@ def patch(self, id): Description|\n |--|--|--|--|--|--|\n |Node|Global|Edit|❌|❌|Update a node specified by id|\n + |Node|Collaboration|Edit|❌|❌|Update a node specified by id which + is part of one of your collaborations|\n |Node|Organization|Edit|❌|❌|Update a node specified by id which is part of your organization|\n Accessible to users. parameters: - in: path - name: id + name: id_ schema: type: integer description: Node id @@ -531,13 +569,9 @@ def patch(self, id): if not node: return {'msg': f'Node id={id} not found!'}, HTTPStatus.NOT_FOUND - auth = g.user or g.node - - if not self.r.e_glo.can(): - own = auth.organization.id == node.organization.id - if not (self.r.e_org.can() and own): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.EDIT, node.organization_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED # update fields if 'name' in data: @@ -561,6 +595,7 @@ def patch(self, id): 'not found!'}, HTTPStatus.NOT_FOUND node.organization = organization + auth = self.obtain_auth() col_id = data.get('collaboration_id') updated_col = col_id and col_id != node.collaboration.id if updated_col:
vantage6-server/vantage6/server/resource/organization.py+20 −29 modified@@ -11,7 +11,8 @@ from vantage6.server.permission import ( Scope as S, Operation as P, - PermissionManager + PermissionManager, + RuleCollection ) from vantage6.server.resource.common.input_schema import ( OrganizationInputSchema @@ -102,6 +103,8 @@ def permissions(permissions: PermissionManager) -> None: description="edit any organization") add(scope=S.ORGANIZATION, operation=P.EDIT, description="edit your own organization info", assign_to_node=True) + add(scope=S.COLLABORATION, operation=P.EDIT, + description='edit collaborating organizations') add(scope=S.GLOBAL, operation=P.CREATE, description="create a new organization") @@ -117,7 +120,7 @@ class OrganizationBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class Organizations(OrganizationBase): @@ -207,6 +210,12 @@ def get(self): if 'country' in args: q = q.filter(db.Organization.country == args['country']) if 'collaboration_id' in args: + # TODO we also need to check here if the user is part of the collab + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return { + 'msg': 'You lack the permission to get all organizations ' + 'in your collaboration!' + }, HTTPStatus.UNAUTHORIZED q = q.join(db.Member).join(db.Collaboration)\ .filter(db.Collaboration.id == args['collaboration_id']) @@ -220,8 +229,9 @@ def get(self): ).all() g.session.commit() - # list comprehension fetish, and add own organization in case - # this organization does not participate in any collaborations yet + # filter orgs in own collaborations, and add own organization in + # case this organization does not participate in any collaborations + # yet org_ids = [o.id for col in collabs for o in col.organizations] org_ids = list(set(org_ids + [auth_org.id])) @@ -353,36 +363,19 @@ def get(self, id): tags: ["Organization"] """ - # obtain organization of authenticated - auth_org = self.obtain_auth_organization() - # retrieve requested organization req_org = db.Organization.get(id) if not req_org: return {'msg': f'Organization id={id} not found!'}, \ HTTPStatus.NOT_FOUND - accepted = False # Check if auth has enough permissions - if self.r.v_glo.can(): - accepted = True - elif self.r.v_col.can(): - # check if the organization is whithin a collaboration - for col in auth_org.collaborations: - if req_org in col.organizations: - accepted = True - # or that the organization is auths org - if req_org == auth_org: - accepted = True - elif self.r.v_org.can(): - accepted = auth_org == req_org - - if accepted: - return org_schema.dump(req_org, many=False), HTTPStatus.OK - else: + if not self.r.can_for_org(P.VIEW, id): return {'msg': 'You do not have permission to do that!'}, \ HTTPStatus.UNAUTHORIZED + return org_schema.dump(req_org, many=False), HTTPStatus.OK + @only_for(("user", "node")) def patch(self, id): """Update organization @@ -396,6 +389,8 @@ def patch(self, id): |--|--|--|--|--|--|\n |Organization|Global|Edit|❌|❌|Update an organization with specified id|\n + |Organization|Collaboration|Edit|❌|❌|Update an organization within + the collaboration the user is part of|\n |Organization|Organization|Edit|❌|❌|Update the organization that the user is part of|\n @@ -442,11 +437,7 @@ def patch(self, id): return {"msg": f"Organization with id={id} not found"}, \ HTTPStatus.NOT_FOUND - if not ( - self.r.e_glo.can() or - (self.r.e_org.can() and g.user and id == g.user.organization.id) or - (self.r.e_org.can() and g.node and id == g.node.organization.id) - ): + if not self.r.can_for_org(P.EDIT, id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED
vantage6-server/vantage6/server/resource/role.py+119 −46 modified@@ -9,12 +9,15 @@ from vantage6.server import db from vantage6.server.resource import ( + get_org_ids_from_collabs, with_user, ServicesResources ) from vantage6.common import logger_name from vantage6.server.permission import ( - PermissionManager + PermissionManager, + RuleCollection, + Operation as P, ) from vantage6.server.model.rule import Operation, Scope from vantage6.server.resource.common.output_schema import RoleSchema, RuleSchema @@ -87,20 +90,29 @@ def permissions(permissions: PermissionManager) -> None: add = permissions.appender(module_name) add(scope=Scope.GLOBAL, operation=Operation.VIEW, description="View any role") + add(scope=Scope.COLLABORATION, operation=Operation.VIEW, + description="View any role in your collaborations") add(scope=Scope.ORGANIZATION, operation=Operation.VIEW, description="View the roles of your organization") add(scope=Scope.GLOBAL, operation=Operation.CREATE, description="Create role for any organization") + add(scope=Scope.COLLABORATION, operation=Operation.CREATE, + description="Create role for any organization in your collaborations") add(scope=Scope.ORGANIZATION, operation=Operation.CREATE, description="Create role for your organization") add(scope=Scope.GLOBAL, operation=Operation.EDIT, description="Edit any role") + add(scope=Scope.COLLABORATION, operation=Operation.EDIT, + description="Edit any role in your collaborations") add(scope=Scope.ORGANIZATION, operation=Operation.EDIT, description="Edit a role from your organization") add(scope=Scope.GLOBAL, operation=Operation.DELETE, - description="Delete any organization") + description="Delete a role from any organization") + add(scope=Scope.COLLABORATION, operation=Operation.DELETE, + description="Delete a role from any organization in your " + "collaborations") add(scope=Scope.ORGANIZATION, operation=Operation.DELETE, - description="Delete your organization") + description="Delete a role from your organization") # ----------------------------------------------------------------------------- @@ -115,7 +127,7 @@ class RoleBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class Roles(RoleBase): @@ -135,6 +147,8 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |Role|Global|View|❌|❌|View all roles|\n + |Role|Collaboration|View|❌|❌|View all roles in your + collaborations|\n |Role|Organization|View|❌|❌|View roles that are part of your organization|\n @@ -166,6 +180,11 @@ def get(self): items: type: integer description: Organization id of which you want to get roles + - in: query + name: collaboration_id + schema: + type: integer + description: Collaboration id - in: query name: rule_id schema: @@ -214,20 +233,43 @@ def get(self): """ q = g.session.query(db.Role) - auth_org_id = self.obtain_organization_id() + auth_org = self.obtain_auth_organization() args = request.args # filter by organization ids (include root role if desired) org_filters = args.getlist('organization_id') if org_filters: + for org_id in org_filters: + if not self.r.can_for_org(P.VIEW, org_id): + return { + 'msg': 'You lack the permission view all roles from ' + f'organization {org_id}!' + }, HTTPStatus.UNAUTHORIZED if 'include_root' in args and args['include_root']: q = q.filter(or_( db.Role.organization_id.in_(org_filters), - db.Role.organization_id == None + db.Role.organization_id.is_(None) )) else: q = q.filter(db.Role.organization_id.in_(org_filters)) + # filter by collaboration id + if 'collaboration_id' in args: + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return { + 'msg': 'You lack the permission view all roles from ' + f'collaboration {args["collaboration_id"]}!' + }, HTTPStatus.UNAUTHORIZED + org_ids = get_org_ids_from_collabs(g.user, + args['collaboration_id']) + if 'include_root' in args and args['include_root']: + q = q.filter(or_( + db.Role.organization_id.in_(org_ids), + db.Role.organization_id.is_(None) + )) + else: + q = q.filter(db.Role.organization_id.in_(org_ids)) + # filter by one or more names or descriptions for param in ['name', 'description']: filters = args.getlist(param) @@ -238,23 +280,49 @@ def get(self): # find roles containing a specific rule if 'rule_id' in args: + rule = db.Rule.get(args['rule_id']) + if not rule: + return {'msg': f'Rule with id={args["rule_id"]} does not ' + 'exist!'}, HTTPStatus.BAD_REQUEST q = q.join(db.role_rule_association).join(db.Rule)\ .filter(db.Rule.id == args['rule_id']) if 'user_id' in args: + user = db.User.get(args['user_id']) + if not user: + return {'msg': f'User with id={args["user_id"]} does not ' + 'exist!'}, HTTPStatus.BAD_REQUEST + elif not self.r.can_for_org(P.VIEW, user.organization_id) and not \ + g.user.id == user.id: + return { + 'msg': 'You lack the permission view roles from the ' + f'organization that user id={user.id} belongs to!' + }, HTTPStatus.UNAUTHORIZED q = q.join(db.Permission).join(db.User)\ .filter(db.User.id == args['user_id']) if not self.r.v_glo.can(): own_role_ids = [role.id for role in g.user.roles] - if self.r.v_org.can(): + if self.r.v_col.can(): + q = q.filter(or_( + db.Role.id.in_(own_role_ids), + db.Role.organization_id.is_(None), + db.Role.organization_id.in_( + [ + org.id + for col in self.obtain_auth_collaborations() + for org in col.organizations + ] + ) + )) + elif self.r.v_org.can(): # allow user to view all roles of their organization and any # other roles they may have themselves, or default roles from # the root organization q = q.filter(or_( - db.Role.organization_id == auth_org_id, + db.Role.organization_id == auth_org.id, db.Role.id.in_(own_role_ids), - db.Role.organization_id == None + db.Role.organization_id.is_(None) )) else: # allow users without permission to view only their own roles @@ -282,6 +350,8 @@ def post(self): Description|\n |--|--|--|--|--|--|\n |Role|Global|Create|❌|❌|Create a role for any organization|\n + |Role|Collaboration|Create|❌|❌|Create a role for organization in + your collaborations|\n |Role|Organization|Create|❌|❌|Create a role for your organization|\n Accessible to users. @@ -356,14 +426,10 @@ def post(self): 'exist!'}, HTTPStatus.NOT_FOUND # check if user is allowed to create this role - if (not self.r.c_glo.can() and - organization_id != g.user.organization_id): + if not self.r.can_for_org(P.CREATE, organization_id): return { - 'msg': 'You cannot create roles for other organizations!' + 'msg': 'You cannot create a role for this organization!' }, HTTPStatus.UNAUTHORIZED - elif not self.r.c_glo.can() and not self.r.c_org.can(): - return {'msg': 'You lack the permission to create roles!'}, \ - HTTPStatus.UNAUTHORIZED # create the actual role role = db.Role(name=data.get("name"), @@ -388,6 +454,8 @@ def get(self, id): Description|\n |--|--|--|--|--|--|\n |Role|Global|View|❌|❌|View all roles|\n + |Role|Collaboration|View|❌|❌|View all roles for your + collaborations|\n |Role|Organization|View|❌|❌|View roles that are part of your organization|\n @@ -420,11 +488,12 @@ def get(self, id): HTTPStatus.NOT_FOUND # check permissions. A user can always view their own roles - if not (self.r.v_glo.can() or role in g.user.roles): - if not (self.r.v_org.can() - and role.organization == g.user.organization): - return {"msg": "You do not have permission to view this."},\ - HTTPStatus.UNAUTHORIZED + if not ( + self.r.can_for_org(P.VIEW, role.organization_id) or + role in g.user.roles + ): + return {"msg": "You do not have permission to view this."},\ + HTTPStatus.UNAUTHORIZED return role_schema.dump(role, many=False), HTTPStatus.OK @@ -440,6 +509,8 @@ def patch(self, id): Description|\n |--|--|--|--|--|--|\n |Role|Global|Edit|❌|❌|Update any role|\n + |Role|Collaboration|Edit|❌|❌|Update any role in your + collaborations|\n |Role|Organization|Edit|❌|❌|Update a role from your organization|\n Accessible to users. @@ -510,13 +581,9 @@ def patch(self, id): }, HTTPStatus.BAD_REQUEST # check permission of the user - if not self.r.e_glo.can(): - if not self.r.e_org.can(): - return {'msg': 'You do not have permission to edit roles!'}, \ - HTTPStatus.UNAUTHORIZED - elif g.user.organization_id != role.organization.id: - return {'msg': 'You can\'t edit roles from another ' - 'organization'}, HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.EDIT, role.organization_id): + return {'msg': 'You do not have permission to edit this role!'}, \ + HTTPStatus.UNAUTHORIZED # process patch if 'name' in data: @@ -552,6 +619,8 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |Role|Global|Delete|❌|❌|Delete any role|\n + |Role|Collaboration|Delete|❌|❌|Delete any role in your + collaborations|\n |Role|Organization|Delete|❌|❌|Delete a role in your organization|\n Accessible to users. @@ -574,6 +643,8 @@ def delete(self, id): responses: 200: description: Ok + 400: + description: Cannot delete default roles 401: description: Unauthorized 404: @@ -589,13 +660,15 @@ def delete(self, id): return {"msg": f"Role with id={id} not found."}, \ HTTPStatus.NOT_FOUND - if not self.r.d_glo.can(): - if not self.r.d_org.can(): - return {'msg': 'You do not have permission to delete roles!'},\ - HTTPStatus.UNAUTHORIZED - elif role.organization.id != g.user.organization.id: - return {'msg': 'You can\'t delete a role from another ' - 'organization'}, HTTPStatus.UNAUTHORIZED + if role.name in [role for role in DefaultRole]: + return { + "msg": f"This role ('{role.name}') is a default role. Default" + " roles cannot be deleted." + }, HTTPStatus.BAD_REQUEST + + if not self.r.can_for_org(P.DELETE, role.organization_id): + return {'msg': 'You do not have permission to delete this role!'},\ + HTTPStatus.UNAUTHORIZED # check if role is assigned to users if role.users: @@ -707,6 +780,8 @@ def post(self, id, rule_id): Description|\n |--|--|--|--|--|--|\n |Role|Global|Edit|❌|❌|Edit any role|\n + |Role|Collaboration|Edit|❌|❌|Edit any role in your collaborations + |\n |Role|Organization|Edit|❌|❌|Edit any role in your organization|\n Accessible to users. @@ -747,11 +822,9 @@ def post(self, id, rule_id): HTTPStatus.NOT_FOUND # check that this user can edit rules - if not self.r.e_glo.can(): - if not (self.r.e_org.can() and - g.user.organization == role.organization): - return {'msg': 'You lack permissions to do that'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.EDIT, role.organization_id): + return {'msg': 'You lack permissions to do that'}, \ + HTTPStatus.UNAUTHORIZED # user needs to role to assign it denied = self.permissions.check_user_rules([rule]) @@ -776,8 +849,10 @@ def delete(self, id, rule_id): |Rule name|Scope|Operation|Assigned to node|Assigned to container| Description|\n |--|--|--|--|--|--|\n - |Role|Global|Delete|❌|❌|Delete any role rule|\n - |Role|Organization|Delete|❌|❌|Delete any role rule in your + |Role|Global|Edit|❌|❌|Delete any rule in a role|\n + |Role|Collaboration|Edit|❌|❌|Delete any rule in roles in your + collaborations|\n + |Role|Organization|Edit|❌|❌|Delete any rule in roles in your organization|\n Accessible to users. @@ -813,11 +888,9 @@ def delete(self, id, rule_id): return {'msg': f'Rule id={rule_id} not found!'}, \ HTTPStatus.NOT_FOUND - if not self.r.d_glo.can(): - if not (self.r.d_org.can() and - g.user.organization == role.organization): - return {'msg': 'You lack permissions to do that'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.EDIT, role.organization_id): + return {'msg': 'You lack permissions to do that'}, \ + HTTPStatus.UNAUTHORIZED # user needs to role to remove it denied = self.permissions.check_user_rules([rule])
vantage6-server/vantage6/server/resource/run.py+85 −24 modified@@ -10,6 +10,7 @@ from vantage6.common import logger_name from vantage6.server import db from vantage6.server.permission import ( + RuleCollection, PermissionManager, Scope as S, Operation as P @@ -88,11 +89,14 @@ def setup(api, api_base, services): def permissions(permissions: PermissionManager): add = permissions.appender(module_name) - add(scope=S.GLOBAL, operation=P.VIEW, - description="view any run") - add(scope=S.ORGANIZATION, operation=P.VIEW, assign_to_container=True, + add(scope=S.GLOBAL, operation=P.VIEW, description="view any run") + add(scope=S.COLLABORATION, operation=P.VIEW, assign_to_container=True, assign_to_node=True, description="view runs of your organizations " "collaborations") + add(scope=S.ORGANIZATION, operation=P.VIEW, + description="view any run of a task created by your organization") + add(scope=S.OWN, operation=P.VIEW, + description="view any run of a task created by you") # ------------------------------------------------------------------------------ @@ -102,13 +106,13 @@ class RunBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class MultiRunBase(RunBase): """Base class for resources that return multiple runs or results""" - def get_query_multiple_runs(self) -> Union[sa.orm.query.Query, tuple]: + def get_query_multiple_runs(self) -> sa.orm.query.Query | tuple: """ Returns a query object that can be used to retrieve runs. @@ -123,10 +127,39 @@ def get_query_multiple_runs(self) -> Union[sa.orm.query.Query, tuple]: q = g.session.query(db_Run) + if 'organization_id' in args: + if not self.r.can_for_org(P.VIEW, args['organization_id']): + return {'msg': 'You lack the permission to view runs for ' + f'organization id={args["organization_id"]}!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(db_Run.organization_id == args['organization_id']) + + if 'task_id' in args: + task = db.Task.get(args['task_id']) + if not task: + return {'msg': f'Task id={args["task_id"]} does not exist!'}, \ + HTTPStatus.BAD_REQUEST + elif not self.r.can_for_org(P.VIEW, task.init_org_id) \ + and not (self.r.v_own.can() and + g.user.id == task.init_user_id): + return {'msg': 'You lack the permission to view runs for ' + f'task id={args["task_id"]}!'}, HTTPStatus.UNAUTHORIZED + q = q.filter(db_Run.task_id == args['task_id']) + + if args.get('node_id'): + node = db.Node.get(args['node_id']) + if not node: + return {'msg': f'Node id={args["node_id"]} does not exist!'}, \ + HTTPStatus.BAD_REQUEST + elif not self.r.can_for_col(P.VIEW, node.collaboration_id): + return {'msg': 'You lack the permission to view runs for ' + f'node id={args["node_id"]}!'}, HTTPStatus.UNAUTHORIZED + q = q.filter(db.Node.id == args.get('node_id'))\ + .filter(db.Collaboration.id == db.Node.collaboration_id) + # relation filters - for param in ['task_id', 'organization_id', 'port']: - if param in args: - q = q.filter(getattr(db_Run, param) == args[param]) + if 'port' in args: + q = q.filter(db_Run.port == args['port']) # date selections for param in ['assigned', 'started', 'finished']: @@ -138,20 +171,27 @@ def get_query_multiple_runs(self) -> Union[sa.orm.query.Query, tuple]: # custom filters if args.get('state') == 'open': - q = q.filter(db_Run.finished_at == None) + q = q.filter(db_Run.finished_at.is_(None)) q = q.join(Organization).join(Node).join(Task, db_Run.task)\ .join(Collaboration) - if args.get('node_id'): - q = q.filter(db.Node.id == args.get('node_id'))\ - .filter(db.Collaboration.id == db.Node.collaboration_id) + if 'collaboration_id' in args: + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return {'msg': 'You lack the permission to view runs for ' + f'collaboration id={args["collaboration_id"]}!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(Collaboration.id == args['collaboration_id']) # filter based on permissions if not self.r.v_glo.can(): - if self.r.v_org.can(): + if self.r.v_col.can(): col_ids = [col.id for col in auth_org.collaborations] q = q.filter(Collaboration.id.in_(col_ids)) + elif self.r.v_org.can(): + q = q.filter(Organization.id == auth_org.id) + elif self.r.v_own.can(): + q = q.filter(Task.init_user_id == g.user.id) else: return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -176,8 +216,11 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |Run|Global|View|❌|❌|View any run|\n - |Run|Organization|View|✅|✅|View the runs of your + |Run|Collaboration|View|✅|✅|View the runs of your organization's collaborations|\n + |Run|Organization|View|❌|❌|View any run from a task created by + your organization|\n + |Run|Own|View|❌|❌|View any run from a task created by you|\n Accessible to users. @@ -192,6 +235,11 @@ def get(self): schema: type: integer description: Organization id + - in: query + name: collaboration_id + schema: + type: integer + description: Collaboration id - in: query name: assigned_from schema: @@ -295,8 +343,11 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |Run|Global|View|❌|❌|View any result|\n - |Run|Organization|View|✅|✅|View the results of your + |Run|Collaboration|View|✅|✅|View the results of your organization's collaborations|\n + |Run|Organization|View|❌|❌|View any result from a task created + by your organization|\n + |Run|Own|View|❌|❌|View any result from a task created by you|\n Accessible to users. @@ -311,6 +362,11 @@ def get(self): schema: type: integer description: Organization id + - in: query + name: collaboration_id + schema: + type: integer + description: Collaboration id - in: query name: assigned_from schema: @@ -414,16 +470,15 @@ def get_single_run(self, id) -> Union[db_Run, tuple]: An algorithm Run object, or a tuple with a message and HTTP error code if the Run could not be retrieved """ - auth_org = self.obtain_auth_organization() - run = db_Run.get(id) if not run: return {'msg': f'Run id={id} not found!'}, \ HTTPStatus.NOT_FOUND - if not self.r.v_glo.can(): - c_orgs = run.task.collaboration.organizations - if not (self.r.v_org.can() and auth_org in c_orgs): - return {'msg': 'You lack the permission to do that!'}, \ + + if not self.r.can_for_org(P.VIEW, run.task.init_org_id) \ + and not (self.r.v_own.can() and + run.task.init_user_id == g.user.id): + return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED return run @@ -444,8 +499,11 @@ def get(self, id): Description|\n |--|--|--|--|--|--|\n |Run|Global|View|❌|❌|View any run|\n - |Run|Organization|View|✅|✅|View the runs of your - organizations collaborations|\n + |Run|Collaboration|View|✅|✅|View the runs of your + organization's collaborations|\n + |Run|Organization|View|❌|❌|View any run from a task created by + your organization|\n + |Run|Own|View|❌|❌|View any run from a task created by you|\n Accessible to users. @@ -600,8 +658,11 @@ def get(self, id): Description|\n |--|--|--|--|--|--|\n |Run|Global|View|❌|❌|View any result|\n - |Run|Organization|View|✅|✅|View the results of your + |Run|Collaboration|View|✅|✅|View the results of your organization's collaborations|\n + |Run|Organization|View|❌|❌|View any result from a task created + by your organization|\n + |Run|Own|View|❌|❌|View any result from a task created by you|\n Accessible to users.
vantage6-server/vantage6/server/resource/task.py+134 −38 modified@@ -11,6 +11,7 @@ from vantage6.common.task_status import TaskStatus, has_task_finished from vantage6.server import db from vantage6.server.permission import ( + RuleCollection, Scope as S, PermissionManager, Operation as P @@ -75,25 +76,32 @@ def permissions(permissions: PermissionManager) -> None: """ add = permissions.appender(module_name) - add(scope=S.GLOBAL, operation=P.VIEW, - description="view any task") - add(scope=S.ORGANIZATION, operation=P.VIEW, assign_to_container=True, - assign_to_node=True, description="view tasks of your organization") + add(scope=S.GLOBAL, operation=P.VIEW, description="view any task") + add(scope=S.COLLABORATION, operation=P.VIEW, assign_to_container=True, + assign_to_node=True, description="view tasks of your collaborations") + add(scope=S.ORGANIZATION, operation=P.VIEW, + description="view tasks that your organization initiated") + add(scope=S.OWN, operation=P.VIEW, + description="view tasks that you initiated") add(scope=S.GLOBAL, operation=P.CREATE, description="create a new task") - add(scope=S.ORGANIZATION, operation=P.CREATE, + add(scope=S.COLLABORATION, operation=P.CREATE, description=( "create a new task for collaborations in which your organization " "participates with" )) add(scope=S.GLOBAL, operation=P.DELETE, description="delete a task") + add(scope=S.COLLABORATION, operation=P.DELETE, + description="delete a task from your collaborations") add(scope=S.ORGANIZATION, operation=P.DELETE, description=( "delete a task from a collaboration in which your organization " "participates with" )) + add(scope=S.OWN, operation=P.DELETE, + description="delete tasks that you created") # ------------------------------------------------------------------------------ @@ -108,7 +116,10 @@ class TaskBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) + # permissions for the run resource are also relevant for the task + # resource as they are sometimes included + self.r_run: RuleCollection = getattr(self.permissions, 'run') class Tasks(TaskBase): @@ -125,8 +136,11 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |Task|Global|View|❌|❌|View any task|\n - |Task|Organization|View|✅|✅|View any task in your organization| + |Task|Collaboration|View|✅|✅|View any task in your collaborations| \n + |Task|Organization|View|❌|❌|View any task that your organization + created|\n + |Task|Own|View|❌|❌|View any task that you created|\n Accessible to users. @@ -241,7 +255,7 @@ def get(self): 200: description: Ok 400: - description: Non-allowed parameter values + description: Non-allowed or wrong parameter values 401: description: Unauthorized @@ -253,31 +267,105 @@ def get(self): q = g.session.query(db.Task) args = request.args - # obtain organization id auth_org_id = self.obtain_organization_id() # check permissions and apply filter if neccassary if not self.r.v_glo.can(): - if self.r.v_org.can(): + if self.r.v_col.can(): q = q.join(db.Collaboration).join(db.Organization)\ .filter(db.Collaboration.organizations.any(id=auth_org_id)) + elif self.r.v_org.can(): + q = q.join(db.Organization)\ + .filter(db.Task.init_org_id == auth_org_id) + elif self.r.v_own.can(): + q = q.filter(db.Task.init_user_id == g.user.id) else: return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED + # if results are included, check permissions on results + if self.is_included('results'): + max_scope_task = self.r.get_max_scope(P.VIEW) + if not self.r_run.has_at_least_scope(max_scope_task, P.VIEW): + max_scope_run = self.r_run.get_max_scope(P.VIEW) + return { + 'msg': 'You cannot view the results of all tasks, as you ' + f'are allowed to view tasks with scope {max_scope_task} ' + f'but you can only view results with scope {max_scope_run}' + }, HTTPStatus.UNAUTHORIZED + + if 'collaboration_id' in args: + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return {'msg': 'You lack the permission to view tasks ' + f'from collaboration {args["collaboration_id"]}!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.join(db.Collaboration).filter( + db.Collaboration.id == args['collaboration_id']) + + if 'init_org_id' in args: + if not self.r.can_for_org(P.VIEW, args['init_org_id']): + return {'msg': 'You lack the permission to view tasks ' + f'from organization id={args["init_org_id"]}!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(db.Task.init_org_id == args['init_org_id']) + + if 'init_user_id' in args: + init_user = db.User.get(args['init_user_id']) + if not init_user: + return {'msg': f'User id={args["init_user_id"]} does not ' + 'exist!'}, HTTPStatus.BAD_REQUEST + elif not self.r.can_for_org(P.VIEW, init_user.organization_id) \ + and not (self.r.v_own.can() and g.user and + init_user.id == g.user.id): + return {'msg': 'You lack the permission to view tasks ' + f'from user id={args["init_user_id"]}!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(db.Task.init_user_id == args['init_user_id']) + + if 'parent_id' in args: + parent = db.Task.get(args['parent_id']) + if not parent: + return {'msg': f'Parent task id={args["parent_id"]} does not ' + 'exist!'}, HTTPStatus.BAD_REQUEST + elif not self.r.can_for_col(P.VIEW, parent.collaboration_id): + return {'msg': 'You lack the permission to view tasks ' + 'from the collaboration that the task with parent_id=' + f'{parent.collaboration_id} belongs to!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(db.Task.parent_id == args['parent_id']) + + if 'job_id' in args: + task_in_job = q.session.query(db.Task).filter( + db.Task.job_id == args['job_id']).first() + if not task_in_job: + return {'msg': f'Job id={args["job_id"]} does not exist!'}, \ + HTTPStatus.BAD_REQUEST + elif not self.r.can_for_col(P.VIEW, task_in_job.collaboration_id): + return {'msg': 'You lack the permission to view tasks ' + 'from the collaboration that the task with job_id=' + f'{task_in_job.collaboration_id} belongs to!'}, \ + HTTPStatus.UNAUTHORIZED + q = q.filter(db.Task.job_id == args['job_id']) - # filter based on arguments - for param in ['init_org_id', 'init_user_id', 'collaboration_id', - 'parent_id', 'job_id']: - if param in args: - q = q.filter(getattr(db.Task, param) == args[param]) for param in ['name', 'image', 'description', 'status']: if param in args: q = q.filter(getattr(db.Task, param).like(args[param])) + if 'run_id' in args: + run = db.Run.get(args['run_id']) + if not run: + return {'msg': f'Run id={args["run_id"]} does not exist!'}, \ + HTTPStatus.BAD_REQUEST + elif not self.r.can_for_col(P.VIEW, run.collaboration_id): + return {'msg': 'You lack the permission to view tasks ' + 'from the collaboration that the run with id=' + f'{run.collaboration_id} belongs to!'}, \ + HTTPStatus.UNAUTHORIZED q = q.join(db.Run).filter(db.Run.id == args['run_id']) + if 'database' in args: q = q.join(db.TaskDatabase)\ .filter(db.TaskDatabase.database == args['database']) + if 'is_user_created' in args: try: user_created = int(args['is_user_created']) @@ -291,7 +379,9 @@ def get(self): f"'{args['is_user_created']}'. Should be an integer." )}, HTTPStatus.BAD_REQUEST + # order to get latest task first q = q.order_by(desc(db.Task.id)) + # paginate tasks try: page = Pagination.from_query(query=q, request=request) @@ -319,7 +409,7 @@ def post(self): Description|\n |--|--|--|--|--|--|\n |Task|Global|Create|❌|❌|Create a new task|\n - |Task|Organization|Create|❌|✅|Create a new task for a specific + |Task|Collaboration|Create|❌|✅|Create a new task for a specific collaboration in which your organization participates|\n ## Accessed as `User`\n @@ -424,12 +514,9 @@ def post(self): image = data.get('image', '') # verify permissions - if g.user: - if not self.r.c_glo.can(): - c_orgs = collaboration.organizations - if not (self.r.c_org.can() and g.user.organization in c_orgs): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if g.user and not self.r.can_for_col(P.CREATE, collaboration.id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED elif g.container: # verify that the container has permissions to create the task @@ -625,7 +712,10 @@ def get(self, id): Description|\n |--|--|--|--|--|--|\n |Task|Global|View|❌|❌|View any task|\n - |Task|Organization|View|✅|✅|View any task in your organization| + |Task|Collaboration|View|✅|✅|View any task in your collaborations| + |Task|Organization|View|❌|❌|View any task that your organization + created|\n + |Task|Own|View|❌|❌|View any task that you created|\n Accessible to users. @@ -659,19 +749,23 @@ def get(self, id): if not task: return {"msg": f"task id={id} is not found"}, HTTPStatus.NOT_FOUND - # determine the organization to which the auth belongs - auth_org = self.obtain_auth_organization() - # obtain schema schema = task_result_schema if request.args.get('include') == \ 'results' else task_schema # check permissions - if not self.r.v_glo.can(): - org_ids = [org.id for org in task.collaboration.organizations] - if not (self.r.v_org.can() and auth_org.id in org_ids): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.VIEW, task.init_org_id) \ + and not (self.r.v_own.can() and g.user and + task.init_user_id == g.user.id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED + # if results are included, check permissions for results + if self.is_included('results') and not \ + self.r_run.can_for_org(P.VIEW, task.init_org_id) \ + and not (self.r.v_own.can() and g.user and + task.init_user_id == g.user.id): + return {'msg': 'You lack the permission to view results for this ' + 'task!'}, HTTPStatus.UNAUTHORIZED return schema.dump(task, many=False), HTTPStatus.OK @@ -687,8 +781,11 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |Task|Global|Delete|❌|❌|Delete a task|\n - |Task|Organization|Delete|❌|❌|Delete a task from a collaboration + |Task|Collaboration|Delete|❌|❌|Delete a task from a collaboration in which your organization participates|\n + |Task|Organization|Delete|❌|❌|Delete a task that your organization + initiated|\n + |Task|Own|Delete|❌|❌|Delete a task you created yourself|\n Accessible to users. @@ -716,14 +813,13 @@ def delete(self, id): task = db.Task.get(id) if not task: - return {"msg": f"task id={id} not found"}, HTTPStatus.NOT_FOUND + return {"msg": f"Task id={id} not found"}, HTTPStatus.NOT_FOUND # validate permissions - if not self.r.d_glo.can(): - orgs = task.collaboration.organizations - if not (self.r.d_org.can() and g.user.organization in orgs): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not self.r.can_for_org(P.DELETE, task.init_org_id) and \ + not (self.r.d_own.can() and task.init_user_id == g.user.id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED # kill the task if it is still running if not has_task_finished(task.status):
vantage6-server/vantage6/server/resource/user.py+87 −35 modified@@ -11,9 +11,11 @@ from vantage6.server.permission import ( Scope as S, Operation as P, - PermissionManager + PermissionManager, + RuleCollection ) from vantage6.server.resource import ( + get_org_ids_from_collabs, with_user, ServicesResources ) @@ -73,20 +75,27 @@ def permissions(permissions: PermissionManager) -> None: add = permissions.appender(module_name) add(S.GLOBAL, P.VIEW, description='View any user') + add(S.COLLABORATION, P.VIEW, + description='View users from your collaboration') add(S.ORGANIZATION, P.VIEW, description='View users from your organization') add(S.GLOBAL, P.CREATE, description='Create a new user for any organization') + add(S.COLLABORATION, P.CREATE, + description='Create a new user for organizations in your ' + 'collaborations') add(S.ORGANIZATION, P.CREATE, description='Create a new user for your organization') - add(S.GLOBAL, P.EDIT, - description='Edit any user') + add(S.GLOBAL, P.EDIT, description='Edit any user') + add(S.COLLABORATION, P.EDIT, + description='Edit any user in your collaborations') add(S.ORGANIZATION, P.EDIT, description='Edit users from your organization') add(S.OWN, P.EDIT, description='Edit your own info') - add(S.GLOBAL, P.DELETE, - description='Delete any user') + add(S.GLOBAL, P.DELETE, description='Delete any user') + add(S.COLLABORATION, P.DELETE, + description='Delete any user in your collaborations') add(S.ORGANIZATION, P.DELETE, description='Delete users from your organization') add(S.OWN, P.DELETE, @@ -104,7 +113,7 @@ class UserBase(ServicesResources): def __init__(self, socketio, mail, api, permissions, config): super().__init__(socketio, mail, api, permissions, config) - self.r = getattr(self.permissions, module_name) + self.r: RuleCollection = getattr(self.permissions, module_name) class Users(UserBase): @@ -121,6 +130,8 @@ def get(self): Description|\n |--|--|--|--|--|--|\n |User|Global|View|❌|❌|View any user details|\n + |User|Collaboration|View|❌|❌|View user details from your + collaborations|\n |User|Organization|View|❌|❌|View users from your organization|\n Accessible to users. @@ -140,6 +151,11 @@ def get(self): schema: type: integer description: Organization id + - in: query + name: collaboration_id + schema: + type: integer + description: Collaboration id - in: query name: firstname schema: @@ -210,6 +226,8 @@ def get(self): description: Ok 401: description: Unauthorized + 400: + description: Invalid values provided for request parameters security: - bearerAuth: [] @@ -224,6 +242,11 @@ def get(self): if param in args: q = q.filter(getattr(db.User, param).like(args[param])) if 'organization_id' in args: + if not self.r.can_for_org(P.VIEW, args['organization_id']): + return { + 'msg': 'You lack the permission view users from the ' + f'organization with id {args["organization_id"]}!' + }, HTTPStatus.UNAUTHORIZED q = q.filter(db.User.organization_id == args['organization_id']) if 'last_seen_till' in args: q = q.filter(db.User.last_seen <= args['last_seen_till']) @@ -232,15 +255,48 @@ def get(self): # find users with a particulare role or rule assigned if 'role_id' in args: + role = db.Role.query.get(args['role_id']) + if not role: + return { + 'msg': f'Role with id={args["role_id"]} does not exist!' + }, HTTPStatus.BAD_REQUEST + elif not self.r.can_for_org(P.VIEW, role.organization_id): + return { + 'msg': 'You lack the permission view users from the ' + f'organization that role with id={role.organization_id} ' + 'belongs to!' + }, HTTPStatus.UNAUTHORIZED q = q.join(db.Permission).join(db.Role)\ .filter(db.Role.id == args['role_id']) + if 'rule_id' in args: + rule = db.Rule.query.get(args['rule_id']) + if not rule: + return { + 'msg': f'Rule with id={args["rule_id"]} does not exist!' + }, HTTPStatus.BAD_REQUEST q = q.join(db.UserPermission).join(db.Rule)\ .filter(db.Rule.id == args['rule_id']) + if 'collaboration_id' in args: + if not self.r.can_for_col(P.VIEW, args['collaboration_id']): + return { + 'msg': 'You lack the permission view all users from ' + f'collaboration {args["collaboration_id"]}!' + }, HTTPStatus.UNAUTHORIZED + q = q.filter(db.User.organization_id.in_( + get_org_ids_from_collabs(g.user, args['collaboration_id']) + )) + # check permissions and apply filter if neccessary if not self.r.v_glo.can(): - if self.r.v_org.can(): + if self.r.v_col.can(): + q = q.filter(db.User.organization_id.in_( + [org.id + for col in g.user.organization.collaborations + for org in col.organizations] + )) + elif self.r.v_org.can(): q = q.filter(db.User.organization_id == g.user.organization_id) else: return {'msg': 'You lack the permission to do that!'}, \ @@ -267,6 +323,8 @@ def post(self): Description|\n |--|--|--|--|--|--|\n |User|Global|Create|❌|❌|Create a new user|\n + |User|Collaboration|Create|❌|❌|Create a new user for any + organization in your collaborations|\n |User|Organization|Create|❌|❌|Create a new user as part of your organization|\n @@ -346,13 +404,10 @@ def post(self): if not org: return {'msg': "Organization does not exist."}, \ HTTPStatus.NOT_FOUND - else: # not-root user cant create users for other organization - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED organization_id = data['organization_id'] # check that user is allowed to create users - if not (self.r.c_glo.can() or self.r.c_org.can()): + if not self.r.can_for_org(P.CREATE, organization_id): return {'msg': 'You lack the permission to do that!'}, \ HTTPStatus.UNAUTHORIZED @@ -425,6 +480,8 @@ def get(self, id): Description|\n |-- |--|--|--|--|--|\n |User|Global|View|❌|❌|View any user details|\n + |User|Collaboration|View|❌|❌|View users from your + collaborations|\n |User|Organization|View|❌|❌|View users from your organization|\n |User|Organization|Own|❌|❌|View details about your own user|\n @@ -457,17 +514,11 @@ def get(self, id): return {"msg": f"user id={id} is not found"}, HTTPStatus.NOT_FOUND same_user = g.user.id == user.id - same_org = g.user.organization.id == user.organization_id - - # allow user to be returned if: - # 1. auth can see all users - # 2. auth can see organization users and user is within organization - # 3. auth is requesting own user details - if ( - self.r.v_glo.can() or - (self.r.v_org.can() and same_org) or - same_user - ): + + # allow user to be returned if authenticated user can view users from + # that organization or if the user is the same as the authenticated + # user. + if (same_user or self.r.can_for_org(P.VIEW, user.organization_id)): return user_schema.dump(user, many=False), HTTPStatus.OK else: return {'msg': 'You lack the permission to do that!'}, \ @@ -485,7 +536,8 @@ def patch(self, id): Description|\n |--|--|--|--|--|--|\n |User|Global|Edit|❌|❌|Edit any user|\n - |User|Organization|Edit|❌|❌|Edit any user in your organization|\n + |User|Collaboration|Edit|❌|❌|Edit users in your collaborations|\n + |User|Organization|Edit|❌|❌|Edit users in your organization|\n |User|Own|Edit|❌|❌|Edit your own user account|\n Accessible to users. @@ -557,13 +609,13 @@ def patch(self, id): return {"msg": "You cannot change your password here!"}, \ HTTPStatus.BAD_REQUEST - if not self.r.e_glo.can(): - if not (self.r.e_org.can() and user.organization == - g.user.organization): - if not (self.r.e_own.can() and user == g.user): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + # check permissions + if not (self.r.e_own.can() and user == g.user) and \ + not self.r.can_for_org(P.EDIT, user.organization_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED + # update user and check for unique constraints if data.get("username") is not None: if user.username != data["username"]: if db.User.exists("username", data["username"]): @@ -689,6 +741,8 @@ def delete(self, id): Description|\n |--|--|--|--|--|--|\n |User|Global|Delete|❌|❌|Delete any user|\n + |User|Collaboration|Delete|❌|❌|Delete users from your + collaboration|\n |User|Organization|Delete|❌|❌|Delete users from your organization|\n |User|Own|Delete|❌|❌|Delete your own account|\n @@ -727,12 +781,10 @@ def delete(self, id): return {"msg": f"user id={id} not found"}, \ HTTPStatus.NOT_FOUND - if not self.r.d_glo.can(): - if not (self.r.d_org.can() and user.organization == - g.user.organization): - if not (self.r.d_own.can() and user == g.user): - return {'msg': 'You lack the permission to do that!'}, \ - HTTPStatus.UNAUTHORIZED + if not (self.r.d_own.can() and user == g.user) and \ + not self.r.can_for_org(P.DELETE, user.organization_id): + return {'msg': 'You lack the permission to do that!'}, \ + HTTPStatus.UNAUTHORIZED # check if user created any tasks if user.created_tasks:
vantage6-server/vantage6/server/utils.py+38 −0 added@@ -0,0 +1,38 @@ +from flask import g + +from vantage6.server import db + + +def obtain_auth_collaborations() -> list[db.Collaboration]: + """ + Obtain the collaborations that the auth is part of. + + Returns + ------- + list[db.Collaboration] + List of collaborations + """ + if g.user: + return g.user.organization.collaborations + elif g.node: + return g.node.organization.collaborations + else: + return [db.Collaboration.get(g.container["collaboration_id"])] + + +def obtain_auth_organization() -> db.Organization: + """ + Obtain the organization model from the auth that is logged in. + + Returns + ------- + db.Organization + Organization model + """ + if g.user: + org_id = g.user.organization.id + elif g.node: + org_id = g.node.organization.id + else: + org_id = g.container["organization_id"] + return db.Organization.get(org_id)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-gc57-xhh5-m94rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-41882ghsaADVISORY
- github.com/pypa/advisory-database/tree/main/vulns/vantage6/PYSEC-2023-201.yamlghsaWEB
- github.com/vantage6/vantage6/blob/0682c4288f43fee5bcc72dc448cdd99bd7e57f76/docs/release_notes.rstghsax_refsource_MISCWEB
- github.com/vantage6/vantage6/commit/86564e103cbac5238ce2fe392e3357e0e8c20220ghsaWEB
- github.com/vantage6/vantage6/pull/711ghsax_refsource_MISCWEB
- github.com/vantage6/vantage6/security/advisories/GHSA-gc57-xhh5-m94rghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.