Skip to content
Open
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
94 changes: 94 additions & 0 deletions pkg/image/apis/image/validation/whitelist/whitelister_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,100 @@ func assertExpectedError(t *testing.T, a, e error) {
}
}

// TestDefaultAllowedRegistries verifies that the default allowed registries
// (as configured by the cluster-openshift-apiserver-operator defaultconfig.yaml)
// correctly allow the expected registries and deny unlisted ones.
func TestDefaultAllowedRegistries(t *testing.T) {
ctx := context.TODO()

// These are the default registries added by the operator's defaultconfig.yaml.
defaultAllowed := openshiftcontrolplanev1.AllowedRegistries{
{DomainName: "image-registry.openshift-image-registry.svc:5000"},
{DomainName: "quay.io"},
{DomainName: "registry.redhat.io"},
}

rw, err := NewRegistryWhitelister(defaultAllowed, nil)
if err != nil {
t.Fatalf("unexpected error creating whitelister: %v", err)
}

for _, tc := range []struct {
name string
hostname string
expectErr bool
}{
{
name: "internal registry is allowed",
hostname: "image-registry.openshift-image-registry.svc:5000",
expectErr: false,
},
{
name: "quay.io is allowed",
hostname: "quay.io",
expectErr: false,
},
{
name: "registry.redhat.io is allowed",
hostname: "registry.redhat.io",
expectErr: false,
},
{
name: "gcr.io is denied",
hostname: "gcr.io",
expectErr: true,
},
{
name: "docker.io is denied",
hostname: "docker.io",
expectErr: true,
},
{
name: "random registry is denied",
hostname: "my-private-registry.example.com",
expectErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
err := rw.AdmitHostname(ctx, tc.hostname, WhitelistTransportSecure)
if tc.expectErr && err == nil {
t.Errorf("expected error for hostname %q, got nil", tc.hostname)
}
if !tc.expectErr && err != nil {
t.Errorf("expected no error for hostname %q, got: %v", tc.hostname, err)
}
})
}
}

// TestEmptyAllowedRegistriesDeniesAll verifies that when AllowedRegistriesForImport
// is empty (the new default behavior after removing WhitelistAllRegistries fallback),
// all registries are denied. This is the deny-all behavior introduced by
// openshift/openshift-apiserver#607.
func TestEmptyAllowedRegistriesDeniesAll(t *testing.T) {
ctx := context.TODO()

rw, err := NewRegistryWhitelister(openshiftcontrolplanev1.AllowedRegistries{}, nil)
if err != nil {
t.Fatalf("unexpected error creating whitelister: %v", err)
}

for _, hostname := range []string{
"quay.io",
"registry.redhat.io",
"docker.io",
"gcr.io",
"image-registry.openshift-image-registry.svc:5000",
} {
t.Run(hostname, func(t *testing.T) {
err := rw.AdmitHostname(ctx, hostname, WhitelistTransportSecure)
if err == nil {
t.Errorf("expected deny-all whitelister to reject %q, but it was allowed", hostname)
}
})
}
}

