OpenMiInk/third_party/ci-notary-service/sftpnotary/authentication.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

96 lines
3.7 KiB
Python

# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: 2023 KDE e.V.
# SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
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)