Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ func IssueRead(t translations.TranslationHelperFunc) inventory.ServerTool {
result, err := GetIssueParent(ctx, gqlClient, deps, owner, repo, issueNumber)
return attachIFC(result), nil, err
case "get_labels":
result, err := GetIssueLabels(ctx, gqlClient, owner, repo, issueNumber)
result, err := GetIssueLabels(ctx, gqlClient, deps, owner, repo, issueNumber)
return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
Expand Down Expand Up @@ -1015,11 +1015,20 @@ func GetIssueParent(ctx context.Context, client *githubv4.Client, deps ToolDepen
}), nil
}

func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
func GetIssueLabels(ctx context.Context, client *githubv4.Client, deps ToolDependencies, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
cache, err := deps.GetRepoAccessCache(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get repo access cache: %w", err)
}
flags := deps.GetFlags(ctx)

// Get current labels on the issue using GraphQL
var query struct {
Repository struct {
Issue struct {
Author struct {
Login githubv4.String
}
Labels struct {
Nodes []struct {
ID githubv4.ID
Expand All @@ -1043,6 +1052,12 @@ func GetIssueLabels(ctx context.Context, client *githubv4.Client, owner string,
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "Failed to get issue labels", err), nil
}

if flags.LockdownMode {
if restricted, err := authorLockdownResult(ctx, cache, owner, repo, string(query.Repository.Issue.Author.Login), lockdownIssueRestrictedMessage); restricted != nil || err != nil {
return restricted, err
}
}

// Extract label information
issueLabels := make([]map[string]any, len(query.Repository.Issue.Labels.Nodes))
for i, label := range query.Repository.Issue.Labels.Nodes {
Expand Down
128 changes: 85 additions & 43 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3686,10 +3686,61 @@ func Test_GetIssueLabels(t *testing.T) {
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number"})

labelsMockClient := func(authorLogin string) *http.Client {
return githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Issue struct {
Author struct {
Login githubv4.String
}
Labels struct {
Nodes []struct {
ID githubv4.ID
Name githubv4.String
Color githubv4.String
Description githubv4.String
}
TotalCount githubv4.Int
} `graphql:"labels(first: 100)"`
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"issueNumber": githubv4.Int(123),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"issue": map[string]any{
"author": map[string]any{
"login": githubv4.String(authorLogin),
},
"labels": map[string]any{
"nodes": []any{
map[string]any{
"id": githubv4.ID("label-1"),
"name": githubv4.String("bug"),
"color": githubv4.String("d73a4a"),
"description": githubv4.String("Something isn't working"),
},
},
"totalCount": githubv4.Int(1),
},
},
},
}),
),
)
}

tests := []struct {
name string
requestArgs map[string]any
mockedClient *http.Client
lockdownEnabled bool
expectToolError bool
expectedToolErrMsg string
}{
Expand All @@ -3701,47 +3752,32 @@ func Test_GetIssueLabels(t *testing.T) {
"repo": "repo",
"issue_number": float64(123),
},
mockedClient: githubv4mock.NewMockedHTTPClient(
githubv4mock.NewQueryMatcher(
struct {
Repository struct {
Issue struct {
Labels struct {
Nodes []struct {
ID githubv4.ID
Name githubv4.String
Color githubv4.String
Description githubv4.String
}
TotalCount githubv4.Int
} `graphql:"labels(first: 100)"`
} `graphql:"issue(number: $issueNumber)"`
} `graphql:"repository(owner: $owner, name: $repo)"`
}{},
map[string]any{
"owner": githubv4.String("owner"),
"repo": githubv4.String("repo"),
"issueNumber": githubv4.Int(123),
},
githubv4mock.DataResponse(map[string]any{
"repository": map[string]any{
"issue": map[string]any{
"labels": map[string]any{
"nodes": []any{
map[string]any{
"id": githubv4.ID("label-1"),
"name": githubv4.String("bug"),
"color": githubv4.String("d73a4a"),
"description": githubv4.String("Something isn't working"),
},
},
"totalCount": githubv4.Int(1),
},
},
},
}),
),
),
mockedClient: labelsMockClient("author"),
expectToolError: false,
},
{
name: "lockdown enabled - author lacks push access",
requestArgs: map[string]any{
"method": "get_labels",
"owner": "owner",
"repo": "repo",
"issue_number": float64(123),
},
mockedClient: labelsMockClient("external-user"),
lockdownEnabled: true,
expectToolError: true,
expectedToolErrMsg: "access to issue details is restricted by lockdown mode",
},
{
name: "lockdown enabled - author has push access",
requestArgs: map[string]any{
"method": "get_labels",
"owner": "owner",
"repo": "repo",
"issue_number": float64(123),
},
mockedClient: labelsMockClient("maintainer"),
lockdownEnabled: true,
expectToolError: false,
},
}
Expand All @@ -3750,11 +3786,17 @@ func Test_GetIssueLabels(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
gqlClient := githubv4.NewClient(tc.mockedClient)
client := mustNewGHClient(t, nil)

var restClient *github.Client
if tc.lockdownEnabled {
restClient = mockRESTPermissionServer(t, "read", map[string]string{"maintainer": "write"})
}

deps := BaseDeps{
Client: client,
GQLClient: gqlClient,
RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
RepoAccessCache: stubRepoAccessCache(restClient, 15*time.Minute),
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled}),
}
handler := serverTool.Handler(deps)

Expand Down
Loading