func TestWhitelistRepository(t *testing.T) {
registries := mkAllowed(false, "registry.example.org:5000")
whitelister, err := NewRegistryWhitelister(registries, nil)
Expand Down
15 changes: 13 additions & 2 deletions pkg/project/apiserver/registry/project/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,26 @@ func (s *REST) Watch(ctx context.Context, options *metainternal.ListOptions) (wa
return nil, fmt.Errorf("no user")
}

includeAllExistingProjects := (options != nil) && options.ResourceVersion == "0"
// includeAllExistingProjects (RV="0") triggers sending initial state.
// sendBookmark (from SendInitialEvents) triggers sending a bookmark with
// k8s.io/initial-events-end annotation after initial events (if any).
includeAllExistingProjects, sendBookmark := false, false
if options != nil {
if options.ResourceVersion == "0" {
includeAllExistingProjects = true
}
if options.SendInitialEvents != nil && *options.SendInitialEvents {
sendBookmark = true
}
}

allowedNamespaces, err := scope.ScopesToVisibleNamespaces(userInfo.GetExtra()[authorizationapi.ScopesKey], s.authCache.GetClusterRoleLister(), true)
if err != nil {
return nil, err
}

m := projectutil.MatchProject(apihelpers.InternalListOptionsToSelectors(options))
watcher := projectauth.NewUserProjectWatcher(userInfo, allowedNamespaces, s.projectCache, s.authCache, includeAllExistingProjects, m)
watcher := projectauth.NewUserProjectWatcher(userInfo, allowedNamespaces, s.projectCache, s.authCache, includeAllExistingProjects, m, sendBookmark)
s.authCache.AddWatcher(watcher)

go watcher.Watch()
Expand Down
44 changes: 38 additions & 6 deletions pkg/project/auth/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"errors"
"strconv"
"sync"

"k8s.io/klog/v2"
Expand Down Expand Up @@ -64,6 +65,9 @@ type userProjectWatcher struct {
initialProjects []corev1.Namespace
// knownProjects maps name to resourceVersion
knownProjects map[string]string

sendBookmark bool
bookmarkResourceVersion string
}

var (
Expand All @@ -72,11 +76,15 @@ var (
watchChannelHWM kstorage.HighWaterMark
)

func NewUserProjectWatcher(user user.Info, visibleNamespaces sets.String, projectCache *projectcache.ProjectCache, authCache WatchableCache, includeAllExistingProjects bool, predicate kstorage.SelectionPredicate) *userProjectWatcher {
func NewUserProjectWatcher(user user.Info, visibleNamespaces sets.String, projectCache *projectcache.ProjectCache, authCache WatchableCache, includeAllExistingProjects bool, predicate kstorage.SelectionPredicate, sendBookmark bool) *userProjectWatcher {
namespaces, _ := authCache.List(user, labels.Everything())
knownProjects := map[string]string{}
var maxRV int
for _, namespace := range namespaces.Items {
knownProjects[namespace.Name] = namespace.ResourceVersion
if n, err := strconv.Atoi(namespace.ResourceVersion); err == nil && n > maxRV {
maxRV = n
}
}

// this is optional. If they don't request it, don't include it.
Expand All @@ -98,13 +106,18 @@ func NewUserProjectWatcher(user user.Info, visibleNamespaces sets.String, projec
authCache: authCache,
initialProjects: initialProjects,
knownProjects: knownProjects,

sendBookmark: sendBookmark,
bookmarkResourceVersion: strconv.Itoa(maxRV),
}
w.emit = func(e watch.Event) {
// if dealing with project events, ensure that we only emit events for projects
// that match the field or label selector specified by a consumer
if project, ok := e.Object.(*projectapi.Project); ok {
if matches, err := predicate.Matches(project); err != nil || !matches {
return
if e.Type != watch.Bookmark {
// if dealing with project events, ensure that we only emit events for projects
// that match the field or label selector specified by a consumer
if project, ok := e.Object.(*projectapi.Project); ok {
if matches, err := predicate.Matches(project); err != nil || !matches {
return
}
}
}

Expand Down Expand Up @@ -186,6 +199,13 @@ func (w *userProjectWatcher) GroupMembershipChanged(namespaceName string, users,

// Watch pulls stuff from etcd, converts, and pushes out the outgoing channel. Meant to be
// called as a goroutine.
//
// Design decision: This implementation balances KEP-3157 watch-list support with backward
// compatibility. Initial events are sent only when rv="0" (includeAllExistingProjects=true).
// For other rv values with SendInitialEvents=true, only the bookmark is sent. This approach
// acknowledges that project visibility depends on both namespace objects and RBAC state. Since
// RBAC changes don't update namespace ResourceVersions, permission-filtered views cannot provide
// the same consistency guarantees (resourceVersionMatch=NotOlderThan) as direct object watches.
func (w *userProjectWatcher) Watch() {
defer close(w.outgoing)
defer func() {
Expand Down Expand Up @@ -214,6 +234,18 @@ func (w *userProjectWatcher) Watch() {
})
}

if w.sendBookmark {
w.emit(watch.Event{
Type: watch.Bookmark,
Object: &projectapi.Project{
ObjectMeta: metav1.ObjectMeta{
ResourceVersion: w.bookmarkResourceVersion,
Annotations: map[string]string{metav1.InitialEventsAnnotationKey: "true"},
},
},
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

for {
select {
case err := <-w.cacheError:
Expand Down
Loading