@zereight/mcp-gitlab 2.1.57 → 2.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +13 -1
- package/README.md +13 -1
- package/README.zh-CN.md +13 -1
- package/build/config.js +2 -0
- package/build/index.js +388 -47
- package/build/schemas.js +138 -3
- package/build/scripts/check-skill-sync.js +5 -0
- package/build/stateless/session-id.js +6 -1
- package/build/test/stateless/session-id.test.js +26 -0
- package/build/test/test-deployment-tools.js +84 -1
- package/build/test/test-dynamic-project-scope.js +362 -0
- package/build/test/test-permission-mode.js +1 -0
- package/build/test/test-toolset-filtering.js +1 -1
- package/build/tools/registry.js +106 -2
- package/build/tools/tool-descriptions.js +2 -0
- package/build/utils/redact-sensitive.js +11 -4
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { getConfig, ENABLE_DYNAMIC_API_URL, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
|
|
2
|
+
import { getConfig, ENABLE_DYNAMIC_API_URL, ENABLE_DYNAMIC_PROJECT_SCOPE, ENABLE_STRICT_PROJECT_SCOPE, GITLAB_AUTH_COOKIE_PATH, GITLAB_ALLOW_UNAUTHENTICATED_TOOL_DISCOVERY, GITLAB_CA_CERT_PATH, GITLAB_JOB_TOKEN, GITLAB_MCP_OAUTH, GITLAB_OAUTH_APP_ID, GITLAB_OAUTH_SCOPES, GITLAB_OAUTH_CALLBACK_PROXY, GITLAB_PERSONAL_ACCESS_TOKEN, GITLAB_POOL_MAX_SIZE, GITLAB_DISABLE_VERSION_CHECK, GITLAB_READ_ONLY_MODE, GITLAB_PERMISSION_MODE, GITLAB_TOOLSETS_RAW, GITLAB_TOOLS_RAW, HOST, HTTP_PROXY, HTTPS_PROXY, IS_OLD, MCP_SERVER_URL, NODE_TLS_REJECT_UNAUTHORIZED, NO_PROXY, OAUTH_STATELESS_CLIENT_TTL_SECONDS, OAUTH_STATELESS_MODE, OAUTH_STATELESS_PENDING_TTL_SECONDS, OAUTH_STATELESS_SESSION_TTL_SECONDS, OAUTH_STATELESS_STORED_TTL_SECONDS, PORT, REMOTE_AUTHORIZATION, SESSION_TIMEOUT_SECONDS, SSE, STREAMABLE_HTTP, MCP_TRUST_PROXY, USE_GITLAB_WIKI, USE_MILESTONE, USE_OAUTH, USE_PIPELINE, GITLAB_TOOL_POLICY_APPROVE_RAW, GITLAB_TOOL_POLICY_HIDDEN_RAW, GITLAB_OAUTH_ALLOWED_GROUPS_RAW, GITLAB_ALLOWED_GROUPS_RAW, GITLAB_OAUTH_ALLOWED_GROUPS, } from "./config.js";
|
|
3
3
|
/** True when the server is running in remote/network mode (SSE or StreamableHTTP transport). */
|
|
4
4
|
const IS_REMOTE = SSE || STREAMABLE_HTTP;
|
|
5
5
|
const STREAMABLE_HTTP_AUTH_TOKEN = getConfig("streamable-http-auth-token", "STREAMABLE_HTTP_AUTH_TOKEN");
|
|
@@ -104,8 +104,8 @@ import { parseSearchReplaceBlocks, applySearchReplace, applyUnifiedDiff, } from
|
|
|
104
104
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
105
105
|
import { GitLabClientPool } from "./gitlab-client-pool.js";
|
|
106
106
|
import { allTools, readOnlyTools, destructiveTools, deleteTools, parseEnabledToolsets, parseIndividualTools, buildFeatureFlagOverrides, isToolInEnabledToolset, TOOLSET_DEFINITIONS, ALL_TOOLSET_IDS, } from "./tools/registry.js";
|
|
107
|
-
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
108
|
-
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetDeploymentSchema, GetEnvironmentSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
107
|
+
import { BulkPublishDraftNotesSchema, CancelPipelineJobSchema, ErasePipelineJobSchema, CancelPipelineSchema, CreateBranchSchema, CreateDraftNoteSchema, CreateIssueLinkSchema, CreateIssueNoteSchema, CreateIssueSchema, CreateIssueEmojiReactionSchema, CreateIssueNoteEmojiReactionSchema, ListIssueEmojiReactionsSchema, ListIssueNoteEmojiReactionsSchema, CreateLabelSchema, // Added
|
|
108
|
+
CreateMergeRequestNoteSchema, CreateMergeRequestDiscussionNoteSchema, CreateMergeRequestEmojiReactionSchema, CreateMergeRequestNoteEmojiReactionSchema, ListMergeRequestEmojiReactionsSchema, ListMergeRequestNoteEmojiReactionsSchema, CreateMergeRequestSchema, CreateMergeRequestThreadSchema, CreateNoteSchema, CreateCommitStatusSchema, CreateOrUpdateFileSchema, CreatePipelineSchema, CreateProjectMilestoneSchema, CreateGroupMilestoneSchema, CreateRepositorySchema, CreateGroupSchema, CreateWikiPageSchema, CreateGroupWikiPageSchema, DeleteBranchSchema, GetProtectedBranchSchema, ListProtectedBranchesSchema, ProtectBranchSchema, UnprotectBranchSchema, UpdateDefaultBranchSchema, DeleteDraftNoteSchema, DeleteGroupMilestoneSchema, DeleteGroupWikiPageSchema, DeleteIssueLinkSchema, DeleteIssueSchema, DeleteIssueEmojiReactionSchema, DeleteIssueNoteEmojiReactionSchema, DeleteLabelSchema, DeleteProjectMilestoneSchema, DeleteWikiPageSchema, DeleteMergeRequestNoteSchema, DeleteMergeRequestEmojiReactionSchema, DeleteMergeRequestNoteEmojiReactionSchema, EditProjectMilestoneSchema, EditGroupMilestoneSchema, ForkRepositorySchema, GetBranchDiffsSchema, GetBranchSchema, GetCommitDiffSchema, GetCommitSchema, GetFileBlameSchema, GitLabBlameEntrySchema, GetDraftNoteSchema, GetFileContentsSchema, GetIssueLinkSchema, GetIssueSchema, GetLabelSchema, GetMergeRequestDiffsSchema, GetMergeRequestSchema, GetMilestoneBurndownEventsSchema, GetMilestoneIssuesSchema, GetMilestoneMergeRequestsSchema, GetGroupMilestoneSchema, GetGroupMilestoneIssuesSchema, GetGroupMilestoneMergeRequestsSchema, GetGroupMilestoneBurndownEventsSchema, GetDeploymentSchema, CreateDeploymentSchema, UpdateDeploymentSchema, DeploymentApprovalSchema, ListDeploymentMergeRequestsSchema, GetEnvironmentSchema, UpdateEnvironmentSchema, StopEnvironmentSchema, StopStaleEnvironmentsSchema, DeleteReviewAppEnvironmentsSchema, PipelineTriggerIdSchema, ListPipelineTriggersSchema, CreatePipelineTriggerSchema, UpdatePipelineTriggerSchema, TriggerPipelineSchema, GetNamespaceSchema, GitLabCiLintResultSchema, GetPipelineJobOutputSchema, PipelineJobControlSchema, GetPipelineSchema, GetPipelineVariablesSchema, UpdatePipelineMetadataSchema, DeletePipelineSchema, PipelineReportSchema, WaitForPipelineSchema, WaitForPipelineJobSchema, PlayPipelineJobsSchema, GetProjectMilestoneSchema, GetProjectSchema, GetRepositoryTreeSchema, GetUsersSchema, GetUserSchema, GitLabUserFullSchema, WhoAmISchema, GitLabCurrentUserSchema, GetWikiPageSchema, GitLabCommitSchema, GitLabCommitStatusSchema, GitLabCompareResultSchema, GitLabContentSchema, GitLabCreateUpdateFileResponseSchema, GitLabDiffSchema,
|
|
109
109
|
// Discussion Schemas
|
|
110
110
|
GitLabDiscussionNoteSchema, // Added
|
|
111
111
|
GitLabDiscussionSchema,
|
|
@@ -430,6 +430,7 @@ function createServer() {
|
|
|
430
430
|
lastUsed: authData.lastUsed,
|
|
431
431
|
apiUrl: authData.apiUrl,
|
|
432
432
|
publicBaseUrl: authData.publicBaseUrl,
|
|
433
|
+
allowedProjectIds: authData.allowedProjectIds,
|
|
433
434
|
};
|
|
434
435
|
// Run the handler within the retrieved context
|
|
435
436
|
const result = await sessionAuthStore.run(sessionContext, () => handleToolCall(request.params));
|
|
@@ -707,6 +708,10 @@ function validateConfiguration() {
|
|
|
707
708
|
if (enableDynamicApiUrl && !remoteAuth) {
|
|
708
709
|
errors.push("ENABLE_DYNAMIC_API_URL=true requires REMOTE_AUTHORIZATION=true");
|
|
709
710
|
}
|
|
711
|
+
const enableDynamicProjectScope = getConfig("enable-dynamic-project-scope", "ENABLE_DYNAMIC_PROJECT_SCOPE") === "true";
|
|
712
|
+
if (enableDynamicProjectScope && !remoteAuth) {
|
|
713
|
+
errors.push("ENABLE_DYNAMIC_PROJECT_SCOPE=true requires REMOTE_AUTHORIZATION=true");
|
|
714
|
+
}
|
|
710
715
|
if (errors.length > 0) {
|
|
711
716
|
logger.error("Configuration validation failed:");
|
|
712
717
|
errors.forEach(err => logger.error(` - ${err}`));
|
|
@@ -967,7 +972,8 @@ async function ensureSessionForRequest() {
|
|
|
967
972
|
redirect: "follow",
|
|
968
973
|
});
|
|
969
974
|
// 401 means auth failed but the request completed - cookies were still exchanged
|
|
970
|
-
|
|
975
|
+
// 403 means insufficient scope (e.g. missing User: Read) but auth is valid
|
|
976
|
+
initialSessionRequestMade = response.ok || response.status === 401 || response.status === 403;
|
|
971
977
|
}
|
|
972
978
|
catch {
|
|
973
979
|
logger.debug("Session warmup request failed, will retry on next request");
|
|
@@ -1048,6 +1054,47 @@ function getEffectiveApiUrl() {
|
|
|
1048
1054
|
}
|
|
1049
1055
|
return GITLAB_API_URL;
|
|
1050
1056
|
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Get the effective project allowlist for the current request
|
|
1059
|
+
* In REMOTE_AUTHORIZATION mode with ENABLE_DYNAMIC_PROJECT_SCOPE, reads the scope
|
|
1060
|
+
* narrowed via the X-GitLab-Allowed-Project-Ids header from session context
|
|
1061
|
+
* (already validated against GITLAB_ALLOWED_PROJECT_IDS by parseAuthHeaders)
|
|
1062
|
+
* Otherwise, uses environment GITLAB_ALLOWED_PROJECT_IDS
|
|
1063
|
+
*/
|
|
1064
|
+
function getEffectiveAllowedProjectIds() {
|
|
1065
|
+
if (ENABLE_DYNAMIC_PROJECT_SCOPE) {
|
|
1066
|
+
const ctx = sessionAuthStore.getStore();
|
|
1067
|
+
if (ctx?.allowedProjectIds && ctx.allowedProjectIds.length > 0) {
|
|
1068
|
+
return ctx.allowedProjectIds;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return GITLAB_ALLOWED_PROJECT_IDS;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Reject tools that cannot be limited to allowed projects when
|
|
1075
|
+
* ENABLE_STRICT_PROJECT_SCOPE is set and an allowlist is in effect
|
|
1076
|
+
*/
|
|
1077
|
+
function rejectIfStrictProjectScope(toolName) {
|
|
1078
|
+
if (ENABLE_STRICT_PROJECT_SCOPE) {
|
|
1079
|
+
rejectIfProjectScopedDeployment(toolName);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Filter a project listing down to the effective allowlist
|
|
1084
|
+
* Applies only when ENABLE_STRICT_PROJECT_SCOPE is set and an allowlist is in effect
|
|
1085
|
+
* Allowlist entries may be numeric IDs or full namespace paths
|
|
1086
|
+
*/
|
|
1087
|
+
function filterProjectsByAllowlist(projects) {
|
|
1088
|
+
if (!ENABLE_STRICT_PROJECT_SCOPE) {
|
|
1089
|
+
return projects;
|
|
1090
|
+
}
|
|
1091
|
+
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
1092
|
+
if (allowedProjectIds.length === 0) {
|
|
1093
|
+
return projects;
|
|
1094
|
+
}
|
|
1095
|
+
return projects.filter(project => allowedProjectIds.includes(String(project.id)) ||
|
|
1096
|
+
allowedProjectIds.includes(project.path_with_namespace));
|
|
1097
|
+
}
|
|
1051
1098
|
/**
|
|
1052
1099
|
* Get fetch configuration with proper client from pool
|
|
1053
1100
|
* Uses connection pooling when dynamic API URLs are enabled
|
|
@@ -1217,20 +1264,21 @@ async function handleGitLabError(response) {
|
|
|
1217
1264
|
* @throws {Error} If GITLAB_ALLOWED_PROJECT_IDS is set and the requested project is not in the whitelist
|
|
1218
1265
|
*/
|
|
1219
1266
|
function getEffectiveProjectId(projectId) {
|
|
1220
|
-
|
|
1267
|
+
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
1268
|
+
if (allowedProjectIds.length > 0) {
|
|
1221
1269
|
// If there's only one allowed project, use it as default
|
|
1222
|
-
if (
|
|
1223
|
-
return
|
|
1270
|
+
if (allowedProjectIds.length === 1 && !projectId) {
|
|
1271
|
+
return allowedProjectIds[0];
|
|
1224
1272
|
}
|
|
1225
1273
|
// If a project ID is provided, check if it's in the whitelist
|
|
1226
|
-
if (projectId && !
|
|
1227
|
-
throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${
|
|
1274
|
+
if (projectId && !allowedProjectIds.includes(projectId)) {
|
|
1275
|
+
throw new Error(`Access denied: Project ${projectId} is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
|
|
1228
1276
|
}
|
|
1229
1277
|
// If no project ID provided but we have multiple allowed projects, require an explicit choice
|
|
1230
|
-
if (!projectId &&
|
|
1231
|
-
throw new Error(`Multiple projects allowed (${
|
|
1278
|
+
if (!projectId && allowedProjectIds.length > 1) {
|
|
1279
|
+
throw new Error(`Multiple projects allowed (${allowedProjectIds.join(", ")}). Please specify a project ID.`);
|
|
1232
1280
|
}
|
|
1233
|
-
return projectId ||
|
|
1281
|
+
return projectId || allowedProjectIds[0];
|
|
1234
1282
|
}
|
|
1235
1283
|
// Prioritize the passed projectId over GITLAB_PROJECT_ID to allow querying different projects
|
|
1236
1284
|
if (projectId) {
|
|
@@ -1245,8 +1293,8 @@ function rejectIfProjectScopedDeployment(toolName) {
|
|
|
1245
1293
|
if (GITLAB_PROJECT_ID) {
|
|
1246
1294
|
throw new Error(`${toolName} is not allowed when GITLAB_PROJECT_ID is set (server is locked to a single project)`);
|
|
1247
1295
|
}
|
|
1248
|
-
if (
|
|
1249
|
-
throw new Error(`${toolName} is not allowed
|
|
1296
|
+
if (getEffectiveAllowedProjectIds().length > 0) {
|
|
1297
|
+
throw new Error(`${toolName} is not allowed while a project allowlist is in effect (GITLAB_ALLOWED_PROJECT_IDS or a session scope restricts access to configured projects)`);
|
|
1250
1298
|
}
|
|
1251
1299
|
}
|
|
1252
1300
|
/**
|
|
@@ -1390,9 +1438,11 @@ async function createIssue(projectId, options) {
|
|
|
1390
1438
|
*/
|
|
1391
1439
|
async function listIssues(projectId, options = {}) {
|
|
1392
1440
|
let url;
|
|
1393
|
-
if (projectId) {
|
|
1394
|
-
|
|
1395
|
-
|
|
1441
|
+
if (projectId || (ENABLE_STRICT_PROJECT_SCOPE && getEffectiveAllowedProjectIds().length > 0)) {
|
|
1442
|
+
// In strict scope, never fall back to the instance-wide endpoint;
|
|
1443
|
+
// resolve the default project (or fail for a multi-project allowlist)
|
|
1444
|
+
const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
|
|
1445
|
+
const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
|
|
1396
1446
|
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/issues`);
|
|
1397
1447
|
}
|
|
1398
1448
|
else {
|
|
@@ -1486,11 +1536,18 @@ function appendMergeRequestFilters(url, options) {
|
|
|
1486
1536
|
* @returns {Promise<GitLabMergeRequest[]>} List of merge requests
|
|
1487
1537
|
*/
|
|
1488
1538
|
async function listMergeRequests(projectId, options = {}) {
|
|
1489
|
-
const decodedProjectId = projectId ? decodeURIComponent(projectId) :
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1539
|
+
const decodedProjectId = projectId ? decodeURIComponent(projectId) : "";
|
|
1540
|
+
let url;
|
|
1541
|
+
if (decodedProjectId ||
|
|
1542
|
+
(ENABLE_STRICT_PROJECT_SCOPE && getEffectiveAllowedProjectIds().length > 0)) {
|
|
1543
|
+
// In strict scope, never fall back to the instance-wide endpoint;
|
|
1544
|
+
// resolve the default project (or fail for a multi-project allowlist)
|
|
1545
|
+
const effectiveProjectId = getEffectiveProjectId(decodedProjectId);
|
|
1546
|
+
url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(effectiveProjectId)}/merge_requests`);
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
url = new URL(`${getEffectiveApiUrl()}/merge_requests`);
|
|
1550
|
+
}
|
|
1494
1551
|
appendMergeRequestFilters(url, options);
|
|
1495
1552
|
const response = await fetch(url.toString(), {
|
|
1496
1553
|
...getFetchConfig(),
|
|
@@ -2179,8 +2236,8 @@ async function resolveProjectOrGroupPath(projectId) {
|
|
|
2179
2236
|
const namespaceMatch = decodedProjectId.match(/^(group|project):(.+)$/i);
|
|
2180
2237
|
const explicitKind = namespaceMatch?.[1]?.toLowerCase();
|
|
2181
2238
|
const requestedProjectId = namespaceMatch ? namespaceMatch[2] : decodedProjectId;
|
|
2182
|
-
if (explicitKind === "group" &&
|
|
2183
|
-
throw new Error("group:<id-or-path> cannot be used
|
|
2239
|
+
if (explicitKind === "group" && getEffectiveAllowedProjectIds().length > 0) {
|
|
2240
|
+
throw new Error("group:<id-or-path> cannot be used while a project allowlist is in effect (GITLAB_ALLOWED_PROJECT_IDS or a session scope), because the project allowlist does not cover groups");
|
|
2184
2241
|
}
|
|
2185
2242
|
const effectiveProjectId = getEffectiveProjectId(requestedProjectId);
|
|
2186
2243
|
if (explicitKind === "group") {
|
|
@@ -2202,7 +2259,7 @@ async function resolveProjectOrGroupPath(projectId) {
|
|
|
2202
2259
|
// resolving to an unrelated group with the same integer ID.
|
|
2203
2260
|
if (projectResponse.status === 404 &&
|
|
2204
2261
|
!explicitKind &&
|
|
2205
|
-
|
|
2262
|
+
getEffectiveAllowedProjectIds().length === 0 &&
|
|
2206
2263
|
!/^\d+$/.test(effectiveProjectId)) {
|
|
2207
2264
|
const groupUrl = new URL(`${getEffectiveApiUrl()}/groups/${encodeURIComponent(effectiveProjectId)}`);
|
|
2208
2265
|
const groupResponse = await fetch(groupUrl.toString(), {
|
|
@@ -3522,14 +3579,19 @@ async function searchProjects(query, page = 1, perPage = 20) {
|
|
|
3522
3579
|
const errorBody = await response.text();
|
|
3523
3580
|
throw new Error(`GitLab API error: ${response.status} ${response.statusText}\n${errorBody}`);
|
|
3524
3581
|
}
|
|
3525
|
-
const
|
|
3582
|
+
const unfiltered = (await response.json());
|
|
3583
|
+
const projects = filterProjectsByAllowlist(unfiltered);
|
|
3526
3584
|
const totalCount = response.headers.get("x-total");
|
|
3527
3585
|
const totalPages = response.headers.get("x-total-pages");
|
|
3528
|
-
|
|
3586
|
+
const nextPage = response.headers.get("x-next-page");
|
|
3587
|
+
// GitLab API doesn't return the total headers for results > 10,000
|
|
3588
|
+
// Keep the unfiltered totals so callers paginate past pages the allowlist filter thinned out;
|
|
3589
|
+
// without them, total_pages stays unset and next_page carries the continuation signal
|
|
3529
3590
|
const count = totalCount ? Number.parseInt(totalCount, 10) : projects.length;
|
|
3530
3591
|
return GitLabSearchResponseSchema.parse({
|
|
3531
3592
|
count,
|
|
3532
|
-
total_pages: totalPages ? Number.parseInt(totalPages, 10) :
|
|
3593
|
+
total_pages: totalPages ? Number.parseInt(totalPages, 10) : undefined,
|
|
3594
|
+
next_page: nextPage ? Number.parseInt(nextPage, 10) : undefined,
|
|
3533
3595
|
current_page: page,
|
|
3534
3596
|
items: projects,
|
|
3535
3597
|
});
|
|
@@ -4623,7 +4685,7 @@ async function listProjects(options = {}) {
|
|
|
4623
4685
|
await handleGitLabError(response);
|
|
4624
4686
|
// Parse and return the data
|
|
4625
4687
|
const data = await response.json();
|
|
4626
|
-
return z.array(GitLabProjectSchema).parse(data);
|
|
4688
|
+
return filterProjectsByAllowlist(z.array(GitLabProjectSchema).parse(data));
|
|
4627
4689
|
}
|
|
4628
4690
|
/**
|
|
4629
4691
|
* List labels for a project
|
|
@@ -4789,7 +4851,7 @@ async function listGroupProjects(options) {
|
|
|
4789
4851
|
});
|
|
4790
4852
|
await handleGitLabError(response);
|
|
4791
4853
|
const projects = await response.json();
|
|
4792
|
-
return GitLabProjectSchema.array().parse(projects);
|
|
4854
|
+
return filterProjectsByAllowlist(GitLabProjectSchema.array().parse(projects));
|
|
4793
4855
|
}
|
|
4794
4856
|
// Webhook API helper functions
|
|
4795
4857
|
/**
|
|
@@ -4800,6 +4862,7 @@ function buildWebhookBaseUrl(projectId, groupId) {
|
|
|
4800
4862
|
projectId = decodeURIComponent(projectId);
|
|
4801
4863
|
return `${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/hooks`;
|
|
4802
4864
|
}
|
|
4865
|
+
rejectIfStrictProjectScope("group webhooks");
|
|
4803
4866
|
const decodedGroupId = decodeURIComponent(groupId);
|
|
4804
4867
|
return `${getEffectiveApiUrl()}/groups/${encodeURIComponent(decodedGroupId)}/hooks`;
|
|
4805
4868
|
}
|
|
@@ -5121,11 +5184,12 @@ async function listPipelines(projectId, options = {}) {
|
|
|
5121
5184
|
* @param {number} pipelineId - The ID of the pipeline
|
|
5122
5185
|
* @returns {Promise<GitLabPipeline>} Pipeline details
|
|
5123
5186
|
*/
|
|
5124
|
-
async function getPipeline(projectId, pipelineId) {
|
|
5187
|
+
async function getPipeline(projectId, pipelineId, signal) {
|
|
5125
5188
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5126
5189
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`);
|
|
5127
5190
|
const response = await fetch(url.toString(), {
|
|
5128
5191
|
...getFetchConfig(),
|
|
5192
|
+
signal,
|
|
5129
5193
|
});
|
|
5130
5194
|
if (response.status === 404) {
|
|
5131
5195
|
throw new Error(`Pipeline not found`);
|
|
@@ -5134,6 +5198,41 @@ async function getPipeline(projectId, pipelineId) {
|
|
|
5134
5198
|
const data = await response.json();
|
|
5135
5199
|
return GitLabPipelineSchema.parse(data);
|
|
5136
5200
|
}
|
|
5201
|
+
async function getPipelineVariables(projectId, pipelineId, page, perPage) {
|
|
5202
|
+
projectId = decodeURIComponent(projectId);
|
|
5203
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/variables`);
|
|
5204
|
+
if (page !== undefined)
|
|
5205
|
+
url.searchParams.set("page", String(page));
|
|
5206
|
+
if (perPage !== undefined)
|
|
5207
|
+
url.searchParams.set("per_page", String(perPage));
|
|
5208
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
5209
|
+
await handleGitLabError(response);
|
|
5210
|
+
return response.json();
|
|
5211
|
+
}
|
|
5212
|
+
async function getPipelineReport(projectId, pipelineId, summary = false, page, perPage) {
|
|
5213
|
+
projectId = decodeURIComponent(projectId);
|
|
5214
|
+
const endpoint = summary ? "test_report_summary" : "test_report";
|
|
5215
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/${endpoint}`);
|
|
5216
|
+
if (page !== undefined)
|
|
5217
|
+
url.searchParams.set("page", String(page));
|
|
5218
|
+
if (perPage !== undefined)
|
|
5219
|
+
url.searchParams.set("per_page", String(perPage));
|
|
5220
|
+
const response = await fetch(url.toString(), { ...getFetchConfig() });
|
|
5221
|
+
await handleGitLabError(response);
|
|
5222
|
+
return response.json();
|
|
5223
|
+
}
|
|
5224
|
+
async function deletePipeline(projectId, pipelineId) {
|
|
5225
|
+
projectId = decodeURIComponent(projectId);
|
|
5226
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}`, { ...getFetchConfig(), method: "DELETE" });
|
|
5227
|
+
await handleGitLabError(response);
|
|
5228
|
+
return response.status === 204 ? { deleted: true } : response.json();
|
|
5229
|
+
}
|
|
5230
|
+
async function updatePipelineMetadata(projectId, pipelineId, name) {
|
|
5231
|
+
projectId = decodeURIComponent(projectId);
|
|
5232
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/pipelines/${encodeGitLabPathSegment(pipelineId)}/metadata`, { ...getFetchConfig(), method: "PUT", body: JSON.stringify({ name }) });
|
|
5233
|
+
await handleGitLabError(response);
|
|
5234
|
+
return response.json();
|
|
5235
|
+
}
|
|
5137
5236
|
/**
|
|
5138
5237
|
* List deployments in a GitLab project
|
|
5139
5238
|
*
|
|
@@ -5165,7 +5264,7 @@ async function listDeployments(projectId, options = {}) {
|
|
|
5165
5264
|
*/
|
|
5166
5265
|
async function getDeployment(projectId, deploymentId) {
|
|
5167
5266
|
projectId = decodeURIComponent(projectId);
|
|
5168
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${deploymentId}`);
|
|
5267
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments/${encodeGitLabPathSegment(deploymentId)}`);
|
|
5169
5268
|
const response = await fetch(url.toString(), {
|
|
5170
5269
|
...getFetchConfig(),
|
|
5171
5270
|
});
|
|
@@ -5176,6 +5275,20 @@ async function getDeployment(projectId, deploymentId) {
|
|
|
5176
5275
|
const data = await response.json();
|
|
5177
5276
|
return GitLabDeploymentSchema.parse(data);
|
|
5178
5277
|
}
|
|
5278
|
+
async function deploymentRequest(projectId, path = "", method = "GET", body, query) {
|
|
5279
|
+
projectId = decodeURIComponent(projectId);
|
|
5280
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/deployments${path}`);
|
|
5281
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
5282
|
+
if (value !== undefined)
|
|
5283
|
+
url.searchParams.set(key, String(value));
|
|
5284
|
+
}
|
|
5285
|
+
const response = await fetch(url.toString(), {
|
|
5286
|
+
...getFetchConfig(), method,
|
|
5287
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
5288
|
+
});
|
|
5289
|
+
await handleGitLabError(response);
|
|
5290
|
+
return response.status === 204 ? { success: true } : response.json();
|
|
5291
|
+
}
|
|
5179
5292
|
/**
|
|
5180
5293
|
* List environments in a GitLab project
|
|
5181
5294
|
*
|
|
@@ -5207,7 +5320,7 @@ async function listEnvironments(projectId, options = {}) {
|
|
|
5207
5320
|
*/
|
|
5208
5321
|
async function getEnvironment(projectId, environmentId) {
|
|
5209
5322
|
projectId = decodeURIComponent(projectId);
|
|
5210
|
-
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${environmentId}`);
|
|
5323
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments/${encodeGitLabPathSegment(environmentId)}`);
|
|
5211
5324
|
const response = await fetch(url.toString(), {
|
|
5212
5325
|
...getFetchConfig(),
|
|
5213
5326
|
});
|
|
@@ -5218,6 +5331,36 @@ async function getEnvironment(projectId, environmentId) {
|
|
|
5218
5331
|
const data = await response.json();
|
|
5219
5332
|
return GitLabEnvironmentSchema.parse(data);
|
|
5220
5333
|
}
|
|
5334
|
+
async function environmentRequest(projectId, path, method, body, query) {
|
|
5335
|
+
projectId = decodeURIComponent(projectId);
|
|
5336
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/environments${path}`);
|
|
5337
|
+
for (const [key, value] of Object.entries(query ?? {}))
|
|
5338
|
+
if (value !== undefined)
|
|
5339
|
+
url.searchParams.set(key, String(value));
|
|
5340
|
+
const response = await fetch(url.toString(), { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
5341
|
+
await handleGitLabError(response);
|
|
5342
|
+
return response.status === 204 ? { success: true } : response.json();
|
|
5343
|
+
}
|
|
5344
|
+
async function pipelineTriggerRequest(projectId, path = "", method = "GET", body) {
|
|
5345
|
+
projectId = decodeURIComponent(projectId);
|
|
5346
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/triggers${path}`, { ...getFetchConfig(), method, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
5347
|
+
await handleGitLabError(response);
|
|
5348
|
+
return response.status === 204 ? { success: true } : response.json();
|
|
5349
|
+
}
|
|
5350
|
+
async function triggerPipeline(projectId, token, ref, variables, inputs) {
|
|
5351
|
+
projectId = decodeURIComponent(projectId);
|
|
5352
|
+
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/trigger/pipeline`);
|
|
5353
|
+
url.searchParams.set("token", token);
|
|
5354
|
+
url.searchParams.set("ref", ref);
|
|
5355
|
+
const body = {};
|
|
5356
|
+
if (variables)
|
|
5357
|
+
body.variables = variables;
|
|
5358
|
+
if (inputs)
|
|
5359
|
+
body.inputs = inputs;
|
|
5360
|
+
const response = await fetch(url.toString(), { ...getFetchConfig(), method: "POST", body: Object.keys(body).length ? JSON.stringify(body) : undefined });
|
|
5361
|
+
await handleGitLabError(response);
|
|
5362
|
+
return response.json();
|
|
5363
|
+
}
|
|
5221
5364
|
/**
|
|
5222
5365
|
* List all jobs in a specific pipeline
|
|
5223
5366
|
*
|
|
@@ -5282,11 +5425,12 @@ async function listPipelineTriggerJobs(projectId, pipelineId, options = {}) {
|
|
|
5282
5425
|
const data = await response.json();
|
|
5283
5426
|
return z.array(GitLabPipelineTriggerJobSchema).parse(data);
|
|
5284
5427
|
}
|
|
5285
|
-
async function getPipelineJob(projectId, jobId) {
|
|
5428
|
+
async function getPipelineJob(projectId, jobId, signal) {
|
|
5286
5429
|
projectId = decodeURIComponent(projectId); // Decode project ID
|
|
5287
5430
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}`);
|
|
5288
5431
|
const response = await fetch(url.toString(), {
|
|
5289
5432
|
...getFetchConfig(),
|
|
5433
|
+
signal,
|
|
5290
5434
|
});
|
|
5291
5435
|
if (response.status === 404) {
|
|
5292
5436
|
throw new Error(`Job not found`);
|
|
@@ -5756,13 +5900,15 @@ async function deletePipelineScheduleVariable(projectId, pipelineScheduleId, key
|
|
|
5756
5900
|
* @param {Object} variables - Optional job variables
|
|
5757
5901
|
* @returns {Promise<GitLabPipelineJob>} The run job
|
|
5758
5902
|
*/
|
|
5759
|
-
async function playPipelineJob(projectId, jobId, variables) {
|
|
5903
|
+
async function playPipelineJob(projectId, jobId, variables, jobInputs) {
|
|
5760
5904
|
projectId = decodeURIComponent(projectId);
|
|
5761
5905
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/play`);
|
|
5762
5906
|
const body = {};
|
|
5763
5907
|
if (variables && variables.length > 0) {
|
|
5764
5908
|
body.job_variables_attributes = variables;
|
|
5765
5909
|
}
|
|
5910
|
+
if (jobInputs)
|
|
5911
|
+
body.job_inputs = jobInputs;
|
|
5766
5912
|
const response = await fetch(url.toString(), {
|
|
5767
5913
|
...getFetchConfig(),
|
|
5768
5914
|
method: "POST",
|
|
@@ -5772,6 +5918,15 @@ async function playPipelineJob(projectId, jobId, variables) {
|
|
|
5772
5918
|
const data = await response.json();
|
|
5773
5919
|
return GitLabPipelineJobSchema.parse(data);
|
|
5774
5920
|
}
|
|
5921
|
+
async function playPipelineJobs(projectId, jobIds, variables, timeoutSeconds = 300, pollIntervalSeconds = 5) {
|
|
5922
|
+
const jobs = [];
|
|
5923
|
+
for (const jobId of jobIds) {
|
|
5924
|
+
const played = await playPipelineJob(projectId, jobId, variables);
|
|
5925
|
+
const completed = await waitForStatus(signal => getPipelineJob(projectId, played.id, signal), TERMINAL_JOB_STATUSES, timeoutSeconds, pollIntervalSeconds);
|
|
5926
|
+
jobs.push(completed);
|
|
5927
|
+
}
|
|
5928
|
+
return jobs;
|
|
5929
|
+
}
|
|
5775
5930
|
/**
|
|
5776
5931
|
* Retry a job
|
|
5777
5932
|
*
|
|
@@ -5779,17 +5934,46 @@ async function playPipelineJob(projectId, jobId, variables) {
|
|
|
5779
5934
|
* @param {number} jobId - The ID of the job to retry
|
|
5780
5935
|
* @returns {Promise<GitLabPipelineJob>} The retried job
|
|
5781
5936
|
*/
|
|
5782
|
-
async function retryPipelineJob(projectId, jobId) {
|
|
5937
|
+
async function retryPipelineJob(projectId, jobId, jobInputs) {
|
|
5783
5938
|
projectId = decodeURIComponent(projectId);
|
|
5784
5939
|
const url = new URL(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/retry`);
|
|
5785
5940
|
const response = await fetch(url.toString(), {
|
|
5786
5941
|
...getFetchConfig(),
|
|
5787
5942
|
method: "POST",
|
|
5943
|
+
body: jobInputs ? JSON.stringify({ job_inputs: jobInputs }) : undefined,
|
|
5788
5944
|
});
|
|
5789
5945
|
await handleGitLabError(response);
|
|
5790
5946
|
const data = await response.json();
|
|
5791
5947
|
return GitLabPipelineJobSchema.parse(data);
|
|
5792
5948
|
}
|
|
5949
|
+
async function erasePipelineJob(projectId, jobId) {
|
|
5950
|
+
projectId = decodeURIComponent(projectId);
|
|
5951
|
+
const response = await fetch(`${getEffectiveApiUrl()}/projects/${encodeURIComponent(getEffectiveProjectId(projectId))}/jobs/${encodeGitLabPathSegment(jobId)}/erase`, { ...getFetchConfig(), method: "POST" });
|
|
5952
|
+
await handleGitLabError(response);
|
|
5953
|
+
return response.status === 204 ? { erased: true } : response.json();
|
|
5954
|
+
}
|
|
5955
|
+
const TERMINAL_PIPELINE_STATUSES = new Set(["success", "failed", "canceled", "skipped", "manual"]);
|
|
5956
|
+
const TERMINAL_JOB_STATUSES = new Set(["success", "failed", "canceled", "skipped", "manual"]);
|
|
5957
|
+
async function waitForStatus(fetchStatus, terminalStatuses, timeoutSeconds = 300, pollIntervalSeconds = 5) {
|
|
5958
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
5959
|
+
const fetchBeforeDeadline = async () => {
|
|
5960
|
+
const remaining = deadline - Date.now();
|
|
5961
|
+
if (remaining <= 0)
|
|
5962
|
+
throw new Error("Timed out waiting for terminal status");
|
|
5963
|
+
return fetchStatus(AbortSignal.timeout(remaining));
|
|
5964
|
+
};
|
|
5965
|
+
let value = await fetchBeforeDeadline();
|
|
5966
|
+
while (!terminalStatuses.has(value.status)) {
|
|
5967
|
+
const remaining = deadline - Date.now();
|
|
5968
|
+
if (remaining <= 0)
|
|
5969
|
+
break;
|
|
5970
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(pollIntervalSeconds * 1000, remaining)));
|
|
5971
|
+
value = await fetchBeforeDeadline();
|
|
5972
|
+
}
|
|
5973
|
+
if (!terminalStatuses.has(value.status))
|
|
5974
|
+
throw new Error(`Timed out waiting for terminal status; last status: ${value.status}`);
|
|
5975
|
+
return value;
|
|
5976
|
+
}
|
|
5793
5977
|
/**
|
|
5794
5978
|
* Cancel a job
|
|
5795
5979
|
*
|
|
@@ -6813,15 +6997,16 @@ async function resolveProjectFullPath(projectId) {
|
|
|
6813
6997
|
* Allowlist entries may be numeric IDs or full namespace paths.
|
|
6814
6998
|
*/
|
|
6815
6999
|
function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
|
|
6816
|
-
|
|
7000
|
+
const allowedProjectIds = getEffectiveAllowedProjectIds();
|
|
7001
|
+
if (allowedProjectIds.length === 0) {
|
|
6817
7002
|
return;
|
|
6818
7003
|
}
|
|
6819
7004
|
const fullPath = project?.fullPath ?? undefined;
|
|
6820
7005
|
const numericId = project?.id?.match(/^gid:\/\/gitlab\/Project\/(\d+)$/)?.[1];
|
|
6821
|
-
const allowed = (fullPath !== undefined &&
|
|
6822
|
-
(numericId !== undefined &&
|
|
7006
|
+
const allowed = (fullPath !== undefined && allowedProjectIds.includes(fullPath)) ||
|
|
7007
|
+
(numericId !== undefined && allowedProjectIds.includes(numericId));
|
|
6823
7008
|
if (!allowed) {
|
|
6824
|
-
throw new Error(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${
|
|
7009
|
+
throw new Error(`Access denied: Vulnerability ${vulnerabilityId} belongs to project ${fullPath ?? numericId ?? "unknown"}, which is not in the allowed project list: ${allowedProjectIds.join(", ")}`);
|
|
6825
7010
|
}
|
|
6826
7011
|
}
|
|
6827
7012
|
/**
|
|
@@ -6831,7 +7016,7 @@ function assertVulnerabilityProjectAllowed(vulnerabilityId, project) {
|
|
|
6831
7016
|
* is not configured.
|
|
6832
7017
|
*/
|
|
6833
7018
|
async function ensureVulnerabilityProjectAllowed(vulnerabilityId) {
|
|
6834
|
-
if (
|
|
7019
|
+
if (getEffectiveAllowedProjectIds().length === 0) {
|
|
6835
7020
|
return;
|
|
6836
7021
|
}
|
|
6837
7022
|
const data = await executeGraphQL(`query getVulnerabilityProject($id: VulnerabilityID!) {
|
|
@@ -7558,6 +7743,7 @@ async function handleToolCall(params) {
|
|
|
7558
7743
|
};
|
|
7559
7744
|
}
|
|
7560
7745
|
case "search_code": {
|
|
7746
|
+
rejectIfStrictProjectScope("search_code");
|
|
7561
7747
|
const args = SearchCodeSchema.parse(params.arguments);
|
|
7562
7748
|
const results = await searchBlobs({
|
|
7563
7749
|
search: args.search,
|
|
@@ -7588,6 +7774,7 @@ async function handleToolCall(params) {
|
|
|
7588
7774
|
};
|
|
7589
7775
|
}
|
|
7590
7776
|
case "search_group_code": {
|
|
7777
|
+
rejectIfStrictProjectScope("search_group_code");
|
|
7591
7778
|
const args = SearchGroupCodeSchema.parse(params.arguments);
|
|
7592
7779
|
const results = await searchBlobs({
|
|
7593
7780
|
search: args.search,
|
|
@@ -7850,6 +8037,7 @@ async function handleToolCall(params) {
|
|
|
7850
8037
|
};
|
|
7851
8038
|
}
|
|
7852
8039
|
case "list_todos": {
|
|
8040
|
+
rejectIfStrictProjectScope("list_todos");
|
|
7853
8041
|
const args = ListTodosSchema.parse(params.arguments);
|
|
7854
8042
|
const todos = await listTodos(args);
|
|
7855
8043
|
return {
|
|
@@ -7857,6 +8045,7 @@ async function handleToolCall(params) {
|
|
|
7857
8045
|
};
|
|
7858
8046
|
}
|
|
7859
8047
|
case "mark_todo_done": {
|
|
8048
|
+
rejectIfStrictProjectScope("mark_todo_done");
|
|
7860
8049
|
const args = MarkTodoDoneSchema.parse(params.arguments);
|
|
7861
8050
|
const todo = await markTodoDone(args.id);
|
|
7862
8051
|
return {
|
|
@@ -7864,6 +8053,7 @@ async function handleToolCall(params) {
|
|
|
7864
8053
|
};
|
|
7865
8054
|
}
|
|
7866
8055
|
case "mark_all_todos_done": {
|
|
8056
|
+
rejectIfStrictProjectScope("mark_all_todos_done");
|
|
7867
8057
|
MarkAllTodosDoneSchema.parse(params.arguments);
|
|
7868
8058
|
await markAllTodosDone();
|
|
7869
8059
|
return {
|
|
@@ -8115,6 +8305,7 @@ async function handleToolCall(params) {
|
|
|
8115
8305
|
};
|
|
8116
8306
|
}
|
|
8117
8307
|
case "list_group_members": {
|
|
8308
|
+
rejectIfStrictProjectScope("list_group_members");
|
|
8118
8309
|
const args = ListGroupMembersSchema.parse(params.arguments);
|
|
8119
8310
|
const { group_id, ...options } = args;
|
|
8120
8311
|
const members = await listGroupMembers(group_id, options);
|
|
@@ -8637,6 +8828,7 @@ async function handleToolCall(params) {
|
|
|
8637
8828
|
};
|
|
8638
8829
|
}
|
|
8639
8830
|
case "list_group_wiki_pages": {
|
|
8831
|
+
rejectIfStrictProjectScope("list_group_wiki_pages");
|
|
8640
8832
|
const { group_id, page, per_page, with_content, render_html } = ListGroupWikiPagesSchema.parse(params.arguments);
|
|
8641
8833
|
const wikiPages = await listGroupWikiPages(group_id, {
|
|
8642
8834
|
page,
|
|
@@ -8649,6 +8841,7 @@ async function handleToolCall(params) {
|
|
|
8649
8841
|
};
|
|
8650
8842
|
}
|
|
8651
8843
|
case "get_group_wiki_page": {
|
|
8844
|
+
rejectIfStrictProjectScope("get_group_wiki_page");
|
|
8652
8845
|
const { group_id, slug, render_html } = GetGroupWikiPageSchema.parse(params.arguments);
|
|
8653
8846
|
const wikiPage = await getGroupWikiPage(group_id, slug, render_html);
|
|
8654
8847
|
return {
|
|
@@ -8656,6 +8849,7 @@ async function handleToolCall(params) {
|
|
|
8656
8849
|
};
|
|
8657
8850
|
}
|
|
8658
8851
|
case "create_group_wiki_page": {
|
|
8852
|
+
rejectIfStrictProjectScope("create_group_wiki_page");
|
|
8659
8853
|
const { group_id, title, content, format } = CreateGroupWikiPageSchema.parse(params.arguments);
|
|
8660
8854
|
const wikiPage = await createGroupWikiPage(group_id, title, content, format);
|
|
8661
8855
|
return {
|
|
@@ -8663,6 +8857,7 @@ async function handleToolCall(params) {
|
|
|
8663
8857
|
};
|
|
8664
8858
|
}
|
|
8665
8859
|
case "update_group_wiki_page": {
|
|
8860
|
+
rejectIfStrictProjectScope("update_group_wiki_page");
|
|
8666
8861
|
const { group_id, slug, title, content, format } = UpdateGroupWikiPageSchema.parse(params.arguments);
|
|
8667
8862
|
const wikiPage = await updateGroupWikiPage(group_id, slug, title, content, format);
|
|
8668
8863
|
return {
|
|
@@ -8670,6 +8865,7 @@ async function handleToolCall(params) {
|
|
|
8670
8865
|
};
|
|
8671
8866
|
}
|
|
8672
8867
|
case "delete_group_wiki_page": {
|
|
8868
|
+
rejectIfStrictProjectScope("delete_group_wiki_page");
|
|
8673
8869
|
const { group_id, slug } = DeleteGroupWikiPageSchema.parse(params.arguments);
|
|
8674
8870
|
await deleteGroupWikiPage(group_id, slug);
|
|
8675
8871
|
return {
|
|
@@ -8720,6 +8916,26 @@ async function handleToolCall(params) {
|
|
|
8720
8916
|
],
|
|
8721
8917
|
};
|
|
8722
8918
|
}
|
|
8919
|
+
case "get_pipeline_variables": {
|
|
8920
|
+
const { project_id, pipeline_id, page, per_page } = GetPipelineVariablesSchema.parse(params.arguments);
|
|
8921
|
+
return { content: [{ type: "text", text: JSON.stringify(await getPipelineVariables(project_id, pipeline_id, page, per_page)) }] };
|
|
8922
|
+
}
|
|
8923
|
+
case "get_pipeline_test_report": {
|
|
8924
|
+
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
8925
|
+
return { content: [{ type: "text", text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, false, page, per_page)) }] };
|
|
8926
|
+
}
|
|
8927
|
+
case "get_pipeline_test_report_summary": {
|
|
8928
|
+
const { project_id, pipeline_id, page, per_page } = PipelineReportSchema.parse(params.arguments);
|
|
8929
|
+
return { content: [{ type: "text", text: JSON.stringify(await getPipelineReport(project_id, pipeline_id, true, page, per_page)) }] };
|
|
8930
|
+
}
|
|
8931
|
+
case "delete_pipeline": {
|
|
8932
|
+
const { project_id, pipeline_id } = DeletePipelineSchema.parse(params.arguments);
|
|
8933
|
+
return { content: [{ type: "text", text: JSON.stringify(await deletePipeline(project_id, pipeline_id)) }] };
|
|
8934
|
+
}
|
|
8935
|
+
case "update_pipeline_metadata": {
|
|
8936
|
+
const { project_id, pipeline_id, name } = UpdatePipelineMetadataSchema.parse(params.arguments);
|
|
8937
|
+
return { content: [{ type: "text", text: JSON.stringify(await updatePipelineMetadata(project_id, pipeline_id, name)) }] };
|
|
8938
|
+
}
|
|
8723
8939
|
case "list_deployments": {
|
|
8724
8940
|
const args = ListDeploymentsSchema.parse(params.arguments);
|
|
8725
8941
|
const { project_id, ...options } = args;
|
|
@@ -8735,6 +8951,26 @@ async function handleToolCall(params) {
|
|
|
8735
8951
|
content: [{ type: "text", text: JSON.stringify(deployment) }],
|
|
8736
8952
|
};
|
|
8737
8953
|
}
|
|
8954
|
+
case "create_deployment": {
|
|
8955
|
+
const { project_id, ...body } = CreateDeploymentSchema.parse(params.arguments);
|
|
8956
|
+
return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, "", "POST", body)) }] };
|
|
8957
|
+
}
|
|
8958
|
+
case "update_deployment": {
|
|
8959
|
+
const { project_id, deployment_id, ...body } = UpdateDeploymentSchema.parse(params.arguments);
|
|
8960
|
+
return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "PUT", body)) }] };
|
|
8961
|
+
}
|
|
8962
|
+
case "delete_deployment": {
|
|
8963
|
+
const { project_id, deployment_id } = GetDeploymentSchema.parse(params.arguments);
|
|
8964
|
+
return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}`, "DELETE")) }] };
|
|
8965
|
+
}
|
|
8966
|
+
case "list_deployment_merge_requests": {
|
|
8967
|
+
const { project_id, deployment_id, ...query } = ListDeploymentMergeRequestsSchema.parse(params.arguments);
|
|
8968
|
+
return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/merge_requests`, "GET", undefined, query)) }] };
|
|
8969
|
+
}
|
|
8970
|
+
case "approve_deployment": {
|
|
8971
|
+
const { project_id, deployment_id, ...body } = DeploymentApprovalSchema.parse(params.arguments);
|
|
8972
|
+
return { content: [{ type: "text", text: JSON.stringify(await deploymentRequest(project_id, `/${encodeGitLabPathSegment(deployment_id)}/approval`, "POST", body)) }] };
|
|
8973
|
+
}
|
|
8738
8974
|
case "list_environments": {
|
|
8739
8975
|
const args = ListEnvironmentsSchema.parse(params.arguments);
|
|
8740
8976
|
const { project_id, ...options } = args;
|
|
@@ -8750,6 +8986,50 @@ async function handleToolCall(params) {
|
|
|
8750
8986
|
content: [{ type: "text", text: JSON.stringify(environment) }],
|
|
8751
8987
|
};
|
|
8752
8988
|
}
|
|
8989
|
+
case "update_environment": {
|
|
8990
|
+
const { project_id, environment_id, ...body } = UpdateEnvironmentSchema.parse(params.arguments);
|
|
8991
|
+
return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "PUT", body)) }] };
|
|
8992
|
+
}
|
|
8993
|
+
case "delete_environment": {
|
|
8994
|
+
const { project_id, environment_id } = GetEnvironmentSchema.parse(params.arguments);
|
|
8995
|
+
return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}`, "DELETE")) }] };
|
|
8996
|
+
}
|
|
8997
|
+
case "stop_environment": {
|
|
8998
|
+
const { project_id, environment_id, force } = StopEnvironmentSchema.parse(params.arguments);
|
|
8999
|
+
return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, `/${encodeGitLabPathSegment(environment_id)}/stop`, "POST", undefined, { force })) }] };
|
|
9000
|
+
}
|
|
9001
|
+
case "stop_stale_environments": {
|
|
9002
|
+
const { project_id, before } = StopStaleEnvironmentsSchema.parse(params.arguments);
|
|
9003
|
+
return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, "/stop_stale", "POST", undefined, { before })) }] };
|
|
9004
|
+
}
|
|
9005
|
+
case "delete_review_app_environments": {
|
|
9006
|
+
const { project_id, ...query } = DeleteReviewAppEnvironmentsSchema.parse(params.arguments);
|
|
9007
|
+
return { content: [{ type: "text", text: JSON.stringify(await environmentRequest(project_id, "/review_apps", "DELETE", undefined, query)) }] };
|
|
9008
|
+
}
|
|
9009
|
+
case "list_pipeline_triggers": {
|
|
9010
|
+
const { project_id } = ListPipelineTriggersSchema.parse(params.arguments);
|
|
9011
|
+
return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id))) }] };
|
|
9012
|
+
}
|
|
9013
|
+
case "get_pipeline_trigger": {
|
|
9014
|
+
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9015
|
+
return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`))) }] };
|
|
9016
|
+
}
|
|
9017
|
+
case "create_pipeline_trigger": {
|
|
9018
|
+
const { project_id, ...body } = CreatePipelineTriggerSchema.parse(params.arguments);
|
|
9019
|
+
return { content: [{ type: "text", text: JSON.stringify(await pipelineTriggerRequest(project_id, "", "POST", body)) }] };
|
|
9020
|
+
}
|
|
9021
|
+
case "update_pipeline_trigger": {
|
|
9022
|
+
const { project_id, trigger_id, ...body } = UpdatePipelineTriggerSchema.parse(params.arguments);
|
|
9023
|
+
return { content: [{ type: "text", text: JSON.stringify(redactSensitiveGitLabFields(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "PUT", body))) }] };
|
|
9024
|
+
}
|
|
9025
|
+
case "delete_pipeline_trigger": {
|
|
9026
|
+
const { project_id, trigger_id } = PipelineTriggerIdSchema.parse(params.arguments);
|
|
9027
|
+
return { content: [{ type: "text", text: JSON.stringify(await pipelineTriggerRequest(project_id, `/${encodeGitLabPathSegment(trigger_id)}`, "DELETE")) }] };
|
|
9028
|
+
}
|
|
9029
|
+
case "trigger_pipeline": {
|
|
9030
|
+
const { project_id, token, ref, variables, inputs } = TriggerPipelineSchema.parse(params.arguments);
|
|
9031
|
+
return { content: [{ type: "text", text: JSON.stringify(await triggerPipeline(project_id, token, ref, variables, inputs)) }] };
|
|
9032
|
+
}
|
|
8753
9033
|
case "list_pipeline_jobs": {
|
|
8754
9034
|
const { project_id, pipeline_id, ...options } = ListPipelineJobsSchema.parse(params.arguments);
|
|
8755
9035
|
const jobs = await listPipelineJobs(project_id, pipeline_id, options);
|
|
@@ -9103,8 +9383,8 @@ async function handleToolCall(params) {
|
|
|
9103
9383
|
};
|
|
9104
9384
|
}
|
|
9105
9385
|
case "play_pipeline_job": {
|
|
9106
|
-
const { project_id, job_id, job_variables_attributes } = PlayPipelineJobSchema.parse(params.arguments);
|
|
9107
|
-
const job = await playPipelineJob(project_id, job_id, job_variables_attributes);
|
|
9386
|
+
const { project_id, job_id, job_variables_attributes, job_inputs } = PlayPipelineJobSchema.parse(params.arguments);
|
|
9387
|
+
const job = await playPipelineJob(project_id, job_id, job_variables_attributes, job_inputs);
|
|
9108
9388
|
return {
|
|
9109
9389
|
content: [
|
|
9110
9390
|
{
|
|
@@ -9114,9 +9394,14 @@ async function handleToolCall(params) {
|
|
|
9114
9394
|
],
|
|
9115
9395
|
};
|
|
9116
9396
|
}
|
|
9397
|
+
case "play_pipeline_jobs": {
|
|
9398
|
+
const { project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds } = PlayPipelineJobsSchema.parse(params.arguments);
|
|
9399
|
+
const jobs = await playPipelineJobs(project_id, job_ids, job_variables_attributes, timeout_seconds, poll_interval_seconds);
|
|
9400
|
+
return { content: [{ type: "text", text: JSON.stringify(jobs) }] };
|
|
9401
|
+
}
|
|
9117
9402
|
case "retry_pipeline_job": {
|
|
9118
|
-
const { project_id, job_id } = RetryPipelineJobSchema.parse(params.arguments);
|
|
9119
|
-
const job = await retryPipelineJob(project_id, job_id);
|
|
9403
|
+
const { project_id, job_id, job_inputs } = RetryPipelineJobSchema.parse(params.arguments);
|
|
9404
|
+
const job = await retryPipelineJob(project_id, job_id, job_inputs);
|
|
9120
9405
|
return {
|
|
9121
9406
|
content: [
|
|
9122
9407
|
{
|
|
@@ -9138,6 +9423,20 @@ async function handleToolCall(params) {
|
|
|
9138
9423
|
],
|
|
9139
9424
|
};
|
|
9140
9425
|
}
|
|
9426
|
+
case "erase_pipeline_job": {
|
|
9427
|
+
const { project_id, job_id } = ErasePipelineJobSchema.parse(params.arguments);
|
|
9428
|
+
return { content: [{ type: "text", text: JSON.stringify(await erasePipelineJob(project_id, job_id)) }] };
|
|
9429
|
+
}
|
|
9430
|
+
case "wait_for_pipeline": {
|
|
9431
|
+
const { project_id, pipeline_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineSchema.parse(params.arguments);
|
|
9432
|
+
const pipeline = await waitForStatus(signal => getPipeline(project_id, pipeline_id, signal), TERMINAL_PIPELINE_STATUSES, timeout_seconds, poll_interval_seconds);
|
|
9433
|
+
return { content: [{ type: "text", text: JSON.stringify(pipeline) }] };
|
|
9434
|
+
}
|
|
9435
|
+
case "wait_for_job": {
|
|
9436
|
+
const { project_id, job_id, timeout_seconds, poll_interval_seconds } = WaitForPipelineJobSchema.parse(params.arguments);
|
|
9437
|
+
const job = await waitForStatus(signal => getPipelineJob(project_id, job_id, signal), TERMINAL_JOB_STATUSES, timeout_seconds, poll_interval_seconds);
|
|
9438
|
+
return { content: [{ type: "text", text: JSON.stringify(job) }] };
|
|
9439
|
+
}
|
|
9141
9440
|
case "list_job_artifacts": {
|
|
9142
9441
|
const { project_id, job_id, ...options } = ListJobArtifactsSchema.parse(params.arguments);
|
|
9143
9442
|
const artifacts = await listJobArtifacts(project_id, job_id, options);
|
|
@@ -9192,6 +9491,7 @@ async function handleToolCall(params) {
|
|
|
9192
9491
|
};
|
|
9193
9492
|
}
|
|
9194
9493
|
case "list_group_merge_requests": {
|
|
9494
|
+
rejectIfStrictProjectScope("list_group_merge_requests");
|
|
9195
9495
|
const { group_id, ...options } = ListGroupMergeRequestsSchema.parse(params.arguments);
|
|
9196
9496
|
const cleanedOptions = cleanMutuallyExclusiveIdUsernameOptions(options, LIST_MERGE_REQUESTS_ID_USERNAME_PAIRS);
|
|
9197
9497
|
const mergeRequests = await listGroupMergeRequests(group_id, cleanedOptions);
|
|
@@ -9311,6 +9611,7 @@ async function handleToolCall(params) {
|
|
|
9311
9611
|
};
|
|
9312
9612
|
}
|
|
9313
9613
|
case "list_group_milestones": {
|
|
9614
|
+
rejectIfStrictProjectScope("list_group_milestones");
|
|
9314
9615
|
const { group_id, ...options } = ListGroupMilestonesSchema.parse(params.arguments);
|
|
9315
9616
|
const milestones = await listGroupMilestones(group_id, options);
|
|
9316
9617
|
return {
|
|
@@ -9323,6 +9624,7 @@ async function handleToolCall(params) {
|
|
|
9323
9624
|
};
|
|
9324
9625
|
}
|
|
9325
9626
|
case "get_group_milestone": {
|
|
9627
|
+
rejectIfStrictProjectScope("get_group_milestone");
|
|
9326
9628
|
const { group_id, milestone_id } = GetGroupMilestoneSchema.parse(params.arguments);
|
|
9327
9629
|
const milestone = await getGroupMilestone(group_id, milestone_id);
|
|
9328
9630
|
return {
|
|
@@ -9335,6 +9637,7 @@ async function handleToolCall(params) {
|
|
|
9335
9637
|
};
|
|
9336
9638
|
}
|
|
9337
9639
|
case "create_group_milestone": {
|
|
9640
|
+
rejectIfStrictProjectScope("create_group_milestone");
|
|
9338
9641
|
const { group_id, ...options } = CreateGroupMilestoneSchema.parse(params.arguments);
|
|
9339
9642
|
const milestone = await createGroupMilestone(group_id, options);
|
|
9340
9643
|
return {
|
|
@@ -9347,6 +9650,7 @@ async function handleToolCall(params) {
|
|
|
9347
9650
|
};
|
|
9348
9651
|
}
|
|
9349
9652
|
case "edit_group_milestone": {
|
|
9653
|
+
rejectIfStrictProjectScope("edit_group_milestone");
|
|
9350
9654
|
const { group_id, milestone_id, ...options } = EditGroupMilestoneSchema.parse(params.arguments);
|
|
9351
9655
|
const milestone = await editGroupMilestone(group_id, milestone_id, options);
|
|
9352
9656
|
return {
|
|
@@ -9359,6 +9663,7 @@ async function handleToolCall(params) {
|
|
|
9359
9663
|
};
|
|
9360
9664
|
}
|
|
9361
9665
|
case "delete_group_milestone": {
|
|
9666
|
+
rejectIfStrictProjectScope("delete_group_milestone");
|
|
9362
9667
|
const { group_id, milestone_id } = DeleteGroupMilestoneSchema.parse(params.arguments);
|
|
9363
9668
|
await deleteGroupMilestone(group_id, milestone_id);
|
|
9364
9669
|
return {
|
|
@@ -9374,6 +9679,7 @@ async function handleToolCall(params) {
|
|
|
9374
9679
|
};
|
|
9375
9680
|
}
|
|
9376
9681
|
case "get_group_milestone_issue": {
|
|
9682
|
+
rejectIfStrictProjectScope("get_group_milestone_issue");
|
|
9377
9683
|
const { group_id, milestone_id, ...options } = GetGroupMilestoneIssuesSchema.parse(params.arguments);
|
|
9378
9684
|
const issues = await getGroupMilestoneIssues(group_id, milestone_id, options);
|
|
9379
9685
|
return {
|
|
@@ -9386,6 +9692,7 @@ async function handleToolCall(params) {
|
|
|
9386
9692
|
};
|
|
9387
9693
|
}
|
|
9388
9694
|
case "get_group_milestone_merge_requests": {
|
|
9695
|
+
rejectIfStrictProjectScope("get_group_milestone_merge_requests");
|
|
9389
9696
|
const { group_id, milestone_id, ...options } = GetGroupMilestoneMergeRequestsSchema.parse(params.arguments);
|
|
9390
9697
|
const mergeRequests = await getGroupMilestoneMergeRequests(group_id, milestone_id, options);
|
|
9391
9698
|
return {
|
|
@@ -9398,6 +9705,7 @@ async function handleToolCall(params) {
|
|
|
9398
9705
|
};
|
|
9399
9706
|
}
|
|
9400
9707
|
case "get_group_milestone_burndown_events": {
|
|
9708
|
+
rejectIfStrictProjectScope("get_group_milestone_burndown_events");
|
|
9401
9709
|
const { group_id, milestone_id, ...options } = GetGroupMilestoneBurndownEventsSchema.parse(params.arguments);
|
|
9402
9710
|
const events = await getGroupMilestoneBurndownEvents(group_id, milestone_id, options);
|
|
9403
9711
|
return {
|
|
@@ -9455,6 +9763,7 @@ async function handleToolCall(params) {
|
|
|
9455
9763
|
};
|
|
9456
9764
|
}
|
|
9457
9765
|
case "list_group_iterations": {
|
|
9766
|
+
rejectIfStrictProjectScope("list_group_iterations");
|
|
9458
9767
|
const args = ListGroupIterationsSchema.parse(params.arguments);
|
|
9459
9768
|
const iterations = await listGroupIterations(args.group_id, args);
|
|
9460
9769
|
return {
|
|
@@ -9653,6 +9962,7 @@ async function handleToolCall(params) {
|
|
|
9653
9962
|
};
|
|
9654
9963
|
}
|
|
9655
9964
|
case "list_events": {
|
|
9965
|
+
rejectIfStrictProjectScope("list_events");
|
|
9656
9966
|
const args = ListEventsSchema.parse(params.arguments);
|
|
9657
9967
|
const events = await listEvents(args);
|
|
9658
9968
|
return {
|
|
@@ -10248,7 +10558,28 @@ async function startStreamableHTTPServer() {
|
|
|
10248
10558
|
const privateToken = req.headers["private-token"] || "";
|
|
10249
10559
|
const jobToken = req.headers["job-token"] || "";
|
|
10250
10560
|
const dynamicApiUrl = req.headers["x-gitlab-api-url"]?.trim();
|
|
10561
|
+
const requestedProjectScope = req.headers["x-gitlab-allowed-project-ids"]?.trim();
|
|
10251
10562
|
let apiUrl = GITLAB_API_URL; // Default API URL
|
|
10563
|
+
let allowedProjectIds;
|
|
10564
|
+
// Only process the requested project scope if the feature is enabled
|
|
10565
|
+
if (ENABLE_DYNAMIC_PROJECT_SCOPE && requestedProjectScope !== undefined) {
|
|
10566
|
+
const requested = requestedProjectScope
|
|
10567
|
+
.split(",")
|
|
10568
|
+
.map(id => id.trim())
|
|
10569
|
+
.filter(Boolean);
|
|
10570
|
+
if (requested.length === 0) {
|
|
10571
|
+
logger.warn("Empty X-GitLab-Allowed-Project-Ids provided");
|
|
10572
|
+
return null;
|
|
10573
|
+
}
|
|
10574
|
+
if (GITLAB_ALLOWED_PROJECT_IDS.length > 0) {
|
|
10575
|
+
const outOfScope = requested.filter(id => !GITLAB_ALLOWED_PROJECT_IDS.includes(id));
|
|
10576
|
+
if (outOfScope.length > 0) {
|
|
10577
|
+
logger.warn(`X-GitLab-Allowed-Project-Ids requested projects outside GITLAB_ALLOWED_PROJECT_IDS: ${outOfScope.join(", ")}`);
|
|
10578
|
+
return null; // Reject if the header tries to widen the configured allowlist
|
|
10579
|
+
}
|
|
10580
|
+
}
|
|
10581
|
+
allowedProjectIds = requested;
|
|
10582
|
+
}
|
|
10252
10583
|
// Only process dynamic URL if the feature is enabled
|
|
10253
10584
|
if (ENABLE_DYNAMIC_API_URL && dynamicApiUrl) {
|
|
10254
10585
|
try {
|
|
@@ -10284,7 +10615,7 @@ async function startStreamableHTTPServer() {
|
|
|
10284
10615
|
}
|
|
10285
10616
|
// Validate token and return AuthData object
|
|
10286
10617
|
if (token && header && validateToken(token)) {
|
|
10287
|
-
return { header, token, lastUsed: Date.now(), apiUrl };
|
|
10618
|
+
return { header, token, lastUsed: Date.now(), apiUrl, allowedProjectIds };
|
|
10288
10619
|
}
|
|
10289
10620
|
return null;
|
|
10290
10621
|
};
|
|
@@ -10434,6 +10765,7 @@ async function startStreamableHTTPServer() {
|
|
|
10434
10765
|
header: headerAuth.header,
|
|
10435
10766
|
token: headerAuth.token,
|
|
10436
10767
|
apiUrl: headerAuth.apiUrl,
|
|
10768
|
+
allowedProjectIds: headerAuth.allowedProjectIds,
|
|
10437
10769
|
};
|
|
10438
10770
|
freshAuthPresent = true;
|
|
10439
10771
|
}
|
|
@@ -10462,7 +10794,12 @@ async function startStreamableHTTPServer() {
|
|
|
10462
10794
|
if (looksLikeStatelessSessionId(incomingSid)) {
|
|
10463
10795
|
const opened = openSessionId(material, incomingSid, sessionTtlSeconds);
|
|
10464
10796
|
if (opened) {
|
|
10465
|
-
effective = {
|
|
10797
|
+
effective = {
|
|
10798
|
+
header: opened.h,
|
|
10799
|
+
token: opened.t,
|
|
10800
|
+
apiUrl: opened.u,
|
|
10801
|
+
allowedProjectIds: opened.p,
|
|
10802
|
+
};
|
|
10466
10803
|
metrics.statelessAuthFromSealedSid++;
|
|
10467
10804
|
}
|
|
10468
10805
|
else {
|
|
@@ -10508,6 +10845,7 @@ async function startStreamableHTTPServer() {
|
|
|
10508
10845
|
header: effective.header,
|
|
10509
10846
|
token: effective.token,
|
|
10510
10847
|
apiUrl: effective.apiUrl,
|
|
10848
|
+
allowedProjectIds: effective.allowedProjectIds,
|
|
10511
10849
|
});
|
|
10512
10850
|
if (freshAuthPresent) {
|
|
10513
10851
|
metrics.statelessSidRotated++;
|
|
@@ -10526,6 +10864,7 @@ async function startStreamableHTTPServer() {
|
|
|
10526
10864
|
lastUsed: Date.now(),
|
|
10527
10865
|
apiUrl: effective.apiUrl,
|
|
10528
10866
|
publicBaseUrl: getForwardedPublicBaseUrl(req, MCP_TRUST_PROXY),
|
|
10867
|
+
allowedProjectIds: effective.allowedProjectIds,
|
|
10529
10868
|
};
|
|
10530
10869
|
// Step 4: create a fresh transport per request.
|
|
10531
10870
|
// DNS rebinding protection is enforced by requireMcpHostAndOrigin middleware;
|
|
@@ -11052,6 +11391,7 @@ async function startStreamableHTTPServer() {
|
|
|
11052
11391
|
lastUsed: authData.lastUsed,
|
|
11053
11392
|
apiUrl: authData.apiUrl,
|
|
11054
11393
|
publicBaseUrl: authData.publicBaseUrl,
|
|
11394
|
+
allowedProjectIds: authData.allowedProjectIds,
|
|
11055
11395
|
};
|
|
11056
11396
|
// Run the entire request handling within AsyncLocalStorage context
|
|
11057
11397
|
await sessionAuthStore.run(ctx, handleRequest);
|
|
@@ -11130,6 +11470,7 @@ async function startStreamableHTTPServer() {
|
|
|
11130
11470
|
lastUsed: authData.lastUsed,
|
|
11131
11471
|
apiUrl: authData.apiUrl,
|
|
11132
11472
|
publicBaseUrl: authData.publicBaseUrl,
|
|
11473
|
+
allowedProjectIds: authData.allowedProjectIds,
|
|
11133
11474
|
};
|
|
11134
11475
|
await sessionAuthStore.run(ctx, handleGetRequest);
|
|
11135
11476
|
}
|