Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions server/gitlab/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ func (g *gitlab) GetProject(ctx context.Context, user *UserInfo, token *oauth2.T
if err != nil {
return nil, err
}
if err = g.checkGroup(project.PathWithNamespace); err != nil {
return nil, err
}

return project, nil
}
Expand Down
67 changes: 67 additions & 0 deletions server/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
Expand All @@ -20,7 +22,9 @@
"github.com/stretchr/testify/require"
internGitlab "github.com/xanzy/go-gitlab"
gomock "go.uber.org/mock/gomock"
"golang.org/x/oauth2"

gitlabapi "github.com/mattermost/mattermost-plugin-gitlab/server/gitlab"
mockgitlab "github.com/mattermost/mattermost-plugin-gitlab/server/mocks"
)

Expand Down Expand Up @@ -394,6 +398,69 @@
}
}

// --- GetProject group-containment gate tests --------------------------------

func TestGetProject_GroupGate(t *testing.T) {
newProjectServer := func(projectPath string) *httptest.Server {
h := http.NewServeMux()
h.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
if req.Method == http.MethodGet && req.URL.Path == "/api/v4/projects/"+projectPath {
_ = json.NewEncoder(w).Encode(map[string]interface{}{

Check failure on line 408 in server/mcp_test.go

View workflow job for this annotation

GitHub Actions / plugin-ci / lint

File is not properly formatted (gofmt)
"id": 4242,
"name": "repo",
"path_with_namespace": projectPath,
"visibility": "private",
"default_branch": "main",
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return
}
http.NotFound(w, req)
})
return httptest.NewServer(h)
}

newClient := func(group, projectPath string) (gitlabapi.Gitlab, *httptest.Server) {
server := newProjectServer(projectPath)
checkGroup := func(ns string) error {
p := &Plugin{}
p.configuration = &configuration{GitlabGroup: group}
return p.isNamespaceAllowed(ns)
}
u, _ := url.Parse(server.URL)
return gitlabapi.New(u.String(), group, checkGroup), server
}

ctx := context.Background()
info := &gitlabapi.UserInfo{UserID: "mm-user"}
token := &oauth2.Token{AccessToken: "tok"}

t.Run("out-of-group project blocked when group configured", func(t *testing.T) {
client, server := newClient("allowed-group", "external-corp/private-secrets")
defer server.Close()
project, err := client.GetProject(ctx, info, token, "external-corp", "private-secrets")
require.Error(t, err)
assert.Nil(t, project)
})

t.Run("in-group project allowed when group configured", func(t *testing.T) {
client, server := newClient("allowed-group", "allowed-group/my-repo")
defer server.Close()
project, err := client.GetProject(ctx, info, token, "allowed-group", "my-repo")
require.NoError(t, err)
require.NotNil(t, project)
assert.Equal(t, "allowed-group/my-repo", project.PathWithNamespace)
})

t.Run("no group configured allows any project", func(t *testing.T) {
client, server := newClient("", "external-corp/private-secrets")
defer server.Close()
project, err := client.GetProject(ctx, info, token, "external-corp", "private-secrets")
require.NoError(t, err)
require.NotNil(t, project)
assert.Equal(t, "external-corp/private-secrets", project.PathWithNamespace)
})
}

// --- Conversion helper tests ------------------------------------------------

func TestIssueToSummary(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions server/permalinks.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func (p *Plugin) processReplacement(r replacement, glClient *gitlab.Client, wg *
}
projectPath := fmt.Sprintf("%s/%s", r.permalinkData.user, r.permalinkData.repo)

// Skip previews for projects outside the configured GitLab group.
if err := p.isNamespaceAllowed(projectPath); err != nil {
return
}

// Check if the project is public
if p.getConfiguration().EnableCodePreview == "public" {
repo, _, err := glClient.Projects.GetProject(projectPath, &gitlab.GetProjectOptions{})
Expand Down
46 changes: 46 additions & 0 deletions server/permalinks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,52 @@ func TestMakeReplacements(t *testing.T) {
}
}

func TestMakeReplacementsNamespaceGate(t *testing.T) {
input := "start https://gitlab.com/mattermost/mattermost-server/-/blob/cbb25838a61872b624ac512556d7bc932486a64c/app/authentication.go#L15-L22 lorem ipsum"

newPlugin := func(group string) *Plugin {
p := NewPlugin()
p.configuration = &configuration{
EnableCodePreview: "privateAndPublic",
GitlabGroup: group,
}
mockPluginAPI := &plugintest.API{}
mockPluginAPI.On("LogDebug", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Maybe()
mockPluginAPI.On("LogWarn", mock.Anything, mock.Anything).Maybe()
p.SetAPI(mockPluginAPI)
return p
}

client, closer := getClient()
defer closer()

t.Run("out-of-group project skips preview when group configured", func(t *testing.T) {
p := newPlugin("other-group")
replacements := p.getPermalinkReplacements(input)
require.Len(t, replacements, 1)
out := p.makeReplacements(input, replacements, client)
assert.Equal(t, input, out, "out-of-group permalink must be left untouched")
})

t.Run("in-group project still renders preview", func(t *testing.T) {
p := newPlugin("mattermost")
replacements := p.getPermalinkReplacements(input)
require.Len(t, replacements, 1)
out := p.makeReplacements(input, replacements, client)
assert.NotEqual(t, input, out, "in-group permalink must be expanded")
assert.Contains(t, out, "TokenLocation")
})

t.Run("no group configured renders preview", func(t *testing.T) {
p := newPlugin("")
replacements := p.getPermalinkReplacements(input)
require.Len(t, replacements, 1)
out := p.makeReplacements(input, replacements, client)
assert.NotEqual(t, input, out, "with no group configured previews must still work")
assert.Contains(t, out, "TokenLocation")
})
}

const (
baseURLPath = "/api/v4"
requestURLPath = "/api/v4/projects/mattermost/mattermost-server/repository/files/app/authentication.go"
Expand Down
Loading