Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/resolve-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default (
CI_PROJECT_URL,
CI_PROJECT_PATH,
CI_API_V4_URL,
CI_API_GRAPHQL_URL,
GL_TOKEN,
GITLAB_TOKEN,
GL_URL,
Expand Down Expand Up @@ -59,6 +60,11 @@ export default (
: service === "gitlab" && CI_API_V4_URL
? CI_API_V4_URL
: urlJoin(defaultedGitlabUrl, isNil(userGitlabApiPathPrefix) ? "/api/v4" : userGitlabApiPathPrefix),
gitlabGraphQlApiUrl: userGitlabUrl
? urlJoin(userGitlabUrl, "/graphql")
: service === "gitlab" && CI_API_GRAPHQL_URL
? CI_API_GRAPHQL_URL
: urlJoin(defaultedGitlabUrl, "/graphql"),
assets: assets ? castArray(assets) : assets,
milestones: milestones ? castArray(milestones) : milestones,
successComment,
Expand Down
64 changes: 49 additions & 15 deletions lib/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export default async (pluginConfig, context) => {
options: { repositoryUrl },
logger,
} = context;
const { gitlabToken, gitlabUrl, gitlabApiUrl, proxy, ...options } = resolveConfig(pluginConfig, context);
const { gitlabToken, gitlabUrl, gitlabApiUrl, gitlabGraphQlApiUrl, proxy, ...options } = resolveConfig(
pluginConfig,
context
);
const { projectPath, projectApiUrl } = getProjectContext(context, gitlabUrl, gitlabApiUrl, repositoryUrl);

debug("apiUrl: %o", gitlabApiUrl);
Expand All @@ -54,28 +57,59 @@ export default async (pluginConfig, context) => {
}

if (gitlabToken && projectPath) {
let projectAccess;
let groupAccess;

logger.log("Verify GitLab authentication (%s)", gitlabApiUrl);

try {
({
permissions: { project_access: projectAccess, group_access: groupAccess },
} = await got
// First, get basic project information to ensure the project exists
await got
.get(projectApiUrl, {
headers: { "PRIVATE-TOKEN": gitlabToken },
...proxy,
})
.json());
if (
context.options.dryRun &&
!((projectAccess && projectAccess.access_level >= 10) || (groupAccess && groupAccess.access_level >= 10))
) {
.json();

// Use GraphQL to check user permissions
debug("Checking permissions via GraphQL");
const query = `
query {
project(fullPath: "${projectPath}") {
userPermissions {
pushToRepository
readRepository
}
}
}
`;

const graphqlResponse = await got
.post(gitlabGraphQlApiUrl, {
headers: {
"Private-Token": gitlabToken,
"Content-Type": "application/json",
Accept: "application/graphql-response+json",
},
json: { query },
...proxy,
})
.json();

if (graphqlResponse.errors) {
debug("GraphQL query returned errors: %O", graphqlResponse.errors);
throw new Error(`GraphQL query failed: ${graphqlResponse.errors.map((e) => e.message).join(", ")}`);
}

const permissions = graphqlResponse.data?.project?.userPermissions;
if (!permissions) {
debug("No permissions data returned from GraphQL query");
throw new Error("Unable to determine permissions from GraphQL response");
}

debug("GraphQL permissions: %O", permissions);

// Check permissions based on GraphQL response
if (context.options.dryRun && !permissions.readRepository) {
errors.push(getError("EGLNOPULLPERMISSION", { projectPath }));
} else if (
!((projectAccess && projectAccess.access_level >= 30) || (groupAccess && groupAccess.access_level >= 30))
) {
} else if (!permissions.pushToRepository) {
errors.push(getError("EGLNOPUSHPERMISSION", { projectPath }));
}
} catch (error) {
Expand Down
11 changes: 11 additions & 0 deletions test/resolve-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const defaultOptions = {
gitlabToken: undefined,
gitlabUrl: "https://gitlab.com",
gitlabApiUrl: urlJoin("https://gitlab.com", "/api/v4"),
gitlabGraphQlApiUrl: urlJoin("https://gitlab.com", "/graphql"),
assets: undefined,
milestones: undefined,
successComment: undefined,
Expand Down Expand Up @@ -41,6 +42,7 @@ test("Returns user config", (t) => {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
assets,
labels: false,
retryLimit,
Expand All @@ -54,6 +56,7 @@ test("Returns user config", (t) => {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
assets,
proxy,
}
Expand All @@ -77,6 +80,7 @@ test("Returns user config via environment variables", (t) => {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
assets,
milestones,
}
Expand All @@ -96,6 +100,7 @@ test("Returns user config via alternative environment variables", (t) => {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
assets,
milestones: undefined,
successComment: undefined,
Expand Down Expand Up @@ -223,6 +228,7 @@ test("Returns user config via alternative environment variables with mismatching
gitlabToken: "TOKEN",
gitlabUrl: "http://host.com",
gitlabApiUrl: "http://host.com/api/prefix",
gitlabGraphQlApiUrl: "http://host.com/graphql",
assets: ["file.js"],
}
);
Expand All @@ -245,6 +251,7 @@ test("Returns user config via alternative environment variables with mismatching
gitlabToken: "TOKEN",
gitlabUrl: "https://host.com",
gitlabApiUrl: "https://host.com/api/prefix",
gitlabGraphQlApiUrl: "https://host.com/graphql",
assets: ["file.js"],
}
);
Expand Down Expand Up @@ -387,6 +394,7 @@ test("Returns default config via GitLab CI/CD environment variables", (t) => {
gitlabToken,
gitlabUrl: "http://ci-host.com",
gitlabApiUrl: CI_API_V4_URL,
gitlabGraphQlApiUrl: "http://ci-host.com/graphql",
}
);
});
Expand Down Expand Up @@ -415,6 +423,7 @@ test("Returns user config over GitLab CI/CD environment variables", (t) => {
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
assets,
failTitle: "The automated release unfortunately failed!",
labels: "bot,release-failed",
Expand Down Expand Up @@ -450,6 +459,7 @@ test("Returns user config via environment variables over GitLab CI/CD environmen
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
}
);
});
Expand Down Expand Up @@ -482,6 +492,7 @@ test("Returns user config via alternative environment variables over GitLab CI/C
gitlabToken,
gitlabUrl,
gitlabApiUrl: urlJoin(gitlabUrl, gitlabApiPathPrefix),
gitlabGraphQlApiUrl: urlJoin(gitlabUrl, "/graphql"),
}
);
});
Expand Down
161 changes: 161 additions & 0 deletions test/verify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,38 @@ test.serial("Verify token and repository access (project_access 30)", async (t)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, { permissions: { project_access: { access_level: 30 } } });

const gitlabGraphQl = nock("https://gitlab.com", { reqheaders: { "Private-Token": "gitlab_token" } })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This test case is now redundant with "Verify token and repository access when GraphQL returns sufficient permissions", or am I missing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct! The test "Verify token and repository access (project_access 30)" is redundant with "Verify token and repository access when GraphQL returns sufficient permissions". The first test contains obsolete REST API permission data (project_access: { access_level: 30 }) that's no longer used by the GraphQL-only implementation. It's one of the 6 obsolete tests identified for removal.

.post("/graphql", {
query: `
query {
project(fullPath: "${owner}/${repo}") {
userPermissions {
pushToRepository
readRepository
}
}
}
`,
})
.reply(200, {
data: {
project: {
userPermissions: {
pushToRepository: true,
readRepository: true,
},
},
},
});

await t.notThrowsAsync(
verify(
{},
{ env, options: { repositoryUrl: `git+https://gitalb.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
t.true(gitlabGraphQl.isDone());
});

test.serial("Verify token and repository access (project_access 40)", async (t) => {
Expand Down Expand Up @@ -988,3 +1013,139 @@ test.serial(
t.true(gitlab.isDone());
}
);

test.serial(
"Throw SemanticReleaseError for group access token with null permissions",
async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GL_TOKEN: "group_access_token" };
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, {
permissions: {
project_access: null,
group_access: null,
},
});

const gitlabGraphQl = nock("https://gitlab.com", { reqheaders: { "Private-Token": "group_access_token" } })
.post("/graphql", {
query: `
query {
project(fullPath: "${owner}/${repo}") {
userPermissions {
pushToRepository
readRepository
}
}
}
`,
})
.reply(200, {
data: {
project: {
userPermissions: {
pushToRepository: false,
readRepository: true,
},
},
},
});

const {
errors: [error, ...errors],
} = await t.throwsAsync(
verify(
{},
{ env, options: { repositoryUrl: `https://gitlab.com:${owner}/${repo}.git` }, logger: t.context.logger }
)
);

t.is(errors.length, 0);
t.is(error.name, "SemanticReleaseError");
t.is(error.code, "EGLNOPUSHPERMISSION");
t.true(gitlab.isDone());
t.true(gitlabGraphQl.isDone());
}
);

test.serial(
"Verify token and repository access with null permissions but successful permission test (group access token)",
async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GL_TOKEN: "group_access_token" };
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, {
permissions: {
project_access: null,
group_access: null,
},
});

const gitlabGraphQl = nock("https://gitlab.com", { reqheaders: { "Private-Token": "group_access_token" } })
.post("/graphql", {
query: `
query {
project(fullPath: "${owner}/${repo}") {
userPermissions {
pushToRepository
readRepository
}
}
}
`,
})
.reply(200, {
data: {
project: {
userPermissions: {
pushToRepository: true,
readRepository: true,
},
},
},
});

await t.notThrowsAsync(
verify(
{},
{ env, options: { repositoryUrl: `https://gitlab.com/${owner}/${repo}.git` }, logger: t.context.logger }
)
);
t.true(gitlab.isDone());
t.true(gitlabGraphQl.isDone());
}
);

test.serial("Throw SemanticReleaseError when GraphQL fails and permissions are null", async (t) => {
const owner = "test_user";
const repo = "test_repo";
const env = { GL_TOKEN: "group_access_token" };
const gitlab = authenticate(env)
.get(`/projects/${owner}%2F${repo}`)
.reply(200, {
permissions: {
project_access: null,
group_access: null,
},
});

const gitlabGraphQl = nock("https://gitlab.com", { reqheaders: { "Private-Token": "group_access_token" } })
.post("/graphql")
.reply(500, { error: "Internal server error" });

const {
errors: [error, ...errors],
} = await t.throwsAsync(
verify({}, { env, options: { repositoryUrl: `https://gitlab.com:${owner}/${repo}.git` }, logger: t.context.logger })
);

t.is(errors.length, 0);
t.is(error.name, "SemanticReleaseError");
t.is(error.code, "EGLNOPUSHPERMISSION");
t.true(gitlab.isDone());
t.true(gitlabGraphQl.isDone());
});