mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
185 lines
7.3 KiB
Go
185 lines
7.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
|
|
auth_model "github.com/hanzoai/git/models/auth"
|
|
"github.com/hanzoai/git/models/organization"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
"github.com/hanzoai/git/models/unittest"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/tests"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestAPIUserReposPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
|
req := NewRequest(t, "GET", "/v1/user/repos").
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
var repos []api.Repository
|
|
DecodeJSON(t, resp, &repos)
|
|
assert.NotEmpty(t, repos)
|
|
for _, repo := range repos {
|
|
assert.False(t, repo.Private)
|
|
}
|
|
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
|
|
|
req = NewRequest(t, "GET", "/v1/users/user2/repos").
|
|
AddTokenAuth(token)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &repos)
|
|
assert.NotEmpty(t, repos)
|
|
for _, repo := range repos {
|
|
assert.False(t, repo.Private)
|
|
}
|
|
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
|
}
|
|
|
|
func repoNames(repos []api.Repository) []string {
|
|
names := make([]string, 0, len(repos))
|
|
for _, repo := range repos {
|
|
names = append(names, repo.FullName)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestAPIRepoByIDPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
|
req := NewRequest(t, "GET", "/v1/repositories/1").
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "GET", "/v1/repositories/2").
|
|
AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
|
|
req := NewRequest(t, "GET", "/v1/users/user2/activities/feeds").
|
|
AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
var activities []api.Activity
|
|
DecodeJSON(t, resp, &activities)
|
|
assert.NotEmpty(t, activities)
|
|
|
|
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequest(t, "GET", "/v1/users/user2/activities/feeds").
|
|
AddTokenAuth(publicToken)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &activities)
|
|
assertPublicActivitiesOnly(t, activities)
|
|
|
|
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
|
|
req = NewRequest(t, "GET", "/v1/orgs/org3/activities/feeds").
|
|
AddTokenAuth(orgToken)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &activities)
|
|
assert.NotEmpty(t, activities)
|
|
|
|
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequest(t, "GET", "/v1/orgs/org3/activities/feeds").
|
|
AddTokenAuth(publicOrgToken)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
DecodeJSON(t, resp, &activities)
|
|
assertPublicActivitiesOnly(t, activities)
|
|
}
|
|
|
|
func TestAPIOrgPermissionsPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// user2 is a member of the private org private_org35
|
|
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "private_org35"})
|
|
|
|
// a full org-scoped token can read the membership permissions
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
|
req := NewRequestf(t, "GET", "/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
// a public-only token must not disclose permissions for a private org
|
|
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequestf(t, "GET", "/v1/users/user2/orgs/%s/permissions", org.Name).AddTokenAuth(publicToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func TestAPITeamReposPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// team 1 (Owners of org3) has access to the private repos org3/repo3 and org3/repo5
|
|
team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
|
|
privateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
|
privateRepo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 5})
|
|
|
|
// a full org+repo scoped token sees the private repos
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository)
|
|
req := NewRequestf(t, "GET", "/v1/teams/%d/repos", team.ID).AddTokenAuth(token)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
repos := DecodeJSON(t, resp, []api.Repository{})
|
|
assert.Contains(t, repoNames(repos), privateRepo.FullName())
|
|
|
|
// a public-only token must not receive any private repo
|
|
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
|
req = NewRequestf(t, "GET", "/v1/teams/%d/repos", team.ID).AddTokenAuth(publicToken)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
repos = DecodeJSON(t, resp, []api.Repository{})
|
|
for _, repo := range repos {
|
|
assert.False(t, repo.Private)
|
|
}
|
|
assert.NotContains(t, repoNames(repos), privateRepo.FullName())
|
|
assert.NotContains(t, repoNames(repos), privateRepo2.FullName())
|
|
// the total-count header must match the filtered page, otherwise it leaks the
|
|
// number of hidden private repos
|
|
assert.Equal(t, strconv.Itoa(len(repos)), resp.Header().Get("X-Total-Count"))
|
|
|
|
// the single-repo endpoint must not confirm a private repo for a public-only token
|
|
req = NewRequestf(t, "GET", "/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
req = NewRequestf(t, "GET", "/v1/teams/%d/repos/%s", team.ID, privateRepo.FullName()).AddTokenAuth(publicToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
|
|
t.Helper()
|
|
|
|
for _, activity := range activities {
|
|
assert.False(t, activity.IsPrivate)
|
|
if activity.Repo != nil {
|
|
assert.False(t, activity.Repo.Private)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAPIRepoLimitedOwnerPublicOnly ensures a public-only token cannot reach a public repo
|
|
// owned by a limited-visibility owner (which is not reachable anonymously).
|
|
func TestAPIRepoLimitedOwnerPublicOnly(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
// limited_org (limited visibility) owns the non-private repo public_repo_on_limited_org
|
|
const url = "/v1/repos/limited_org/public_repo_on_limited_org"
|
|
|
|
// a public-only token is confined to genuinely public resources
|
|
publicOnlyToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
|
req := NewRequest(t, "GET", url).AddTokenAuth(publicOnlyToken)
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
|
|
// a normal token can still reach the limited owner's public repo
|
|
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
|
req = NewRequest(t, "GET", url).AddTokenAuth(token)
|
|
MakeRequest(t, req, http.StatusOK)
|
|
}
|