Files
zot/pkg/api/authz_internal_test.go

767 lines
23 KiB
Go

package api
import (
"crypto/tls"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/api/constants"
"zotregistry.dev/zot/v2/pkg/log"
reqCtx "zotregistry.dev/zot/v2/pkg/requestcontext"
)
// permitted returns just the bool from AccessController.isPermitted; tests
// that don't care about the deny reason use this for assert readability.
func permitted(ac *AccessController, evalReq *evalRequest, pg config.PolicyGroup) bool {
ok, _ := ac.isPermitted(evalReq, pg)
return ok
}
func TestPolicyConditions(t *testing.T) {
t.Parallel()
pastRFC := time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)
futureRFC := time.Now().Add(time.Hour).UTC().Format(time.RFC3339)
makeAC := func(policies []config.Policy, groups config.Groups) *AccessController {
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: policies},
},
Groups: groups,
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
return &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
}
makeER := func(username, repo string, groups []string) *evalRequest {
uac := reqCtx.NewUserAccessControl()
uac.SetUsername(username)
uac.AddGroups(groups)
return &evalRequest{
userAc: uac,
action: constants.ReadPermission,
repository: repo,
}
}
t.Run("user policy without conditions is permitted", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
}}, nil)
assert.True(t, permitted(ac, makeER("alice", "repo", nil), ac.Config.Repositories["**"]))
})
t.Run("user policy with future-time condition is permitted", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.time < timestamp("` + futureRFC + `")`,
Message: "access expired",
}},
}}, nil)
assert.True(t, permitted(ac, makeER("alice", "repo", nil), ac.Config.Repositories["**"]))
})
t.Run("user policy with past-time condition is denied", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.time < timestamp("` + pastRFC + `")`,
Message: "access expired",
}},
}}, nil)
assert.False(t, permitted(ac, makeER("alice", "repo", nil), ac.Config.Repositories["**"]))
})
t.Run("group policy with failing condition is denied", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Policy{{
Groups: []string{"devs"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.time < timestamp("` + pastRFC + `")`,
Message: "access expired",
}},
}}, config.Groups{"devs": config.Group{Users: []string{"alice"}}})
assert.False(t, permitted(ac, makeER("alice", "repo", []string{"devs"}),
ac.Config.Repositories["**"]))
})
t.Run("condition can reference repository", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.repository.startsWith("prod/")`,
Message: "only prod/* allowed",
}},
}}, nil)
assert.True(t, permitted(ac, makeER("alice", "prod/api", nil), ac.Config.Repositories["**"]))
assert.False(t, permitted(ac, makeER("alice", "staging/api", nil), ac.Config.Repositories["**"]))
})
t.Run("invalid expression fails CompileAccessControl", func(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `this is not valid CEL`,
Message: "broken",
}},
}}},
},
}
_, err := CompileAccessControl(cfg)
assert.Error(t, err)
})
t.Run("uncompiled expression denies (defense in depth)", func(t *testing.T) {
t.Parallel()
// Build the AccessController without going through CompileAccessControl.
// At authz time the lookup misses and the policy is treated as not granting.
ac := &AccessController{
Config: &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
// A unique expression unlikely to be cached by other tests.
Expression: `req.repository == "uncompiled-fixture-only"`,
Message: "no compile",
}},
}}},
},
},
Log: log.NewLogger("debug", ""),
}
assert.False(t, permitted(ac, makeER("alice", "uncompiled-fixture-only", nil),
ac.Config.Repositories["**"]))
})
t.Run("conditional entry contributes glob patterns optimistically", func(t *testing.T) {
t.Parallel()
// Glob computation does NOT evaluate conditions because repo and
// reference are unknown at glob-time. A policy with a condition
// that would deny at request-time still contributes its pattern;
// per-request can() does the real enforcement. Over-listing in
// catalog is the chosen tradeoff over under-listing.
ac := makeAC([]config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.time < timestamp("` + pastRFC + `")`,
Message: "access expired",
}},
}}, nil)
patterns := ac.getGlobPatterns(makeER("alice", "", nil))
assert.True(t, patterns["**"])
})
}
// TestPolicyConditionsRequestFields verifies the full set of req.* fields
// exposed to CEL conditions: HTTP context, TLS, reference parsing, auth
// flags, and OIDC claims passthrough.
func TestPolicyConditionsRequestFields(t *testing.T) {
t.Parallel()
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
uac.SetClaims(map[string]any{
"email": "alice@example.com",
"email_verified": true,
"roles": []any{"prod-pusher", "dev"},
})
httpReq := httptest.NewRequest("PUT", "/v2/prod/api/manifests/v1.2.3", nil)
httpReq.RemoteAddr = "10.0.0.5:54321"
httpReq.Header.Set("User-Agent", "docker/24.0.7")
httpReq.TLS = &tls.ConnectionState{Version: tls.VersionTLS13}
makeER := func() *evalRequest {
return &evalRequest{
httpReq: httpReq,
userAc: uac,
action: constants.ReadPermission,
repository: "prod/api",
reference: "v1.2.3",
}
}
cases := []struct {
name string
expr string
}{
{"method", `req.method == "PUT"`},
{"userAgent", `req.userAgent.startsWith("docker/")`},
{"client.ip", `req.client.ip == "10.0.0.5"`},
{"tls.enabled", `req.tls.enabled`},
{"tls.version", `req.tls.version == "1.3"`},
{"reference (tag)", `req.reference == "v1.2.3"`},
{"referenceType is tag", `req.referenceType == "tag"`},
{"tag set", `req.tag == "v1.2.3"`},
{"digest empty for tag", `req.digest == ""`},
{"auth.anonymous false", `!req.auth.anonymous`},
{"auth.admin false", `!req.auth.admin`},
{"claims passthrough", `req.claims.email_verified == true`},
{"claims list", `"prod-pusher" in req.claims.roles`},
{"action", `req.action == "read"`},
{"repository", `req.repository == "prod/api"`},
{"user.username", `req.user.username == "alice"`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: tc.expr,
Message: "denied",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
assert.True(t, permitted(ac, makeER(), ac.Config.Repositories["**"]), tc.expr)
})
}
}
func TestPolicyConditionsDigestReference(t *testing.T) {
t.Parallel()
digest := "sha256:abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.referenceType == "digest" && req.digest.startsWith("sha256:")`,
Message: "expected digest",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
evalReq := &evalRequest{
userAc: uac,
action: constants.ReadPermission,
repository: "prod/api",
reference: digest,
}
assert.True(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
}
func TestTLSVersionString(t *testing.T) {
t.Parallel()
cases := map[uint16]string{
tls.VersionTLS10: "1.0",
tls.VersionTLS11: "1.1",
tls.VersionTLS12: "1.2",
tls.VersionTLS13: "1.3",
0xffff: "",
}
for v, want := range cases {
if got := tlsVersionString(v); got != want {
t.Errorf("tlsVersionString(%#x) = %q, want %q", v, got, want)
}
}
}
func TestEvalRequestNilSafety(t *testing.T) {
t.Parallel()
var nilER *evalRequest
assert.Equal(t, "", nilER.username())
assert.Nil(t, nilER.groups())
emptyER := &evalRequest{}
assert.Equal(t, "", emptyER.username())
assert.Nil(t, emptyER.groups())
}
func TestCompileAccessControlNilAndAdminPolicy(t *testing.T) {
t.Parallel()
programs, err := CompileAccessControl(nil)
assert.NoError(t, err)
assert.Nil(t, programs)
cfg := &config.AccessControlConfig{
AdminPolicy: config.Policy{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `definitely not valid`,
Message: "broken admin",
}},
},
}
_, err = CompileAccessControl(cfg)
assert.Error(t, err)
assert.Contains(t, err.Error(), "adminPolicy")
}
func TestEvalRequestRemoteAddrWithoutPort(t *testing.T) {
t.Parallel()
httpReq := httptest.NewRequest("GET", "/v2/", nil)
httpReq.RemoteAddr = "no-port-here"
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.client.ip == "no-port-here"`,
Message: "bad ip",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
evalReq := &evalRequest{httpReq: httpReq, userAc: uac, action: constants.ReadPermission, repository: "r"}
assert.True(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
}
func TestPolicyConditionRuntimeTypeError(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.repository`,
Message: "wrong type",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
evalReq := &evalRequest{userAc: uac, action: constants.ReadPermission, repository: "r"}
assert.False(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
}
func TestPolicyGroupNonMatchingNoActionOrGroup(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{
{Groups: []string{"devs"}, Actions: []string{constants.CreatePermission}},
{Groups: []string{"others"}, Actions: []string{constants.ReadPermission}},
}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
uac.AddGroups([]string{"devs"})
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
evalReq := &evalRequest{userAc: uac, action: constants.ReadPermission, repository: "r"}
assert.False(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
}
func TestCompileAccessControlDedupesIdenticalExpressions(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{
{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{
{Expression: `req.repository == "x"`, Message: "first"},
{Expression: `req.repository == "x"`, Message: "duplicate"},
},
},
}},
},
}
programs, err := CompileAccessControl(cfg)
assert.NoError(t, err)
assert.Len(t, programs, 1)
}
func TestNewAccessControllerNilAccessControl(t *testing.T) {
t.Parallel()
cfg := &config.Config{
HTTP: config.HTTPConfig{}, // AccessControl is nil
Log: &config.LogConfig{Level: "debug"},
}
ac := NewAccessController(cfg)
assert.NotNil(t, ac)
assert.NotNil(t, ac.Config)
assert.Empty(t, ac.Config.Repositories)
}
func TestControllerLoadNewConfigRecompilesConditions(t *testing.T) {
t.Parallel()
htp := NewHTPasswd(log.NewLogger("debug", ""))
htw, err := NewHTPasswdWatcher(htp, "")
assert.NoError(t, err)
original := config.New()
original.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{
{Expression: `req.repository == "old"`, Message: "old"},
},
}}},
},
}
originalPrograms, err := CompileAccessControl(original.HTTP.AccessControl)
assert.NoError(t, err)
original.HTTP.AccessControl.StoreCompiledConditions(originalPrograms)
ctlr := &Controller{
Config: original,
Log: log.NewLogger("debug", ""),
HTPasswd: htp,
HTPasswdWatcher: htw,
}
newConfig := config.New()
newConfig.HTTP.AccessControl = &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{
{Expression: `req.repository == "new"`, Message: "new"},
},
}}},
},
}
ctlr.LoadNewConfig(newConfig)
updated := ctlr.Config.HTTP.AccessControl.LoadCompiledConditions()
_, hasNew := updated[`req.repository == "new"`]
assert.True(t, hasNew, "new expression should be compiled after reload")
_, hasOld := updated[`req.repository == "old"`]
assert.False(t, hasOld, "old expression should be evicted after reload")
}
// TestPolicyConditionForwardedFor verifies that req.client.forwardedFor
// exposes the X-Forwarded-For chain split into a list, and that conditions
// can express "trust XFF only when the TCP peer is the configured proxy".
func TestPolicyConditionForwardedFor(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.client.ip == "10.0.0.5" && size(req.client.forwardedFor) > 0 && req.client.forwardedFor[0] == "192.0.2.7"`,
Message: "must arrive via trusted proxy from 192.0.2.7",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
assert.NoError(t, err)
cfg.StoreCompiledConditions(programs)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
mk := func(remoteAddr, xff string) *evalRequest {
req := httptest.NewRequest("GET", "/v2/", nil)
req.RemoteAddr = remoteAddr
if xff != "" {
req.Header.Set("X-Forwarded-For", xff)
}
return &evalRequest{
httpReq: req,
userAc: uac,
action: constants.ReadPermission,
repository: "r",
}
}
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
// Through the trusted proxy with the expected client IP at the head of XFF: granted.
assert.True(t, permitted(ac, mk("10.0.0.5:1234", "192.0.2.7, 10.0.0.5"),
ac.Config.Repositories["**"]))
// Same XFF claim but TCP peer is NOT the trusted proxy: denied (XFF is spoofable).
assert.False(t, permitted(ac, mk("203.0.113.9:1234", "192.0.2.7"),
ac.Config.Repositories["**"]))
// Trusted proxy but no XFF header: denied.
assert.False(t, permitted(ac, mk("10.0.0.5:1234", ""),
ac.Config.Repositories["**"]))
}
// TestPolicyConditionDenyReasonIsSurfaced verifies that when a condition
// denies, the operator-authored Message bubbles up through isPermitted/can
// so the handler can put it in the 403 response detail.
func TestPolicyConditionDenyReasonIsSurfaced(t *testing.T) {
t.Parallel()
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{
Expression: `req.repository.startsWith("prod/")`,
Message: "alice may only read prod/*",
}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
assert.NoError(t, err)
cfg.StoreCompiledConditions(programs)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
ac := &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
evalReq := &evalRequest{userAc: uac, action: constants.ReadPermission, repository: "staging/api"}
ok, reason := ac.isPermitted(evalReq, ac.Config.Repositories["**"])
assert.False(t, ok)
assert.Equal(t, "alice may only read prod/*", reason)
}
// TestSetGlobPatternsOrderIndependence verifies that SetGlobPatterns can be
// called before SetIsAdmin without panicking on a nil internal map. This is a
// regression guard for an earlier ordering hazard between the two setters.
func TestSetGlobPatternsOrderIndependence(t *testing.T) {
t.Parallel()
uac := reqCtx.NewUserAccessControl()
uac.SetGlobPatterns(constants.ReadPermission, map[string]bool{"**": true})
uac.SetIsAdmin(true)
assert.True(t, uac.IsAdmin())
uac2 := reqCtx.NewUserAccessControl()
uac2.SetIsAdmin(true)
uac2.SetGlobPatterns(constants.ReadPermission, map[string]bool{"**": true})
assert.True(t, uac2.IsAdmin())
}
func TestPolicyConditionsAnonymousAndAdminFlags(t *testing.T) {
t.Parallel()
mkAC := func(expr string) *AccessController {
cfg := &config.AccessControlConfig{
Repositories: config.Repositories{
"**": config.PolicyGroup{Policies: []config.Policy{{
Users: []string{"alice", ""},
Actions: []string{constants.ReadPermission},
Conditions: []config.Condition{{Expression: expr, Message: "denied"}},
}}},
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
return &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
}
t.Run("anonymous true when username empty", func(t *testing.T) {
t.Parallel()
ac := mkAC(`req.auth.anonymous`)
uac := reqCtx.NewUserAccessControl()
evalReq := &evalRequest{userAc: uac, action: constants.ReadPermission, repository: "r"}
assert.True(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
})
t.Run("admin reflects evalRequest.isAdmin", func(t *testing.T) {
t.Parallel()
ac := mkAC(`req.auth.admin`)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
evalReq := &evalRequest{userAc: uac, action: constants.ReadPermission, repository: "r", isAdmin: true}
assert.True(t, permitted(ac, evalReq, ac.Config.Repositories["**"]))
})
}
func TestAdminPolicyConditions(t *testing.T) {
t.Parallel()
makeAC := func(adminConditions []config.Condition) *AccessController {
cfg := &config.AccessControlConfig{
AdminPolicy: config.Policy{
Users: []string{"alice"},
Actions: []string{constants.ReadPermission},
Conditions: adminConditions,
},
}
programs, err := CompileAccessControl(cfg)
if err != nil {
t.Fatalf("CompileAccessControl: %v", err)
}
cfg.StoreCompiledConditions(programs)
return &AccessController{Config: cfg, Log: log.NewLogger("debug", "")}
}
httpReqTLS := httptest.NewRequest("GET", "/v2/", nil)
httpReqTLS.TLS = &tls.ConnectionState{Version: tls.VersionTLS13}
httpReqPlain := httptest.NewRequest("GET", "/v2/", nil)
uac := reqCtx.NewUserAccessControl()
uac.SetUsername("alice")
t.Run("admin without conditions is permitted (existing behavior)", func(t *testing.T) {
t.Parallel()
ac := makeAC(nil)
can, reason := ac.can(httpReqPlain, uac, constants.ReadPermission, "any/repo", "ref")
assert.True(t, can)
assert.Equal(t, "", reason)
})
t.Run("admin condition met grants access", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Condition{{
Expression: `req.tls.enabled`,
Message: "admin actions require TLS",
}})
can, reason := ac.can(httpReqTLS, uac, constants.ReadPermission, "any/repo", "ref")
assert.True(t, can)
assert.Equal(t, "", reason)
})
t.Run("admin condition denied surfaces operator message", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Condition{{
Expression: `req.tls.enabled`,
Message: "admin actions require TLS",
}})
can, reason := ac.can(httpReqPlain, uac, constants.ReadPermission, "any/repo", "ref")
assert.False(t, can)
assert.Equal(t, "admin actions require TLS", reason)
})
t.Run("non-admin user is unaffected by admin conditions", func(t *testing.T) {
t.Parallel()
ac := makeAC([]config.Condition{{
Expression: `req.tls.enabled`,
Message: "admin actions require TLS",
}})
bob := reqCtx.NewUserAccessControl()
bob.SetUsername("bob")
can, _ := ac.can(httpReqTLS, bob, constants.ReadPermission, "any/repo", "ref")
assert.False(t, can)
})
}