-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce RestrictUsersAdmission plugin for collaboration #11743
Conversation
Looks like flake #10988. Please [test] again! |
Looks like flake #11649. Please [test] again! |
This PR implements proposal #9932 to restrict role bindings based on project annotations. The implementation is based on deads's WIP PR #9885. @deads2k, @smarterclayton, do you have time to review the approach in this PR? |
const ( | ||
// CollaboratorAnnotation is the key for an annotation on Namespace that | ||
// specifies collaborators on the namespace. | ||
CollaboratorAnnotation = "authorization.openshift.io/collaborators" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 3.3, the kube infrastructure to register different groups was hopelessly broken. In 3.4, it's only mostly broken. In 3.5, it mostly works.
I think I'd prefer to see this as a separate API group and resource. That would allow "clean" management via oc
and all of our usual tooling.
|
||
// Type is the type of collaborators against which this CollaboratorMatcher | ||
// may be matched. | ||
Type CollaboratorType `json:"type"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap these and call them Spec
. I can envision a world where this controller driven and a controller drives updates to status to describe matches. That would prevent multiple lookup/matching calls on every API server and allow an admin to see who actually matches for something like content assist from a Status
field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// only one may be non-nil
type CollaboratorMatcherSpec struct{
UserMatcher *UserMatcher
GroupMatcher *GroupMatcher
ServiceAccountMatcher *ServiceAccountMatcher
}
type UserMatcher struct{
Users []string
UsersInTheseGroups []string
UsersMatchingTheseSelectors []*unversioned.LabelSelector
}
type GroupMatcher struct{
Groups []string
GroupsMatchingTheseSelectors []*unversioned.LabelSelector
}
type ServiceAccountMatcher struct{
ServiceAccounts []string
ServiceAccountsInTheseNamespaces []string
}
The previous proposal never addressed the question of how an end user
would get access to add other people to collaborate in a system where
this was primarily to prevent abuse. How would this be used to allow
for an online-like cluster (where collaboration was reasonable for
arbitrary user emails)?
Does this presuppose a higher level system for that?
I'd like to be able to answer that question before we add that code.
|
Yes. This assumes an external workflow could be built to handle workflow problems like "apply, approve" and "invite, accept" flows. |
90211eb
to
9f692fb
Compare
@@ -5,7 +5,7 @@ syntax = 'proto2'; | |||
|
|||
package ytb.ewangdian.workers.dev.openshift.origin.pkg.security.api.v1; | |||
|
|||
import "github.com/openshift/origin/pkg/quota/api/v1/generated.proto"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know why this is getting deleted.
@deads2k, could you give this PR another look? |
// a string name on the name of a group to which the user belongs, or | ||
// a label selector. | ||
type UserMatcher struct { | ||
Users []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
godoc all API fields.
I'm assuming these are literal string matches.
type UserMatcher struct { | ||
Users []string | ||
UsersInTheseGroups []string | ||
UsersMatchingTheseSelectors []*unversioned.LabelSelector |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Selector
will do.
// a label selector. | ||
type UserMatcher struct { | ||
Users []string | ||
UsersInTheseGroups []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Groups
with good docs.
// the service-account name or the name of the service account's namespace. | ||
type ServiceAccountMatcher struct { | ||
ServiceAccounts []string | ||
ServiceAccountsInTheseNamespaces []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Namespaces
?
// ServiceAccountMatcher matches a service account by a string match on either | ||
// the service-account name or the name of the service account's namespace. | ||
type ServiceAccountMatcher struct { | ||
ServiceAccounts []string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to a slice of structs which have namespace
and name
.
@@ -0,0 +1,59 @@ | |||
package api |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sub-package here is in admission
. I was actually expecting you to add these directly to authorization/api/types.go
and then read them inside of admission.
// Only bother checking deltas. | ||
newSubjects := []kapi.ObjectReference{} | ||
policy := authorizationapi.GetPolicyBindingName(binding.RoleRef.Namespace) | ||
policybinding, _ := q.policyBindingLister.PolicyBindings(ns).Get(policy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old object is provided via a.GetOldObject()
. Use that instead of a cached. copy.
for _, matcher := range matchersList.Items { | ||
// TODO: Use a cache for users, groups, and serviceaccounts. | ||
switch { | ||
case matcher.Spec.UserMatcher != nil: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see an interface defined that checks a subject. Something like Allowed(subject) error
. I would be able to take a collaboratormatcherspec
and get an allowed back out and then something that provides a union of subjectcheckers
. It'll make it easier to read.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you suggesting something like the following?
func (q *restrictUsersAdmission) Admit(a admission.Attributes) (err error) {
// ...
allowed := NewEmptyAllowed()
allowed.SetNamespace(ns)
allowed.SetClients(q.kclient, q.oclient)
for _, matcher := range matchersList.Items {
// NewAllowed(nil) is the same as NewEmptyAllowed().
// allowed.Merge(NewEmptyAllowed()) is a no-op.
allowed = allowed.Merge(NewAllowed(matcher.UserMatcher))
allowed = allowed.Merge(NewAllowed(matcher.GroupMatcher))
allowed = allowed.Merge(NewAllowed(matcher.ServiceAccountMatcher))
}
// ...
}
For comparison, here is what I have now:
func (q *restrictUsersAdmission) Admit(a admission.Attributes) (err error) {
// ...
checker, err := NewRoleBindingRestrictions(ns, q.kclient, q.oclient)
if err != nil {
return admission.NewForbidden(a, err)
}
for _, matcher := range matchersList.Items {
checker.AddMatcher(&matcher)
}
// ...
}
The latter seems simpler to me, but maybe I misunderstand the suggestion.
} | ||
|
||
for _, literal := range userInGroupLiterals { | ||
for _, group := range user.Groups { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a groupcache which the API server already maintains to get group information for a user. Use that instead of hitting the API for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. You need labels too. nevermind.
Let's the types well documented and then make the Also, |
return nil | ||
} | ||
|
||
matchersList, err := q.oclient.CollaboratorMatchers(ns).List(kapi.ListOptions{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I have no matchers, seems like I should let anyone bind in. That would be backwards compatible.
9f692fb
to
21038d5
Compare
I have updated the PR: • Renamed fields in @deads2k, what do you think now? |
// SubjectChecker before use. | ||
type SubjectChecker interface { | ||
Allowed(kapi.ObjectReference) error | ||
AddMatcher(authorizationapi.CollaboratorMatcher) error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want this on the interface
// RoleBindingRestrictions aggregates rules specified by CollaboratorMatcher | ||
// objects in a namespace and uses these rules to determine whether subjects | ||
// (users, groups, or service accounts) are collaborators on the namespace. | ||
type RoleBindingRestrictions struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like one struct for each kind of Matcher
.
// the namespace. | ||
func (r *RoleBindingRestrictions) Allowed(subject *kapi.ObjectReference) error { | ||
switch subject.Kind { | ||
case authorizationapi.SystemUserKind: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to start by managing these inside their respect user and group restrictions, but match them based only on literals.
var ( | ||
userBobRef = kapi.ObjectReference{ | ||
Kind: authorizationapi.UserKind, | ||
Namespace: "namespace", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
users don't have namespaces.
) | ||
return nil | ||
} | ||
|
||
func (obj *RoleBindingRestrictionList) GetObjectKind() unversioned.ObjectKind { return &obj.TypeMeta } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expected you to get these automatically. Can you delete them and have it still compile/work?
@@ -137,7 +137,7 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole { | |||
authorizationapi.NewRule(read...).Groups(certificatesGroup).Resources("certificatesigningrequests", "certificatesigningrequests/approval", "certificatesigningrequests/status").RuleOrDie(), | |||
|
|||
authorizationapi.NewRule(read...).Groups(authzGroup).Resources("clusterpolicies", "clusterpolicybindings", "clusterroles", "clusterrolebindings", | |||
"policies", "policybindings", "roles", "rolebindings").RuleOrDie(), | |||
"policies", "policybindings", "roles", "rolebindings", "rolebindingrestrictions").RuleOrDie(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a project admin should be allowed to see the rolebindingrestrictions
@@ -330,6 +331,7 @@ var ( | |||
// openshiftAdmissionControlPlugins gives the in-order default admission chain for openshift resources. | |||
openshiftAdmissionControlPlugins = []string{ | |||
"ProjectRequestLimit", | |||
"openshift.io/RestrictSubjectBindings", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run after originnamespacelifecycle
Comments in a separate commit if you would. Almost done. |
4889e10
to
c8b0909
Compare
I've updated generated files and pushed a new commit addressing the latest review comments. |
c8b0909
to
79f03c9
Compare
Saw what could be flake #9681 again:
https://ci.openshift.redhat.com/jenkins/job/test_pull_requests_origin_conformance/9550/consoleFull |
Flake #12235 (https://ci.openshift.redhat.com/jenkins/job/test_pull_requests_origin_conformance/9552/consoleFull). New flake #12251 (https://ci.openshift.redhat.com/jenkins/job/test_pull_requests_origin_networking/2380/consoleFull). Also, fixed the bootstrap policy again. We want to allow
|
79f03c9
to
6266fda
Compare
return true, nil | ||
errs = append(errs, err) | ||
} else if allowed { | ||
return true, kerrors.NewAggregate(errs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return true, nil
. We don't want the errors bubbling back since it will make the impl harder in the admission plugin and ultimately they don't matter if we're allowed.
@@ -157,6 +153,9 @@ func (q *restrictUsersAdmission) Admit(a admission.Attributes) (err error) { | |||
|
|||
glog.V(4).Infof("Handling policybinding %s/%s", | |||
policybinding.Namespace, policybinding.Name) | |||
|
|||
default: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
duplicate these switch checks at the top of the method, no impl inside of it. That way we get a 4 line check everyone can see:
// we only care about rolebindings and policybindings
switch resource{
case rolebindings:
case policybindings:
default:
return nil
}
Two minor comments I'd like to see fixed before merge. Go ahead and squash them in and this will be ready. |
Add the RestrictUsersAdmission admission plugin for collaboration.
6266fda
to
471d717
Compare
Evaluated for origin test up to 471d717 |
continuous-integration/openshift-jenkins/test Running (https://ci.openshift.redhat.com/jenkins/job/test_pr_origin/12390/) (Base Commit: e9e7618) |
continuous-integration/openshift-jenkins/test SUCCESS |
lgtm merge |
Disabled the merge tag. @ncdc Are we good to merge this or are you close enough we should hold it? |
I am probably still a few days away. |
[merge] |
Evaluated for origin merge up to 471d717 |
continuous-integration/openshift-jenkins/merge SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pr_origin/12405/) (Base Commit: 46585f4) (Image: devenv-rhel7_5549) |
Flake #11016.
|
Add the
RestrictUsersAdmission
admission plugin for collaboration.openshift-bot, please [test]!