forgejo-mcp/operation/org/membership.go
Byte Flavour 4969ee5881
feat: robust stateless auth with security fixes and improved tests
This commit addresses the review feedback on PR #138:
- Fixed a privilege escalation vulnerability where failed ephemeral client
  creation would silently fall back to the global singleton token.
- Refactored forgejo.Client(ctx) to return (*forgejo.Client, error).
- Updated all 100+ tool handlers to properly handle client initialization errors.
- Improved token extraction to handle schemes (token/bearer) case-insensitively.
- Enhanced Unit Tests (pkg/forgejo/forgejo_test.go) to verify that different
  concurrent requests with different tokens correctly use their respective
  tokens in the Authorization header.
- Updated README and demos to reflect case-insensitivity support.
2026-05-24 13:44:48 +02:00

125 lines
3.7 KiB
Go

package org
import (
"context"
"errors"
"fmt"
"codeberg.org/goern/forgejo-mcp/v2/operation/params"
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
"codeberg.org/goern/forgejo-mcp/v2/pkg/log"
"codeberg.org/goern/forgejo-mcp/v2/pkg/to"
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/mark3labs/mcp-go/mcp"
)
const (
ListOrgMembersToolName = "list_org_members"
CheckOrgMembershipToolName = "check_org_membership"
RemoveOrgMemberToolName = "remove_org_member"
)
var (
ListOrgMembersTool = mcp.NewTool(
ListOrgMembersToolName,
mcp.WithDescription("List members of an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
mcp.WithNumber("page", mcp.Required(), mcp.Description(params.Page), mcp.DefaultNumber(1), mcp.Min(1)),
mcp.WithNumber("limit", mcp.Required(), mcp.Description(params.Limit), mcp.DefaultNumber(100), mcp.Min(1)),
)
CheckOrgMembershipTool = mcp.NewTool(
CheckOrgMembershipToolName,
mcp.WithDescription("Check if a user is a member of an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
mcp.WithString("user", mcp.Required(), mcp.Description(params.User)),
)
RemoveOrgMemberTool = mcp.NewTool(
RemoveOrgMemberToolName,
mcp.WithDescription("Remove a member from an organization"),
mcp.WithString("org", mcp.Required(), mcp.Description(params.Org)),
mcp.WithString("user", mcp.Required(), mcp.Description(params.User)),
)
)
func ListOrgMembersFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called ListOrgMembersFn")
orgName, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("organization name is required"))
}
page, ok := req.GetArguments()["page"].(float64)
if !ok {
page = 1
}
limit, ok := req.GetArguments()["limit"].(float64)
if !ok {
limit = 100
}
opt := forgejo_sdk.ListOrgMembershipOption{
ListOptions: forgejo_sdk.ListOptions{
Page: int(page),
PageSize: int(limit),
},
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
members, _, err := client.ListOrgMembership(orgName, opt)
if err != nil {
return to.ErrorResult(fmt.Errorf("list org members err: %v", err))
}
return to.TextResult(members)
}
func CheckOrgMembershipFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called CheckOrgMembershipFn")
orgName, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("organization name is required"))
}
user, ok := req.GetArguments()["user"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
isMember, _, err := client.CheckOrgMembership(orgName, user)
if err != nil {
return to.ErrorResult(fmt.Errorf("check org membership err: %v", err))
}
return to.TextResult(map[string]interface{}{
"org": orgName,
"user": user,
"is_member": isMember,
})
}
func RemoveOrgMemberFn(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
log.Debugf("Called RemoveOrgMemberFn")
orgName, ok := req.GetArguments()["org"].(string)
if !ok {
return to.ErrorResult(errors.New("organization name is required"))
}
user, ok := req.GetArguments()["user"].(string)
if !ok {
return to.ErrorResult(errors.New("username is required"))
}
client, err := forgejo.Client(ctx)
if err != nil {
return to.ErrorResult(err)
}
_, err = client.DeleteOrgMembership(orgName, user)
if err != nil {
return to.ErrorResult(fmt.Errorf("remove org member err: %v", err))
}
return to.TextResult(fmt.Sprintf("User '%s' removed from organization '%s'", user, orgName))
}