# SPDX-License-Identifier: BSD-2-Clause # SPDX-FileCopyrightText: 2023 KDE e.V. # SPDX-FileContributor: Ingo Klöcker import logging from typing import Any, Optional import requests from sftpnotary import projects log = logging.getLogger(__name__) authentication = None def setAuthentication(auth): global authentication authentication = auth class AuthenticationFailed(Exception): pass class GitLabJobTokenAuthentication: def __init__(self, apiV4Url: str, userAgent: Optional[str] = None): self.apiV4Url = apiV4Url self.userAgent = userAgent def authenticate(self, *, request, **_): jobData = self.getJob(request.token) if jobData is None: raise AuthenticationFailed("Failed to retrieve job. The job is no longer running or the token is invalid.") projectId = jobData.get("pipeline", {}).get("project_id") if projectId is None: raise AuthenticationFailed("Job data does not include the project ID (pipeline.project_id).") pipelineSource = jobData.get("pipeline", {}).get("source") if pipelineSource is None: raise AuthenticationFailed("Job data does not include the pipeline source (pipeline.source).") # instead of trying to figure out the real project path and branch of an MR (possibly originating from a clone) # we reject all merge request jobs if pipelineSource == "merge_request_event": raise AuthenticationFailed("Merge requests are not cleared for this request.") projectData = self.getProject(projectId) if projectData is None: raise AuthenticationFailed("Failed to retrieve project for job.") projectPath = projectData.get("path_with_namespace") branch = jobData.get("ref") if branch != request.branch: raise AuthenticationFailed( f"Branch in request ({request.branch}) does not match the job's branch ({branch})." ) if projectPath != request.projectPath: raise AuthenticationFailed( f"Project path in request ({request.projectPath}) does not match the job's project path " f"({projectPath})." ) if not projects.settings.exists(projectPath, branch): raise AuthenticationFailed(f"Branch '{branch}' of project '{projectPath}' is not cleared for this request.") def apiRequest(self, path: str, **kwargs: Any) -> Any: apiEndpoint = f"{self.apiV4Url}{path}" headers = kwargs.setdefault("headers", {}) if self.userAgent: headers.update({"User-Agent": self.userAgent}) timeout = kwargs.setdefault("timeout", 5) # seconds del kwargs["timeout"] # pass timeout explicitly to satisfy bandit r = requests.get(apiEndpoint, timeout=timeout, **kwargs) r.raise_for_status() return r.json() def getJob(self, jobToken: str): # Fails if job token is invalid or job is no longer running. headers = { "Authorization": "Bearer " + jobToken, } try: return self.apiRequest("/job", headers=headers) except requests.RequestException as e: log.error("Error: Failed to retrieve job.") log.debug("Error details:", exc_info=e) def getProject(self, projectId: Optional[int]): if projectId is None: raise TypeError("Project ID must be a number.") try: return self.apiRequest(f"/projects/{projectId}") except requests.RequestException as e: log.error("Error: Failed to retrieve project for job.") log.debug("Error details:", exc_info=e)