Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to teploy are documented here. Format follows [Keep a Change

## [Unreleased]

### Fixed

- Native repository proof for a CI checkout (`trigger` with a GitHub or
GitLab repository ID) now compares host and path only. A checkout over
`ssh://git@gitlab.lan:2222/g/r` matches the API's
`http://gitlab.lan:8080/g/r` instead of being refused; the API authority
stays pinned by `TEPLOY_TRIGGER_API_BASE`.

## [0.1.45] - 2026-10-20

### Upgrade notes
Expand Down
13 changes: 12 additions & 1 deletion internal/cli/trigger_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ func canonicalRepository(raw string) (string, error) {
}
return u.String(), nil
}
// sameRepositoryPath compares two canonical repositories by host and path.
// The forge API reports its HTTP clone URL, while a checkout may use SSH on
// a different port (self-hosted forges almost always do); the transport
// and port are not the repository. The API itself is authority-pinned, so
// only the locator spelling is relaxed here.
func sameRepositoryPath(a, b string) bool {
ua, ea := url.Parse(a)
ub, eb := url.Parse(b)
return ea == nil && eb == nil && ua.Hostname() != "" &&
ua.Hostname() == ub.Hostname() && ua.Path == ub.Path
}
func checkoutRepository(dir string) (string, error) {
out, err := exec.Command("git", "-C", dir, "config", "--get", "remote.origin.url").Output()
if err != nil {
Expand Down Expand Up @@ -158,7 +169,7 @@ func verifyRepositoryMapping(ctx context.Context, identity, clone string, transp
locator = metadata.HTTPURL
}
actual, err := canonicalRepository(locator)
if err != nil || actual != clone {
if err != nil || !sameRepositoryPath(actual, clone) {
return fmt.Errorf("authenticated API repository does not map to checkout origin")
}
return nil
Expand Down
43 changes: 43 additions & 0 deletions internal/cli/trigger_identity_transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cli

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)

func TestRepositoryMappingIgnoresCheckoutTransport(t *testing.T) {
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/projects/5" || r.Header.Get("PRIVATE-TOKEN") != "tok" {
http.NotFound(w, r)
return
}
w.Write([]byte(`{"id":5,"http_url_to_repo":"http://gitlab.lan:8080/group/app.git"}`))
}))
defer api.Close()
t.Setenv("TEPLOY_TRIGGER_API_BASE", api.URL)
t.Setenv("TEPLOY_TRIGGER_FORGE", "gitlab")
t.Setenv("TEPLOY_TRIGGER_ALLOW_HTTP", "1")
t.Setenv("TEPLOY_TRIGGER_API_TOKEN", "tok")
identity, _ := json.Marshal([]string{"gitlab", api.URL, "id:5"})

check := func(remote string) error {
clone, err := canonicalRepository(remote)
if err != nil {
t.Fatal(err)
}
return verifyRepositoryMapping(context.Background(), string(identity), clone, http.DefaultTransport)
}
// The checkout uses SSH on the forge's SSH port; the API reports HTTP.
if err := check("ssh://git@gitlab.lan:2222/group/app.git"); err != nil {
t.Fatalf("ssh checkout of the same repository refused: %v", err)
}
if err := check("ssh://git@gitlab.lan:2222/group/other.git"); err == nil {
t.Fatal("checkout of another repository accepted")
}
if err := check("ssh://git@elsewhere.lan:2222/group/app.git"); err == nil {
t.Fatal("checkout on another host accepted")
}
}
Loading