Skip to content

Commit 87ae6be

Browse files
committed
address fixmes and todos; add godocs
1 parent 63d7b41 commit 87ae6be

File tree

3 files changed

+114
-60
lines changed

3 files changed

+114
-60
lines changed

pkg/psalabelsyncer/podsecurity_label_sync_controller.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const (
3333
labelSyncControlLabel = "security.openshift.io/scc.podSecurityLabelSync"
3434
)
3535

36+
// PodSecurityAdmissionLabelSynchronizationController watches over namespaces labelled with
37+
// "security.openshift.io/scc.podSecurityLabelSync: true" and configures the PodSecurity
38+
// admission namespace label to match the user account privileges in terms of being able
39+
// to use SCCs
3640
type PodSecurityAdmissionLabelSynchronizationController struct {
3741
namespaceClient corev1client.NamespaceInterface
3842

@@ -109,7 +113,7 @@ func NewPodSecurityAdmissionLabelSynchronizationController(
109113
rbacInformers.ClusterRoles().Informer(),
110114
).
111115
WithFilteredEventsInformersQueueKeysFunc(
112-
nsToQueueKey,
116+
nameToKey,
113117
func(obj interface{}) bool {
114118
ns, ok := obj.(*corev1.Namespace)
115119
if !ok {
@@ -118,7 +122,6 @@ func NewPodSecurityAdmissionLabelSynchronizationController(
118122
return false
119123
}
120124
// the SCC mapping requires the annotation
121-
// FIXME: make the mapping not panic but error out instead
122125
if ns.Annotations == nil || len(ns.Annotations[securityv1.UIDRangeAnnotation]) == 0 {
123126
return false
124127
}
@@ -158,14 +161,14 @@ func (c *PodSecurityAdmissionLabelSynchronizationController) sync(ctx context.Co
158161
return fmt.Errorf(errFmt, qKey, err)
159162
}
160163

161-
if err := c.syncNamespace(ctx, ns); err != nil {
164+
if err := c.syncNamespace(ctx, controllerContext, ns); err != nil {
162165
return fmt.Errorf(errFmt, qKey, err)
163166
}
164167

165168
return nil
166169
}
167170

168-
func (c *PodSecurityAdmissionLabelSynchronizationController) syncNamespace(ctx context.Context, ns *corev1.Namespace) error {
171+
func (c *PodSecurityAdmissionLabelSynchronizationController) syncNamespace(ctx context.Context, controllerContext factory.SyncContext, ns *corev1.Namespace) error {
169172
// We cannot safely determine the SCC level for an NS until it gets the UID annotation.
170173
// No need to care about re-queueing the key, we should get the NS once it is updated
171174
// with the annotation.
@@ -194,7 +197,10 @@ func (c *PodSecurityAdmissionLabelSynchronizationController) syncNamespace(ctx c
194197
// TODO: the SCC was removed in the meantime and synced in the cache?
195198
return err
196199
}
197-
sccPSaLevel := convertSCCToPSALevel(ns, scc)
200+
sccPSaLevel, err := convertSCCToPSALevel(ns, scc)
201+
if err != nil {
202+
return err
203+
}
198204

199205
if sccPSaLevel > currentNSLevel {
200206
currentNSLevel = sccPSaLevel
@@ -226,14 +232,18 @@ func (c *PodSecurityAdmissionLabelSynchronizationController) syncNamespace(ctx c
226232
return nil
227233
}
228234

229-
func nsToQueueKey(obj runtime.Object) []string {
235+
// nameToKey turns a meta object into a key by using the object's name.
236+
func nameToKey(obj runtime.Object) []string {
230237
metaObj, ok := obj.(metav1.ObjectMetaAccessor)
231238
if !ok {
232239
return factory.DefaultQueueKeysFunc(obj)
233240
}
234241
return []string{metaObj.GetObjectMeta().GetName()}
235242
}
236243

244+
// queueKeysForObj returns slice with:
245+
// - a namespace name for a namespaced object
246+
// - all cluster namespaces names for cluster-wide objects
237247
func (c *PodSecurityAdmissionLabelSynchronizationController) queueKeysForObj(obj runtime.Object) []string {
238248
metaObj, ok := obj.(metav1.ObjectMetaAccessor)
239249
if !ok {
@@ -248,6 +258,7 @@ func (c *PodSecurityAdmissionLabelSynchronizationController) queueKeysForObj(obj
248258
return c.allWatchedNamespacesAsQueueKeys(obj)
249259
}
250260

261+
// allWatchedNamespacesAsQueueKeys returns all namespace names slice irregardles of the retrieved object.
251262
func (c *PodSecurityAdmissionLabelSynchronizationController) allWatchedNamespacesAsQueueKeys(_ runtime.Object) []string {
252263
namespaces, err := c.namespaceLister.List(c.nsLabelSelector)
253264
if err != nil && !apierrors.IsNotFound(err) {
@@ -262,6 +273,8 @@ func (c *PodSecurityAdmissionLabelSynchronizationController) allWatchedNamespace
262273
return qKeys
263274
}
264275

276+
// controlledNamespacesLabelSelector returns label selector to be used with the
277+
// PodSecurityAdmissionLabelSynchronizationController.
265278
func controlledNamespacesLabelSelector() (labels.Selector, error) {
266279
labelRequirement, err := labels.NewRequirement(labelSyncControlLabel, selection.NotEquals, []string{"false"})
267280
if err != nil {

pkg/psalabelsyncer/sccrolecache.go

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
corev1 "k8s.io/api/core/v1"
99
rbacv1 "k8s.io/api/rbac/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1011
"k8s.io/apimachinery/pkg/labels"
1112
"k8s.io/apimachinery/pkg/util/sets"
1213
"k8s.io/apiserver/pkg/authentication/serviceaccount"
@@ -15,6 +16,7 @@ import (
1516
rbacv1informers "k8s.io/client-go/informers/rbac/v1"
1617
rbacv1listers "k8s.io/client-go/listers/rbac/v1"
1718
"k8s.io/client-go/tools/cache"
19+
"k8s.io/client-go/util/retry"
1820
"k8s.io/klog/v2"
1921
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
2022

@@ -23,8 +25,12 @@ import (
2325
securityv1listers "github.com/openshift/client-go/security/listers/security/v1"
2426
)
2527

28+
// The index name to be used along with the BySAIndexKeys indexing function
2629
const BySAIndexName = "ByServiceAccount"
2730

31+
// SAToSCCCache is a construct that caches roles and rolebindings
32+
// (and their cluster variants) and based on that and on SCCs present in the cluster
33+
// it allows retrieving a set of SCCs for a given ServiceAccount
2834
type SAToSCCCache struct {
2935
roleLister rbacv1listers.RoleLister
3036
clusterRoleLister rbacv1listers.ClusterRoleLister
@@ -75,18 +81,21 @@ func (r *roleBindingObj) Subjects() []rbacv1.Subject {
7581
return r.roleBinding.Subjects
7682
}
7783

78-
func (r *roleBindingObj) AppliesToNS(ns string) bool {
84+
func (r *roleBindingObj) Namespace() string {
7985
if r.clusterRoleBinding != nil {
80-
return true
86+
return ""
8187
}
82-
return ns == r.roleBinding.Namespace
88+
return r.roleBinding.Namespace
8389
}
8490

85-
func (r *roleBindingObj) Namespace() string {
91+
// AppliesToNS returns true if:
92+
// - the wrapped object is a cluster role binding
93+
// - the namespace of a wrapped rolebinding matches the namespace supplied in `ns`
94+
func (r *roleBindingObj) AppliesToNS(ns string) bool {
8695
if r.clusterRoleBinding != nil {
87-
return ""
96+
return true
8897
}
89-
return r.roleBinding.Namespace
98+
return ns == r.roleBinding.Namespace
9099
}
91100

92101
// roleObj helps to handle roles and clusterroles in a generic manner
@@ -133,6 +142,11 @@ func (r *roleObj) Namespace() string {
133142
return r.role.Namespace
134143
}
135144

145+
// BySAIndexKeys is a cache.IndexFunc indexing function that shall be used on
146+
// rolebinding and clusterrolebinding informer caches.
147+
// It retrieves the subjects of the incoming object and if there are SA, SA groups
148+
// or the system:authenticated group subjects, these will all be returned as a slice
149+
// of strings to create an index for the SA or SA group.
136150
func BySAIndexKeys(obj interface{}) ([]string, error) {
137151
roleBinding, err := newRoleBindingObj(obj)
138152
if err != nil {
@@ -163,17 +177,13 @@ func NewSAToSCCCache(rbacInformers rbacv1informers.Interface, sccInfomer securit
163177

164178
sccLister: sccInfomer.Lister(),
165179

166-
// TODO: do I need these?
167-
rolesSynced: rbacInformers.Roles().Informer().HasSynced,
168-
roleBindingsSynced: rbacInformers.RoleBindings().Informer().HasSynced,
169-
170180
usefulRoles: make(map[string][]string),
171181
}
172182
}
173183

174184
// SCCsFor returns a slice of all the SCCs that a given service account can use
175-
// to run pods in its namespace
176-
// It expects the serviceAccount name in the system:serviceaccount:<ns>:<name> form
185+
// to run pods in its namespace.
186+
// It expects the serviceAccount name in the system:serviceaccount:<ns>:<name> form.
177187
func (c *SAToSCCCache) SCCsFor(serviceAccount *corev1.ServiceAccount) (sets.String, error) {
178188
saUserInfo := serviceaccount.UserInfo(
179189
serviceAccount.Namespace,
@@ -212,7 +222,6 @@ func (c *SAToSCCCache) SCCsFor(serviceAccount *corev1.ServiceAccount) (sets.Stri
212222
}
213223
}
214224

215-
// TODO: (idea): determine, ahead of time, which SCCs are allowed for all authenticated or for all SAs?
216225
for _, o := range objs {
217226
rb, err := newRoleBindingObj(o)
218227
if err != nil {
@@ -242,44 +251,63 @@ func (c *SAToSCCCache) SCCsFor(serviceAccount *corev1.ServiceAccount) (sets.Stri
242251
return allowedSCCs, nil
243252
}
244253

245-
func (c *SAToSCCCache) GetRoleFromRoleRef(ns string, roleRef rbacv1.RoleRef) (*roleObj, error) {
254+
// getRoleFromRoleRef tries to retrieve the role or clusterrole from roleRef.
255+
func (c *SAToSCCCache) getRoleFromRoleRef(ns string, roleRef rbacv1.RoleRef) (*roleObj, error) {
246256
var role interface{}
247-
var err error
257+
var retryErr error
248258
switch kind := roleRef.Kind; kind {
249-
case "Role": // FIXME: add retries (retry.WithBackoff) on NotFound
250-
role, err = c.roleLister.Roles(ns).Get(roleRef.Name)
259+
case "Role":
260+
retryErr = retry.OnError(retry.DefaultBackoff, apierrors.IsNotFound, func() error {
261+
var err error
262+
role, err = c.roleLister.Roles(ns).Get(roleRef.Name)
263+
return err
264+
})
265+
251266
case "ClusterRole":
252-
role, err = c.clusterRoleLister.Get(roleRef.Name)
267+
retryErr = retry.OnError(retry.DefaultBackoff, apierrors.IsNotFound, func() error {
268+
var err error
269+
role, err = c.clusterRoleLister.Get(roleRef.Name)
270+
return err
271+
})
272+
253273
default:
254274
return nil, fmt.Errorf("unknown kind in roleRef: %s", kind)
255275
}
256-
if err != nil {
257-
return nil, err
276+
277+
if retryErr != nil {
278+
return nil, retryErr
258279
}
259280

260281
return newRoleObj(role)
261282
}
262283

284+
// IsRoleBindingRelevant returns true if the cluster/rolebinding supplied binds
285+
// to a Role that provides access to at least one of the SCCs available in the
286+
// cluster.
263287
func (c *SAToSCCCache) IsRoleBindingRelevant(obj interface{}) bool {
264288
rb, err := newRoleBindingObj(obj)
265289
if err != nil {
266290
klog.Warningf("unexpected error, this may be a bug: %v", err)
267291
return false
268292
}
269293

270-
role, err := c.GetRoleFromRoleRef(rb.Namespace(), rb.RoleRef())
294+
role, err := c.getRoleFromRoleRef(rb.Namespace(), rb.RoleRef())
271295
if err != nil {
272296
klog.Infof("failed to retrieve a role for a rolebinding ref: %v", err)
273297
return false
274298
}
275299

276-
// TODO: actually cache the relevant rolebindings and relevant roles
277-
// or maybe only the roles and update cached roles on a role update?
278300
return c.IsRoleInvolvesSCCs(role, false)
279301
}
280302

303+
// IsRoleInvolvesSCCs returns true if the role supplied in obj provides access
304+
// to at least one of the SCCs present in the cluster.
305+
// Set isRoleUpdate to true if instead of using the cached role the supplied object
306+
// should be used to update the role in the cache as well.
281307
func (c *SAToSCCCache) IsRoleInvolvesSCCs(obj interface{}, isRoleUpdate bool) bool {
282-
c.usefulRolesLock.Lock() // TODO: comment this stuff
308+
// lock the roles cache to make sure the state of the role that's later retrieved here
309+
// is written properly
310+
c.usefulRolesLock.Lock()
283311
defer c.usefulRolesLock.Unlock()
284312

285313
role, err := newRoleObj(obj)
@@ -288,23 +316,31 @@ func (c *SAToSCCCache) IsRoleInvolvesSCCs(obj interface{}, isRoleUpdate bool) bo
288316
return false
289317
}
290318

291-
sccs, err := c.sccLister.List(labels.Everything()) // TODO: this should probably requeue, right?
292-
if err != nil {
293-
klog.Warning("failed to list SCCs: %v", err)
294-
return false
295-
}
296-
297319
if isRoleUpdate {
320+
sccs, err := c.sccLister.List(labels.Everything())
321+
if err != nil {
322+
klog.Warning("failed to list SCCs: %v", err)
323+
return false
324+
}
325+
298326
c.syncRoleCache(role.Namespace(), role.Name(), role.Rules(), sccs)
299327
}
300328

301329
return len(c.usefulRoles[fmt.Sprintf("%s/%s", role.Namespace(), role.Name())]) != 0
302330
}
303331

332+
// ReinitializeRoleCache clears the current cache of roles that provide access
333+
// to SCCs and reinitializes it by pulling all cluster-/roles and SCCs and
334+
// reevaluating them.
335+
// This should be used rather rarely as there are many computations involved
336+
// when assessing all the present cluster-/role rules.
304337
func (c *SAToSCCCache) ReinitializeRoleCache() error {
305-
c.usefulRolesLock.Lock() // TODO: comment this stuff
338+
// rewriting the whole cache, lock is needed
339+
c.usefulRolesLock.Lock()
306340
defer c.usefulRolesLock.Unlock()
307341

342+
c.usefulRoles = map[string][]string{}
343+
308344
roles, err := c.roleLister.List(labels.Everything())
309345
if err != nil {
310346
return fmt.Errorf("failed to initialize role cache: %w", err)
@@ -331,6 +367,9 @@ func (c *SAToSCCCache) ReinitializeRoleCache() error {
331367
return nil
332368
}
333369

370+
// syncRoleCache will write the current mapping of "role->SCCs it allows" to the cache.
371+
// It expects the c.usefulRolesLock to be already locked as even the wrapping context
372+
// handling roles and SCCs likely requires synchronization.
334373
func (c *SAToSCCCache) syncRoleCache(roleNS, roleName string, rules []rbacv1.PolicyRule, sccs []*securityv1.SecurityContextConstraints) {
335374
if c.usefulRolesLock.TryLock() {
336375
c.usefulRolesLock.Unlock()
@@ -340,12 +379,12 @@ func (c *SAToSCCCache) syncRoleCache(roleNS, roleName string, rules []rbacv1.Pol
340379
dummyUserInfo := &user.DefaultInfo{
341380
Name: "dummyUser",
342381
}
343-
if allowedSCCs := SCCsAllowedByPolicyRules("", dummyUserInfo, sccs, rules); len(allowedSCCs) > 0 {
382+
if allowedSCCs := sccsAllowedByPolicyRules("", dummyUserInfo, sccs, rules); len(allowedSCCs) > 0 {
344383
c.usefulRoles[fmt.Sprintf("%s/%s", roleNS, roleName)] = allowedSCCs
345384
}
346385
}
347386

348-
func SCCsAllowedByPolicyRules(nsName string, saUserInfo user.Info, sccs []*securityv1.SecurityContextConstraints, rules []rbacv1.PolicyRule) []string {
387+
func sccsAllowedByPolicyRules(nsName string, saUserInfo user.Info, sccs []*securityv1.SecurityContextConstraints, rules []rbacv1.PolicyRule) []string {
349388
ar := authorizer.AttributesRecord{
350389
User: saUserInfo,
351390
APIGroup: securityv1.GroupName,

0 commit comments

Comments
 (0)