96 lines
3.7 KiB
Python
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)
|