From 5fc51c31848e1723873a96324bb816b2f1edc0ca Mon Sep 17 00:00:00 2001 From: Scott Black Date: Sat, 6 Jan 2024 20:33:59 -0700 Subject: [PATCH] add policy generation from hydroshar --- app/api/subsetter/app/db.py | 14 +++- .../access_control/policy_generation.py | 44 ++++++----- .../app/routers/access_control/router.py | 78 ++++++++++++------- app/api/subsetter/app/users.py | 2 +- app/api/subsetter/config/__init__.py | 3 + 5 files changed, 87 insertions(+), 54 deletions(-) diff --git a/app/api/subsetter/app/db.py b/app/api/subsetter/app/db.py index e90f2ef2..2a9688fb 100644 --- a/app/api/subsetter/app/db.py +++ b/app/api/subsetter/app/db.py @@ -1,4 +1,5 @@ from enum import Enum +from functools import lru_cache from typing import List, Optional, Tuple import httpx @@ -9,10 +10,14 @@ from subsetter.config import get_settings -DATABASE_URL = get_settings().mongo_url -client = motor.motor_asyncio.AsyncIOMotorClient(DATABASE_URL, uuidRepresentation="standard") +client = motor.motor_asyncio.AsyncIOMotorClient(get_settings().mongo_url, uuidRepresentation="standard") db = client[get_settings().mongo_database] +client_hydroshare = motor.motor_asyncio.AsyncIOMotorClient( + get_settings().hydroshare_mongo_url, uuidRepresentation="standard" +) +db_hydroshare = client_hydroshare[get_settings().hydroshare_mongo_database] + class OAuthAccount(BaseOAuthAccount): pass @@ -80,3 +85,8 @@ async def update_submission(self, submission: Submission) -> None: async def get_user_db(): yield BeanieUserDatabase(User, OAuthAccount) + + +@lru_cache +def get_hydroshare_access_db(): + return db_hydroshare diff --git a/app/api/subsetter/app/routers/access_control/policy_generation.py b/app/api/subsetter/app/routers/access_control/policy_generation.py index 861388ba..9cd7b655 100644 --- a/app/api/subsetter/app/routers/access_control/policy_generation.py +++ b/app/api/subsetter/app/routers/access_control/policy_generation.py @@ -44,6 +44,7 @@ def refresh_minio_policy(user): ''' import copy +from typing import Dict def bucket_name(resource_id: str): @@ -52,7 +53,7 @@ def bucket_name(resource_id: str): return "subsetter-outputs" -def create_view_statements(owner, submissions) -> list: +def create_view_statements(user, views: Dict[str, list[str]]) -> list: view_statement_template_get = { "Effect": "Allow", "Action": ["s3:GetBucketLocation", "s3:GetObject"], @@ -64,35 +65,36 @@ def create_view_statements(owner, submissions) -> list: "Resource": [], "Condition": {"StringLike": {"s3:prefix": []}}, } - bucketname = bucket_name("blah") - get_resources = [f"arn:aws:s3:::{bucketname}/{owner.username}/*"] + + get_resources = [f"arn:aws:s3:::{user.username}/*"] view_statement = copy.deepcopy(view_statement_template_listing) - view_statement["Resource"] = [f"arn:aws:s3:::{bucketname}"] - view_statement["Condition"]["StringLike"]["s3:prefix"] = [f"{owner.username}/*"] + view_statement["Resource"] = [f"arn:aws:s3:::{user.username}"] + del view_statement["Condition"] list_statements = [view_statement] - for user, submission in submissions: - bucketname = bucket_name(submission.workflow_id) - get_resources.append( - f"arn:aws:s3:::{bucketname}/{user.username}/{submission.workflow_name}/{submission.workflow_id}/*" - ) + for bucket_owner, resource_paths in views.items(): + get_resources = get_resources + [ + f"arn:aws:s3:::{bucket_owner}/{resource_path}/*" for resource_path in resource_paths + ] view_statement = copy.deepcopy(view_statement_template_listing) - view_statement["Resource"] = [f"arn:aws:s3:::{bucketname}"] + view_statement["Resource"] = [f"arn:aws:s3:::{bucket_owner}"] view_statement["Condition"]["StringLike"]["s3:prefix"] = [ - f"{user.username}/{submission.workflow_name}/{submission.workflow_id}/*" + f"{resource_path}/*" for resource_path in resource_paths ] list_statements.append(view_statement) view_statement_template_get["Resource"] = get_resources return list_statements + [view_statement_template_get] -# def create_edit_owner_statements(resource_ids: list[str]) -> list: -# edit_statement_template = {"Effect": "Allow", "Action": ["s3:*"], "Resource": []} -# edit_statement_template["Resource"] = [ -# f"arn:aws:s3:::{bucket_name(resource_id)}/hydroshare/{resource_id}/*" for resource_id in resource_ids -# ] -# return [edit_statement_template] +def create_edit_statements(user, edits: Dict[str, list[str]]) -> list: + edit_statement_template = {"Effect": "Allow", "Action": ["s3:*"], "Resource": []} + resources = [] + for bucket_owner, resource_paths in edits.items(): + resources = resources + [f"arn:aws:s3:::{bucket_owner}/{resource_path}/*" for resource_path in resource_paths] + edit_statement_template["Resource"] = resources + return [edit_statement_template] -def minio_policy(user, view_submissions): - view_statements = create_view_statements(user, view_submissions) - return {"Version": "2012-10-17", "Statement": view_statements} +def minio_policy(user, owners: Dict[str, list[str]], edits: Dict[str, list[str]], views: Dict[str, list[str]]): + statements = create_view_statements(user, views) + statements = statements + create_edit_statements(user, edits) + return {"Version": "2012-10-17", "Statement": statements} diff --git a/app/api/subsetter/app/routers/access_control/router.py b/app/api/subsetter/app/routers/access_control/router.py index a48c56ad..19aa9089 100644 --- a/app/api/subsetter/app/routers/access_control/router.py +++ b/app/api/subsetter/app/routers/access_control/router.py @@ -2,54 +2,72 @@ import os import subprocess import tempfile +from typing import Dict -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from pydantic import BaseModel -from subsetter.app.db import Submission, User +from subsetter.app.db import User, get_hydroshare_access_db from subsetter.app.routers.access_control.policy_generation import minio_policy from subsetter.app.users import current_active_user router = APIRouter() -class ShareWorkflowBody(BaseModel): +class UserAccess(BaseModel): + owner: list[str] + edit: list[str] + view: list[str] + + +class MinioUserResourceAccess(BaseModel): + owners: list[str] + resource_id: str + minio_resource_url: str + + +class MinioUserAccess(BaseModel): + owner: list[MinioUserResourceAccess] + edit: list[MinioUserResourceAccess] + view: list[MinioUserResourceAccess] + + +class UserPrivilege(BaseModel): username: str - workflow_id: str + all: UserAccess + minio: MinioUserAccess -@router.post('/policy/add') -async def share_workflow_with_user(share_params: ShareWorkflowBody, user: User = Depends(current_active_user)): - submission: Submission = user.get_submission(share_params.workflow_id) - if submission: - submission.add_user(share_params.username) - await user.update_submission(submission) - return user - else: - return HTTPException(status_code=400) +def check_owners_in_bucket_path(resource_access: MinioUserResourceAccess): + for owner in resource_access.owners: + if f"/browser/{owner}/" in resource_access.minio_resource_url: + return owner + return None -@router.delete('/policy/remove') -async def unshare_workflow_with_user(share_params: ShareWorkflowBody, user: User = Depends(current_active_user)): - submission: Submission = user.get_submission(share_params.workflow_id) - if submission: - submission.remove_user(share_params.username) - await user.update_submission(submission) - return user - else: - return HTTPException(status_code=400) +def sort_privileges(user_accesses: list[MinioUserResourceAccess]): + authorized_users = {} + for user_access in user_accesses: + bucket_owner = check_owners_in_bucket_path(user_access) + if bucket_owner: + resource_path = user_access.minio_resource_url.split(f"{bucket_owner}/", 1)[-1] + authorized_users.setdefault(bucket_owner, []).append(resource_path) + return authorized_users @router.get('/policy') async def generate_user_policy(user: User = Depends(current_active_user)): - users = await User.find({"submissions.view_users": user.username}).to_list() - # this should be rewritten to query all on the db, but I don't have time to figure that out now - matching_submissions = [] - for u in users: - for submission in u.submissions: - if user.username in submission.view_users: - matching_submissions.append((u, submission)) - return minio_policy(user, matching_submissions) + hydroshare_access_db = get_hydroshare_access_db() + user_privilege = await hydroshare_access_db.userprivileges.find_one({"username": user.username}) + user_privilege: UserPrivilege = UserPrivilege(**user_privilege) + + # Check Authorization + minio_user_access: MinioUserAccess = user_privilege.minio + authorized_owners: Dict[str, list[str]] = sort_privileges(minio_user_access.owner) + authorized_edits: Dict[str, list[str]] = sort_privileges(minio_user_access.edit) + authorized_views: Dict[str, list[str]] = sort_privileges(minio_user_access.view) + + return minio_policy(user, authorized_owners, authorized_edits, authorized_views) @router.get('/profile') diff --git a/app/api/subsetter/app/users.py b/app/api/subsetter/app/users.py index 7714b6c9..5ebd85a6 100644 --- a/app/api/subsetter/app/users.py +++ b/app/api/subsetter/app/users.py @@ -11,7 +11,7 @@ from httpx_oauth.oauth2 import OAuth2 from subsetter.app.db import User, get_user_db -from subsetter.config import get_settings, get_minio_client +from subsetter.config import get_minio_client, get_settings SECRET = "SECRET" diff --git a/app/api/subsetter/config/__init__.py b/app/api/subsetter/config/__init__.py index ccbccb4c..c65f1ef4 100644 --- a/app/api/subsetter/config/__init__.py +++ b/app/api/subsetter/config/__init__.py @@ -16,6 +16,9 @@ class Settings(BaseSettings): mongo_url: str mongo_database: str + hydroshare_mongo_url: str + hydroshare_mongo_database: str + oauth2_client_id: str oauth2_client_secret: str oauth2_redirect_url: str