diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c187c08484e0ecd7ebb31d73ed15842cc17afa6a..8f894d6898c2a7238caa5fee74f29acefeb17bdd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: files: app args: [--check] - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.288' + rev: 'v0.0.289' hooks: - id: ruff - repo: https://github.com/PyCQA/isort diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 7132caeba5f21524c9a0a110ce6a9d29fe3bf158..cf6b14eb570d506cdbcd30c04dad78f547475957 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -9,7 +9,7 @@ from app.api.utils import check_repo, upload_scm_file from app.core.config import settings from app.crud import CRUDWorkflow, CRUDWorkflowVersion from app.crud.crud_workflow_mode import CRUDWorkflowMode -from app.git_repository import GitHubRepository, build_repository +from app.git_repository import build_repository from app.schemas.workflow import WorkflowIn, WorkflowOut, WorkflowStatistic, WorkflowUpdate from app.schemas.workflow_version import WorkflowVersion as WorkflowVersionSchema from app.scm import SCM, Provider @@ -136,12 +136,7 @@ async def create_workflow( try: # Build a git repository object based on the repository url - repo = build_repository( - workflow.repository_url, workflow.git_commit_hash, token=workflow.token, username=workflow.username - ) - # if the workflow is in a private GitHub repo and the username is None, then take the username from the url - if repo.token is not None and workflow.username is None and isinstance(repo, GitHubRepository): - workflow.username = repo.user + repo = build_repository(workflow.repository_url, workflow.git_commit_hash, token=workflow.token) except NotImplementedError: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Supplied Git Repository is not supported") @@ -384,7 +379,6 @@ async def update_workflow( repo = build_repository( workflow.repository_url, version_update.git_commit_hash, - username=workflow.credentials_username, token=workflow.credentials_token, ) diff --git a/app/api/endpoints/workflow_credentials.py b/app/api/endpoints/workflow_credentials.py index a8d109051a8714b99a2e2296062c3fd6717c3937..69a3fad214625dd76ab7fc1fdfb04213e83b06ca 100644 --- a/app/api/endpoints/workflow_credentials.py +++ b/app/api/endpoints/workflow_credentials.py @@ -7,7 +7,7 @@ from app.api.utils import check_repo, upload_scm_file from app.core.config import settings from app.crud.crud_workflow import CRUDWorkflow from app.crud.crud_workflow_version import CRUDWorkflowVersion -from app.git_repository import GitHubRepository, build_repository +from app.git_repository import build_repository from app.schemas.workflow import WorkflowCredentialsIn from app.scm import SCM, Provider @@ -17,40 +17,34 @@ workflow_authorization = AuthorizationDependency(resource="workflow") Authorization = Annotated[Callable[[str], Awaitable[Any]], Depends(workflow_authorization)] -@router.delete("", status_code=status.HTTP_204_NO_CONTENT, summary="Delete the credentials of a workflow") -async def delete_workflow_credentials( - workflow: CurrentWorkflow, - db: DBSession, - current_user: CurrentUser, - authorization: Authorization, - s3: S3Service, -) -> None: +@router.get("", status_code=status.HTTP_200_OK, summary="Get the credentials of a workflow") +async def get_workflow_credentials( + workflow: CurrentWorkflow, current_user: CurrentUser, authorization: Authorization +) -> WorkflowCredentialsIn: """ - Delete the credentials for the repository of a workflow.\n - Permission "workflow:delete" required. + Get the credentials for the repository of a workflow. Only the developer of a workflow can do this.\n + Permission "workflow:update" required. \f Parameters ---------- workflow : clowmdb.models.Workflow Workflow with given ID. Dependency Injection. - db : sqlalchemy.ext.asyncio.AsyncSession. - Async database session to perform query on. Dependency Injection. current_user : clowmdb.models.User Current user. Dependency Injection. authorization : Callable[[str], Awaitable[Any]] Async function to ask the auth service for authorization. Dependency Injection. - s3 : boto3_type_annotations.s3.ServiceResource - S3 Service to perform operations on buckets in Ceph. Dependency Injection. Returns ------- workflow : app.schemas.workflow.WorkflowOut Workflow with existing ID """ - rbac_operation = "delete" if workflow.developer_id == current_user.uid else "delete_any" - await authorization(rbac_operation) - s3.Bucket(settings.PARAMS_BUCKET).Object(f"{workflow.workflow_id.hex}.scm").delete() - await CRUDWorkflow.update_credentials(db, workflow.workflow_id, token=None, username=None) + await authorization("update") + if current_user.uid != workflow.developer_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Only the developer can retrieve the repository credentials" + ) + return WorkflowCredentialsIn(token=workflow.credentials_token) @router.put("", status_code=status.HTTP_200_OK, summary="Update the credentials of a workflow") @@ -100,14 +94,8 @@ async def update_workflow_credentials( latest_version = await CRUDWorkflowVersion.get_latest(db, workflow.workflow_id, published=False) # Build a git repository object based on the repository url repo = build_repository( - workflow.repository_url, - latest_version.git_commit_hash, # type: ignore[union-attr] - token=credentials.token, - username=credentials.username, + workflow.repository_url, latest_version.git_commit_hash, token=credentials.token # type: ignore[union-attr] ) - # if the workflow is in a private GitHub repo and the username is None, then take the username from the url - if credentials.username is None and isinstance(repo, GitHubRepository): - credentials.username = repo.user await check_repo(repo=repo, client=client) scm_provider = Provider.from_repo(repo=repo, name=f"repo{workflow.workflow_id.hex}") @@ -117,6 +105,40 @@ async def update_workflow_credentials( scm=SCM(providers=[scm_provider]), # type: ignore[list-item] scm_file_id=workflow.workflow_id.hex, ) - await CRUDWorkflow.update_credentials( - db, workflow.workflow_id, token=credentials.token, username=credentials.username - ) + await CRUDWorkflow.update_credentials(db, workflow.workflow_id, token=credentials.token) + + +@router.delete("", status_code=status.HTTP_204_NO_CONTENT, summary="Delete the credentials of a workflow") +async def delete_workflow_credentials( + workflow: CurrentWorkflow, + db: DBSession, + current_user: CurrentUser, + authorization: Authorization, + s3: S3Service, +) -> None: + """ + Delete the credentials for the repository of a workflow.\n + Permission "workflow:delete" required. + \f + Parameters + ---------- + workflow : clowmdb.models.Workflow + Workflow with given ID. Dependency Injection. + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. Dependency Injection. + current_user : clowmdb.models.User + Current user. Dependency Injection. + authorization : Callable[[str], Awaitable[Any]] + Async function to ask the auth service for authorization. Dependency Injection. + s3 : boto3_type_annotations.s3.ServiceResource + S3 Service to perform operations on buckets in Ceph. Dependency Injection. + + Returns + ------- + workflow : app.schemas.workflow.WorkflowOut + Workflow with existing ID + """ + rbac_operation = "delete" if workflow.developer_id == current_user.uid else "delete_any" + await authorization(rbac_operation) + s3.Bucket(settings.PARAMS_BUCKET).Object(f"{workflow.workflow_id.hex}.scm").delete() + await CRUDWorkflow.update_credentials(db, workflow.workflow_id, token=None) diff --git a/app/api/endpoints/workflow_execution.py b/app/api/endpoints/workflow_execution.py index 8c16324a38d1043df0d9684bdc2e601ce089106c..4af48311dc406f7f1f9a13297618195fae4818e9 100644 --- a/app/api/endpoints/workflow_execution.py +++ b/app/api/endpoints/workflow_execution.py @@ -24,7 +24,7 @@ from app.api.utils import ( ) from app.core.config import settings from app.crud import CRUDWorkflowExecution, CRUDWorkflowVersion -from app.git_repository import GitHubRepository, build_repository +from app.git_repository import build_repository from app.schemas.workflow_execution import DevWorkflowExecutionIn, WorkflowExecutionIn, WorkflowExecutionOut from app.scm import SCM, Provider from app.slurm.slurm_rest_client import SlurmClient @@ -216,11 +216,7 @@ async def start_arbitrary_workflow( workflow_execution_in.repository_url, workflow_execution_in.git_commit_hash, token=workflow_execution_in.token, - username=workflow_execution_in.username, ) - # if the workflow is in a private GitHub repo and the username is None, then take the username from the url - if repo.token is not None and workflow_execution_in.username is None and isinstance(repo, GitHubRepository): - workflow_execution_in.username = repo.user except NotImplementedError: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Supplied Git Repository is not supported") diff --git a/app/api/endpoints/workflow_version.py b/app/api/endpoints/workflow_version.py index 4c667aeb5d0b644e261a3a37e2f67791330b610e..cf46afcb32b79e4953abb41ed9f7263f766a01fd 100644 --- a/app/api/endpoints/workflow_version.py +++ b/app/api/endpoints/workflow_version.py @@ -277,7 +277,6 @@ async def download_workflow_documentation( workflow.repository_url, workflow_version.git_commit_hash, workflow.credentials_token, - workflow.credentials_username, ) path = document.standard_path() if mode_id is not None: diff --git a/app/crud/crud_workflow.py b/app/crud/crud_workflow.py index 89842fd4dbaecda81cd5d946ab693911cda7416f..4e3d5214ebfd61c17df0ca1e6eaf17e69ecd483f 100644 --- a/app/crud/crud_workflow.py +++ b/app/crud/crud_workflow.py @@ -70,7 +70,7 @@ class CRUDWorkflow: @staticmethod async def update_credentials( - db: AsyncSession, workflow_id: Union[UUID, bytes], token: Optional[str] = None, username: Optional[str] = None + db: AsyncSession, workflow_id: Union[UUID, bytes], token: Optional[str] = None ) -> None: """ Delete a workflow. @@ -83,15 +83,9 @@ class CRUDWorkflow: UID of a workflow token : str | None Token to save in the database. If None, the token in the database gets deleted - username : str | None - Username to save in the database. If None, the token in the database gets deleted """ wid = workflow_id.bytes if isinstance(workflow_id, UUID) else workflow_id - stmt = ( - update(Workflow) - .where(Workflow._workflow_id == wid) - .values(credentials_token=token, credentials_username=username) - ) + stmt = update(Workflow).where(Workflow._workflow_id == wid).values(credentials_token=token) await db.execute(stmt) await db.commit() @@ -204,7 +198,6 @@ class CRUDWorkflow: short_description=workflow.short_description, developer_id=developer, credentials_token=workflow.token, - credentials_username=workflow.username, ) db.add(workflow_db) await db.commit() diff --git a/app/git_repository/__init__.py b/app/git_repository/__init__.py index 05faf5f326a63b9bb75c559f63565e52905ecd18..ed6bf0edaea75d12de37da86a970e3e4b9760c56 100644 --- a/app/git_repository/__init__.py +++ b/app/git_repository/__init__.py @@ -8,9 +8,7 @@ from app.git_repository.github import GitHubRepository from app.git_repository.gitlab import GitlabRepository -def build_repository( - url: AnyHttpUrl, git_commit_hash: str, token: Optional[str] = None, username: Optional[str] = None -) -> GitRepository: +def build_repository(url: AnyHttpUrl, git_commit_hash: str, token: Optional[str] = None) -> GitRepository: """ Build the right git repository object based on the url @@ -21,9 +19,7 @@ def build_repository( git_commit_hash : str Pin down git commit hash token : str | None - - username : str | None - + Token to access a private git repository Returns ------- repo : GitRepository @@ -31,7 +27,7 @@ def build_repository( """ domain = urlparse(str(url)).netloc if "github" in domain: - return GitHubRepository(url=str(url), git_commit_hash=git_commit_hash, token=token, user=username) + return GitHubRepository(url=str(url), git_commit_hash=git_commit_hash, token=token) elif "gitlab" in domain: return GitlabRepository(url=str(url), git_commit_hash=git_commit_hash, token=token) raise NotImplementedError("Unknown Git repository Provider") diff --git a/app/git_repository/github.py b/app/git_repository/github.py index 8c8a389b37a5397232f9452eb106cad57674313b..5a890dd9700359ea7d73878439ad6f4b94b85eb7 100644 --- a/app/git_repository/github.py +++ b/app/git_repository/github.py @@ -33,14 +33,14 @@ class GitHubRepository(GitRepository): @cached_property def request_auth(self) -> Optional[BasicAuth]: if self._token is not None: - return BasicAuth(username=self.user, password=self._token) + return BasicAuth(username=self.account, password=self._token) return None @cached_property def request_headers(self) -> Dict[str, str]: return {"Accept": "application/vnd.github.object+json", "X-GitHub-Api-Version": "2022-11-28"} - def __init__(self, url: str, git_commit_hash: str, token: Optional[str] = None, user: Optional[str] = None): + def __init__(self, url: str, git_commit_hash: str, token: Optional[str] = None): parse_result = urlparse(url) path_parts = parse_result.path[1:].split("/") self.url = url @@ -48,7 +48,6 @@ class GitHubRepository(GitRepository): self.repository = path_parts[1] self.commit = git_commit_hash self._token = token - self.user = user if user else self.account def check_file_url(self, filepath: str) -> AnyHttpUrl: return AnyHttpUrl.build( diff --git a/app/schemas/workflow.py b/app/schemas/workflow.py index 9359b7129eaa601cf1ccea443084412b6f2a5be6..4f4b6f35f95aa1967eee6847f828deeab14d4d01 100644 --- a/app/schemas/workflow.py +++ b/app/schemas/workflow.py @@ -58,12 +58,6 @@ class WorkflowIn(_BaseWorkflow): examples=["vnpau89avpa48iunga984gh9h89pvhj"], max_length=128, ) - username: Optional[str] = Field( - default=None, - description="Username belonging to the token. If not provided, it will be parsed from the git repository URL", - examples=["bilbobaggins"], - max_length=128, - ) modes: Optional[List[WorkflowModeIn]] = Field( default=None, max_length=10, description="List of modes with alternative entrypoint the new workflow has" ) @@ -125,12 +119,6 @@ class WorkflowCredentialsIn(BaseModel): examples=["vnpau89avpa48iunga984gh9h89pvhj"], max_length=128, ) - username: Optional[str] = Field( - default=None, - description="Username belonging to the token. If not provided, it will be parsed from the git repository URL", - examples=["bilbobaggins"], - max_length=128, - ) class WorkflowUpdate(BaseModel): diff --git a/app/schemas/workflow_execution.py b/app/schemas/workflow_execution.py index b1086e54c77f3f8b9d6181525f335646fdec9525..80b44a4c6d57ced71bae2a72153c166b676b5fa4 100644 --- a/app/schemas/workflow_execution.py +++ b/app/schemas/workflow_execution.py @@ -111,12 +111,6 @@ class DevWorkflowExecutionIn(BaseModel): examples=["vnpau89avpa48iunga984gh9h89pvhj"], max_lenght=128, ) - username: Optional[str] = Field( - default=None, - description="Username belonging to the token. If not provided, it will be parsed from the git repository URL", - examples=["bilbobaggins"], - max_length=128, - ) mode: Optional[WorkflowModeIn] = Field( default=None, description="Mode of the workflow with an alternative entrypoint" ) diff --git a/app/scm/scm.py b/app/scm/scm.py index 36dbc929f24995e947732cd4a4b62e9395adddb4..fc1c93ede167b16b9e214a08dd9bb4af276a8203 100644 --- a/app/scm/scm.py +++ b/app/scm/scm.py @@ -115,7 +115,7 @@ class Provider: if repo.token is None: return None elif isinstance(repo, GitHubRepository): - return GitHubProvider(user=repo.user, password=repo.token) + return GitHubProvider(user=repo.account, password=repo.token) elif isinstance(repo, GitlabRepository): return GitlabProvider( name=name, password=repo.token, server=str(AnyHttpUrl.build(scheme="https", host=repo.domain))[:-1] diff --git a/app/tests/api/test_workflow.py b/app/tests/api/test_workflow.py index 47058d463e86ba40dd046fce54a47dcf549de140..f1e8a7dc65140f8cfad248a591561e5931469a78 100644 --- a/app/tests/api/test_workflow.py +++ b/app/tests/api/test_workflow.py @@ -170,7 +170,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): obj.delete() @pytest.mark.asyncio - async def test_create_workflow_with_private_github_with_username( + async def test_create_workflow_with_private_github( self, client: AsyncClient, db: AsyncSession, @@ -178,7 +178,7 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): mock_s3_service: MockS3ServiceResource, ) -> None: """ - Test for successfully creating a workflow with a private GitHub repository with a provided username. + Test for successfully creating a workflow with a private GitHub repository. Parameters ---------- @@ -198,7 +198,6 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): short_description=random_lower_string(65), repository_url="https://github.de/example-user/example", token=token, - username="bilbobaggins", ).model_dump() response = await client.post(self.base_path, json=workflow, headers=random_user.auth_headers) assert response.status_code == status.HTTP_201_CREATED @@ -211,71 +210,6 @@ class TestWorkflowRoutesCreate(_TestWorkflowRoutes): assert db_workflow is not None # Check if token is saved assert db_workflow.credentials_token == token - assert db_workflow.credentials_username == "bilbobaggins" - - # Download SCM file in PARAMS_BUCKET that should be created - scm_file_name = f"{db_workflow.workflow_id.hex}.scm" - assert scm_file_name in mock_s3_service.Bucket(settings.PARAMS_BUCKET).objects.all_keys() - obj = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(scm_file_name) - with BytesIO() as f: - obj.download_fileobj(f) - f.seek(0) - scm = SCM.deserialize(f) - - # Check content of SCM file - assert len(scm.providers) == 1 - provider = scm.providers[0] - assert provider.password == token - assert provider.name == "github" - assert provider.user == "bilbobaggins" - - # Cleanup after test - await db.execute(delete(Workflow).where(Workflow._workflow_id == db_workflow.workflow_id.bytes)) - await db.commit() - obj.delete() - - @pytest.mark.asyncio - async def test_create_workflow_with_private_github_without_username( - self, - client: AsyncClient, - db: AsyncSession, - random_user: UserWithAuthHeader, - mock_s3_service: MockS3ServiceResource, - ) -> None: - """ - Test for successfully creating a workflow with a private GitHub repository without a provided username. - - Parameters - ---------- - db : sqlalchemy.ext.asyncio.AsyncSession. - Async database session to perform query on. - client : httpx.AsyncClient - HTTP Client to perform the request on. - random_user : app.tests.utils.user.UserWithAuthHeader - Random user for testing. - mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource - Mock S3 Service to manipulate objects. - """ - token = random_lower_string(20) - workflow = WorkflowIn( - git_commit_hash=random_hex_string(), - name=random_lower_string(10), - short_description=random_lower_string(65), - repository_url="https://github.de/example-user/example", - token=token, - ).model_dump() - response = await client.post(self.base_path, json=workflow, headers=random_user.auth_headers) - assert response.status_code == status.HTTP_201_CREATED - created_workflow = response.json() - assert created_workflow["private"] - - # Check if workflow is created in database - stmt = select(Workflow).where(Workflow._workflow_id == UUID(hex=created_workflow["workflow_id"]).bytes) - db_workflow = await db.scalar(stmt) - assert db_workflow is not None - # Check if token is saved - assert db_workflow.credentials_token == token - assert db_workflow.credentials_username == "example-user" # Download SCM file in PARAMS_BUCKET that should be created scm_file_name = f"{db_workflow.workflow_id.hex}.scm" diff --git a/app/tests/api/test_workflow_credentials.py b/app/tests/api/test_workflow_credentials.py index 9f031125807e87ee8f2ecba384efec90342c1349..b028264f82639b283d13cc544e3a8f238e0bcdc1 100644 --- a/app/tests/api/test_workflow_credentials.py +++ b/app/tests/api/test_workflow_credentials.py @@ -46,7 +46,7 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): random_workflow : app.schemas.workflow.WorkflowOut Random workflow for testing. """ - credentials = WorkflowCredentialsIn(token=random_lower_string(15), username=random_lower_string(15)) + credentials = WorkflowCredentialsIn(token=random_lower_string(15)) response = await client.put( "/".join([self.base_path, str(random_workflow.workflow_id), "credentials"]), json=credentials.model_dump(), @@ -59,7 +59,6 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): assert db_workflow is not None assert db_workflow.credentials_token == credentials.token - assert db_workflow.credentials_username == credentials.username scm_file = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(f"{db_workflow.workflow_id.hex}.scm") with BytesIO() as f: @@ -98,7 +97,7 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): random_private_workflow : app.schemas.workflow.WorkflowOut Random private workflow for testing. """ - credentials = WorkflowCredentialsIn(token=random_lower_string(15), username=random_lower_string(15)) + credentials = WorkflowCredentialsIn(token=random_lower_string(15)) response = await client.put( "/".join([self.base_path, str(random_private_workflow.workflow_id), "credentials"]), json=credentials.model_dump(), @@ -111,7 +110,6 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): assert db_workflow is not None assert db_workflow.credentials_token == credentials.token - assert db_workflow.credentials_username == credentials.username scm_file = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(f"{db_workflow.workflow_id.hex}.scm") with BytesIO() as f: @@ -123,7 +121,7 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): assert scm.providers[0].password == credentials.token @pytest.mark.asyncio - async def test_update_workflow_credentials_without_username( + async def test_update_workflow_credentials( self, db: AsyncSession, client: AsyncClient, @@ -195,7 +193,7 @@ class TestWorkflowCredentialsRoutesUpdate(_TestWorkflowCredentialRoutes): random_private_workflow : app.schemas.workflow.WorkflowOut Random private workflow for testing. """ - credentials = WorkflowCredentialsIn(token=random_lower_string(15), username=random_lower_string(15)) + credentials = WorkflowCredentialsIn(token=random_lower_string(15)) response = await client.put( "/".join([self.base_path, str(random_private_workflow.workflow_id), "credentials"]), json=credentials.model_dump(), @@ -241,7 +239,6 @@ class TestWorkflowCredentialsRoutesDelete(_TestWorkflowCredentialRoutes): assert db_workflow is not None assert db_workflow.credentials_token is None - assert db_workflow.credentials_username is None scm_file = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(f"{db_workflow.workflow_id.hex}.scm") with pytest.raises(ClientError): @@ -284,9 +281,72 @@ class TestWorkflowCredentialsRoutesDelete(_TestWorkflowCredentialRoutes): assert db_workflow is not None assert db_workflow.credentials_token is None - assert db_workflow.credentials_username is None scm_file = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(f"{db_workflow.workflow_id.hex}.scm") with pytest.raises(ClientError): with BytesIO() as f: scm_file.download_fileobj(f) + + +class TestWorkflowCredentialsRoutesGet(_TestWorkflowCredentialRoutes): + @pytest.mark.asyncio + async def test_get_workflow_credentials( + self, + db: AsyncSession, + client: AsyncClient, + random_user: UserWithAuthHeader, + random_private_workflow: WorkflowOut, + ) -> None: + """ + Test for getting the credentials on a workflow as the developer. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_private_workflow : app.schemas.workflow.WorkflowOut + Random private workflow for testing. + """ + response = await client.get( + "/".join([self.base_path, str(random_private_workflow.workflow_id), "credentials"]), + headers=random_user.auth_headers, + ) + assert response.status_code == status.HTTP_200_OK + + stmt = select(Workflow).where(Workflow._workflow_id == random_private_workflow.workflow_id.bytes) + db_workflow = await db.scalar(stmt) + assert db_workflow is not None + + assert response.json()["token"] == db_workflow.credentials_token + + @pytest.mark.asyncio + async def test_get_workflow_credentials_as_foreign_user( + self, + db: AsyncSession, + client: AsyncClient, + random_second_user: UserWithAuthHeader, + random_private_workflow: WorkflowOut, + ) -> None: + """ + Test for getting the credentials on a workflow as a user who is not the developer. + + Parameters + ---------- + db : sqlalchemy.ext.asyncio.AsyncSession. + Async database session to perform query on. + client : httpx.AsyncClient + HTTP Client to perform the request on. + random_second_user : app.tests.utils.user.UserWithAuthHeader + Random user for testing. + random_private_workflow : app.schemas.workflow.WorkflowOut + Random private workflow for testing. + """ + response = await client.get( + "/".join([self.base_path, str(random_private_workflow.workflow_id), "credentials"]), + headers=random_second_user.auth_headers, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN diff --git a/app/tests/api/test_workflow_execution.py b/app/tests/api/test_workflow_execution.py index 1d87f67cc1ffc181dc1b3a38c0b8434afe613cfb..3f360224a57803d0670fdd09b345dbd9c74a645e 100644 --- a/app/tests/api/test_workflow_execution.py +++ b/app/tests/api/test_workflow_execution.py @@ -821,7 +821,7 @@ class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): scm_obj.delete() @pytest.mark.asyncio - async def test_start_dev_workflow_execution_from_private_github_without_username( + async def test_start_dev_workflow_execution_from_private_github( self, client: AsyncClient, random_user: UserWithAuthHeader, @@ -829,7 +829,7 @@ class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_slurm_cluster: MockSlurmCluster, ) -> None: """ - Test for starting a workflow execution with an arbitrary private GitHub repository without a provided username. + Test for starting a workflow execution with an arbitrary private GitHub repository. Parameters ---------- @@ -895,82 +895,6 @@ class TestDevWorkflowExecutionRoutesCreate(_TestWorkflowExecutionRoutes): mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(params_file_name).delete() scm_obj.delete() - @pytest.mark.asyncio - async def test_start_dev_workflow_execution_from_private_github_with_username( - self, - client: AsyncClient, - random_user: UserWithAuthHeader, - mock_s3_service: MockS3ServiceResource, - mock_slurm_cluster: MockSlurmCluster, - ) -> None: - """ - Test for starting a workflow execution with an arbitrary private GitHub repository with a provided username. - - Parameters - ---------- - client : httpx.AsyncClient - HTTP Client to perform the request on. - random_user : app.tests.utils.user.UserWithAuthHeader - Random user for testing. - mock_s3_service : app.tests.mocks.mock_s3_resource.MockS3ServiceResource - Mock S3 Service to manipulate objects. - mock_slurm_cluster : app.tests.mocks.mock_slurm_cluster.MockSlurmCluster - Mock Slurm cluster to inspect submitted jobs. - """ - token = random_lower_string(15) - execution_in = DevWorkflowExecutionIn( - git_commit_hash=random_hex_string(), - repository_url="https://github.com/example-user/example", - parameters={}, - token=token, - username="bilbobaggins", - ) - response = await client.post( - f"{self.base_path}/arbitrary", headers=random_user.auth_headers, json=execution_in.model_dump() - ) - assert response.status_code == status.HTTP_201_CREATED - execution_response = response.json() - assert execution_response["user_id"] == random_user.user.uid - assert execution_response["status"] == WorkflowExecution.WorkflowExecutionStatus.PENDING - - execution_id = UUID(hex=execution_response["execution_id"]) - - # Check if params file is created - params_file_name = f"params-{execution_id.hex }.json" - assert params_file_name in mock_s3_service.Bucket(settings.PARAMS_BUCKET).objects.all_keys() - - # Check if SCM file is created - scm_file_name = f"{execution_id.hex}.scm" - assert scm_file_name in mock_s3_service.Bucket(settings.PARAMS_BUCKET).objects.all_keys() - scm_obj = mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(scm_file_name) - with BytesIO() as f: - scm_obj.download_fileobj(f) - f.seek(0) - scm = SCM.deserialize(f) - - # Check content of SCM file - assert len(scm.providers) == 1 - provider = scm.providers[0] - assert provider.password == token - assert provider.name == "github" - assert provider.user == "bilbobaggins" - - job = mock_slurm_cluster.get_job_by_name(str(execution_id)) - assert job is not None - assert job["job"]["environment"]["TOWER_WORKSPACE_ID"] == execution_id.hex[:16] - assert "NXF_SCM_FILE" in job["job"]["environment"].keys() - assert job["job"]["environment"]["NXF_SCM_FILE"] == f"{settings.PARAMS_BUCKET_MOUNT_PATH}/{scm_file_name}" - - nextflow_script = job["script"] - assert "-hub github" in nextflow_script - assert "-entry" not in nextflow_script - assert f"-revision {execution_in.git_commit_hash}" in nextflow_script - assert f"run {execution_in.repository_url}" in nextflow_script - - # Clean up after test - mock_s3_service.Bucket(settings.PARAMS_BUCKET).Object(params_file_name).delete() - scm_obj.delete() - @pytest.mark.asyncio async def test_start_dev_workflow_execution_with_unknown_repository( self, diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 934b11af77bda8ca816a5d803a90d259a74283e2..4fa2b806b68adc77efd39ab1b50da1e08138cbba 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -227,19 +227,16 @@ async def random_private_workflow( """ # Update credentials in Database token = random_lower_string(15) - username = random_lower_string(15) await db.execute( update(Workflow) .where(Workflow._workflow_id == random_workflow.workflow_id.bytes) - .values(credentials_token=token, credentials_username=username) + .values(credentials_token=token) ) await db.commit() # Upload SCM file to PARAMS_BUCKET scm_provider = Provider.from_repo( - build_repository( - random_workflow.repository_url, random_workflow_version.git_commit_hash, token=token, username=username - ), + build_repository(random_workflow.repository_url, random_workflow_version.git_commit_hash, token=token), name=f"repo{random_workflow.workflow_id.hex}", ) assert scm_provider is not None diff --git a/app/tests/crud/test_workflow.py b/app/tests/crud/test_workflow.py index dfa5783b0e18b9ce6c2fce4f1be799444e9c302f..47b2a07c092cb0cc5d4f48af17d3c2d688cbbdf2 100644 --- a/app/tests/crud/test_workflow.py +++ b/app/tests/crud/test_workflow.py @@ -198,16 +198,12 @@ class TestWorkflowCRUDCreate: Random workflow for testing. """ token = random_lower_string(15) - username = random_lower_string(15) - await CRUDWorkflow.update_credentials( - db, workflow_id=random_workflow.workflow_id, token=token, username=username - ) + await CRUDWorkflow.update_credentials(db, workflow_id=random_workflow.workflow_id, token=token) stmt = select(Workflow).where(Workflow._workflow_id == random_workflow.workflow_id.bytes) workflow = await db.scalar(stmt) assert workflow is not None assert workflow.credentials_token == token - assert workflow.credentials_username == username class TestWorkflowCRUDDelete: @@ -247,12 +243,9 @@ class TestWorkflowCRUDDelete: random_private_workflow : app.schemas.workflow.WorkflowOut Random private workflow for testing. """ - await CRUDWorkflow.update_credentials( - db, workflow_id=random_private_workflow.workflow_id, token=None, username=None - ) + await CRUDWorkflow.update_credentials(db, workflow_id=random_private_workflow.workflow_id, token=None) stmt = select(Workflow).where(Workflow._workflow_id == random_private_workflow.workflow_id.bytes) workflow = await db.scalar(stmt) assert workflow is not None assert workflow.credentials_token is None - assert workflow.credentials_username is None diff --git a/app/tests/unit/test_scm.py b/app/tests/unit/test_scm.py index 8a80711e38bb280bcbab93fc62d97d63f9c41121..d438d82c5d9f74dd1c6495eb8e57102d49b77655 100644 --- a/app/tests/unit/test_scm.py +++ b/app/tests/unit/test_scm.py @@ -111,7 +111,6 @@ class TestSCM: assert provider is not None assert provider.name == "github" assert isinstance(provider, GitHubProvider) - assert provider.user == repo.user assert provider.password == repo.token def test_build_provider_from_private_gitlab_repo(