-
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
template broker should use SAR, not impersonation #14216
template broker should use SAR, not impersonation #14216
Conversation
[test][testextended][extended:core(templates)] |
ef8e182
to
2629ca7
Compare
2629ca7
to
c9689fc
Compare
so i think my open questions are around symmetry:
or
|
}, | ||
{ | ||
APIGroups: []string{templateapi.GroupName}, | ||
// "impersonate" is required for the API server to accept creation of |
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.
How about verb=assign, group=templateapi.GroupName resource=templateinstances . You could then bind that role locally in a namespace to allow subdivision or clusterwide if you (cluster-admin) don't care about subdivision.
if err != nil || resp == nil || !resp.Allowed { | ||
if err := s.authorize(userinfo, &authorizationapi.Action{ | ||
Namespace: templateInstance.Namespace, | ||
Verb: "impersonate", |
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 prefer "assign". It's distinct and descriptive.
pkg/client/cache/template.go
Outdated
@@ -8,7 +8,8 @@ import ( | |||
|
|||
type StoreToTemplateLister interface { | |||
List() ([]*templateapi.Template, error) | |||
GetTemplateByUID(uid string) (*templateapi.Template, error) | |||
ListByNamespace(namespace string) ([]*templateapi.Template, 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.
You should really switch to the generated lister which should already have a per-namespace option.
pkg/cmd/server/api/types.go
Outdated
// TemplateServiceBrokerConfig holds information related to the template | ||
// service broker. The broker is enabled if TemplateServiceBrokerConfig is | ||
// non-nil. | ||
TemplateServiceBrokerConfig *TemplateServiceBrokerConfig |
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.
wasn't this in another pull? I feel like I already approved 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.
yes - there are 2 commits here - the first is last my trello card from last sprint which is currently in the merge queue
@@ -129,4 +130,6 @@ const ( | |||
BuildStrategyJenkinsPipelineRoleBindingName = BuildStrategyJenkinsPipelineRoleName + "-binding" | |||
|
|||
OpenshiftSharedResourceViewRoleBindingName = OpenshiftSharedResourceViewRoleName + "s" | |||
|
|||
TemplateServiceBrokerRoleBindingName = TemplateServiceBrokerRoleName + "-binding" |
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.
let's try to start matching upstream. The names match exactly upstream.
@@ -91,6 +92,12 @@ const ( | |||
|
|||
InfraGarbageCollectorControllerServiceAccountName = "garbage-collector-controller" | |||
GarbageCollectorControllerRoleName = "system:garbage-collector-controller" | |||
|
|||
InfraTemplateInstanceControllerServiceAccountName = "template-instance-controller" |
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 #14150 merges ahead of you, I'd like to use the new pattern. It aligns us with usptream and allows us to start splitting out ownership of launch mechanism and config information.
// "impersonate" is required for the API server to accept updates to | ||
// TemplateInstance objects where the requester username is not the | ||
// API caller. | ||
Verbs: sets.NewString("get", "list", "watch", "update", "impersonate"), |
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 "impersonate" I didn't expect. There's a controller that's mutating ownership? Where it get information?
If its the updating detection server-side, that should be based on a change, not just that information being present.
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 its the updating detection server-side, that should be based on a change, not just that information being present.
Oh, you're updating spec from the controller, not just status? I could see that being required then, but why update spec?
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 controller updates status only. At the moment, things are set up such that you must be the requester or have "impersonate" to edit any part of the object, hence "impersonate" is required here. Would it be better to allow anyone to change the status? Or is it somehow possible to restrict things so that just the controller can update status, and this can be done without "impersonate"?
APIGroups: []string{"*"}, | ||
// delete is needed to avoid error: "cannot set an ownerRef on a | ||
// resource you can't delete" | ||
Verbs: sets.NewString("create", "delete"), |
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 is the controller that actually does the creation of resources from the template? He can't be allowed to have this permission. This would allow me to create oauthaccesstokens
which I could use to create a super-user token and own the cluster.
You will have to enumerate.
}, | ||
{ | ||
APIGroups: []string{kapi.GroupName}, | ||
Verbs: sets.NewString("list"), |
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 looks unusual to me. No watch paired to list?
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.
No - the template service broker only lists services in a namespace in response to an incoming bind call.
|
||
func GetBootstrapProjectRoleBindings(namespace string) []authorizationapi.RoleBinding { | ||
return append(GetBootstrapServiceAccountProjectRoleBindings(namespace), | ||
GetBootstrapTemplateServiceBrokerProjectRoleBindings(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.
Granting rights for an alpha feature? Can you separate this particular bit to a different pull? I'd expect it to be gated based on whether the feature is enabled or not, but I don't want to block progress on the rest of your pull.
c := TemplateInstanceController{restconfig: restconfig, templateclient: templateclientset.NewForConfigOrDie(&restconfig).Template()} | ||
func NewTemplateInstanceController(restconfig *rest.Config) *TemplateInstanceController { | ||
c := TemplateInstanceController{ | ||
oc: client.NewOrDie(restconfig), |
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.
It's unusual to create your own client. Please take them as arguments.
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.
To confirm: is the pattern that I should cause a templateapi client to be created somewhere higher up the call stack and pass this in with the oc & kc clients?
oc: client.NewOrDie(restconfig), | ||
kc: kclientsetinternal.NewForConfigOrDie(restconfig), | ||
templateclient: templateclientset.NewForConfigOrDie(restconfig).Template(), | ||
} | ||
_, c.controller = cache.NewInformer( |
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 a generated shared informer is more appropriate.
) | ||
|
||
type TemplateInstanceController struct { | ||
restconfig rest.Config | ||
oc *client.Client |
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.
Generated client.
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.
Or a dynamic client. Probably a dynamic client I guess.
@@ -100,25 +110,35 @@ func (c *TemplateInstanceController) handle(templateInstance *templateapi.Templa | |||
return err | |||
} | |||
|
|||
func (c *TemplateInstanceController) authorize(u user.Info, action *authorizationapi.Action) error { | |||
sar := authorizationapi.AddUserToSAR(u, &authorizationapi.SubjectAccessReview{Action: *action}) |
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.
Use the upstream subjectaccessreviews.authorization.k8s.io
We're going to (someday) remove our endpoint.
if err = c.authorize(u, &authorizationapi.Action{ | ||
Namespace: templateInstance.Namespace, | ||
Verb: "create", | ||
Group: templateapi.LegacyGroupName, |
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.
no legacy group use here. We should be switched over for a new feature.
return nil | ||
} | ||
if err == nil { | ||
err = errors.New(resp.Reason) |
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.
why not forbidden here too?
@@ -100,25 +110,35 @@ func (c *TemplateInstanceController) handle(templateInstance *templateapi.Templa | |||
return err | |||
} | |||
|
|||
func (c *TemplateInstanceController) authorize(u user.Info, action *authorizationapi.Action) 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.
right idea.
Namespace: namespace, | ||
Verb: "create", | ||
Group: info.Mapping.GroupVersionKind.Group, | ||
Resource: info.Mapping.Resource, |
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.
make sure that this is never empty.
Op: cmd.Create, | ||
Op: func(info *resource.Info, namespace string, obj runtime.Object) (runtime.Object, error) { | ||
if err = c.authorize(u, &authorizationapi.Action{ | ||
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.
make sure this is never empty.
@@ -118,23 +119,30 @@ func SelectableFields(obj *templateapi.TemplateInstance) fields.Set { | |||
return templateapi.TemplateInstanceToSelectableFields(obj) | |||
} | |||
|
|||
func (s *templateInstanceStrategy) authorize(u user.Info, action *authorizationapi.Action) 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.
copy/paste surprises me. Should be shareable, right?
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.
Yes - but I don't know where - is somewhere in pkg/authorization that this could/should go? I'm sort of surprised there isn't one already.
@liggitt can you think of a reasonable and thread-safe way to build a transport wrapper that takes a user and does the check on all requests rather than trying to catch them all? |
c9689fc
to
bf9b178
Compare
|
||
utilruntime.HandleError(fmt.Errorf("TemplateInstance status update failed (%d/%d): %v", try+1, updateRetries, err)) | ||
|
||
if apierrors.IsConflict(err) && try+1 < updateRetries { |
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 logic looks exactly like RetryOnConflict
.
Also, it's a little unusual to bother with retrying on conflict instead of just re-running the controller func. Something really expensive happen before this? Your controller needs to be level driven anyway, so you must be able to re-run your controller sync method or you're inherently unstable on failures and the like.
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.
Honestly, I think this functionality is inherently unstable on failures and the like. I think this is where an imperative world meets a level driven world. @bparees @smarterclayton how do you want to proceed?
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.
@jim-minter so the big issue here is that if we retry the event, we'll get create failures because the objects already exist, right? But we should be able to tell that we created those objects based on their ownerrefs and ignore the create failures and proceed to the status update.
So i'm inclined to say we can just retry the event as @deads2k suggests (if we add the other logic to be more intelligent about handling create failures), unless i'm forgetting something?
return false | ||
} | ||
|
||
func (statusStrategy) PrepareForCreate(ctx apirequest.Context, obj runtime.Object) { |
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.
you shouldn't have these in the interface since you only need an UpdateStrategy
return b | ||
} | ||
|
||
func (b *Broker) waitForReady() 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 expected this to be called from a filter function in your handler chain so that we could easily make sure the entire handler was covered.
} | ||
|
||
func NewBroker(restconfig restclient.Config, localSAR client.LocalSubjectAccessReviewsNamespacer, secretsGetter internalversion.SecretsGetter, informers shared.InformerFactory, namespaces []string) *Broker { | ||
func NewBroker(privrestconfig restclient.Config, privkc kclientset.Interface, infraNamespace string, informer templateinformer.TemplateInformer, namespaces []string) *Broker { | ||
informer.Informer().AddIndexers(cache.Indexers{ |
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.
Do this right where you create the informers. We really shouldn't ever create one without this and later we can update the generator to allow it.
if err == nil { | ||
err = errors.New("forbidden") | ||
// TODO: enable SAR for template - at the moment I think this doesn't work | ||
// properly because group information isn't populated in u. |
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.
does this mean that your information flow is incomplete somewhere? That is a severe problem.
You sent an email a while back describing the information flow. This makes it sound like information is being dropped somewhere and that's concerning. Group information for the same user may not be stable across multiple requests (different certificates as a for instance) and the @liggitt you familiar with the information flow here? |
@jim-minter wasn't this the header ( |
9630180
to
c4da547
Compare
096db99
to
965e3ea
Compare
965e3ea
to
1f0aba8
Compare
Evaluated for origin test up to 1f0aba8 |
Evaluated for origin testextended up to 1f0aba8 |
continuous-integration/openshift-jenkins/testextended SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin_extended/465/) (Base Commit: 48e5e40) (Extended Tests: core(templates)) |
continuous-integration/openshift-jenkins/test SUCCESS (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin/1641/) (Base Commit: 48e5e40) |
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'm going to merge this because none of my comments should block it, but i'd like to see a follow up PR that addresses them and also:
- general addition of godoc (i didn't call out all the places it's missing)
- more logging (at level 2 or 4) in the controllers+broker api so we can debug request flows/failures.
@@ -45,5 +45,16 @@ func AddObjectsToTemplate(template *Template, objects []runtime.Object, targetVe | |||
} | |||
|
|||
return nil | |||
} | |||
|
|||
func FilterTemplateInstanceCondition(conditions []TemplateInstanceCondition, condType TemplateInstanceConditionType) []TemplateInstanceCondition { |
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
provisionErr := c.provision(templateInstance) | ||
|
||
templateInstance.Status.Conditions = templateapi.FilterTemplateInstanceCondition(templateInstance.Status.Conditions, templateapi.TemplateInstanceReady) | ||
templateInstance.Status.Conditions = templateapi.FilterTemplateInstanceCondition(templateInstance.Status.Conditions, templateapi.TemplateInstanceInstantiateFailure) |
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.
can a templateinstance go from a Failed condition to Ready condition?
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.
No. Is your opinion that these lines are overkill? I was wanting to guarantee that whatever these conditions may currently contain, subsequently they only contain the result of this process.
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.
yeah i was wondering if they were, but i agree w/ the goal of certainty, so i think it's fine to leave them.
if err != nil || resp == nil || !resp.Allowed { | ||
if err := util.Authorize(s.kc.Authorization().SubjectAccessReviews(), userinfo, &authorization.ResourceAttributes{ | ||
Namespace: templateInstance.Namespace, | ||
Verb: "assign", |
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.
should be a constant?
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.
Can do, but my impression was that this seems to match wider existing code style.
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.
[shrug]. we can leave it, i just worry about someone getting the name wrong or typo'ing it somewhere.
Verb: "assign", | ||
Group: templateapi.GroupName, | ||
Resource: "templateinstances", | ||
}); err != nil { | ||
return field.ErrorList{field.Forbidden(field.NewPath("spec.impersonateUser"), "impersonation forbidden")} |
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.
s/spec.impersonateUser/spec.requester.username/ ?
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.
doh - thanks
"github.com/openshift/origin/pkg/openservicebroker/api" | ||
templateapi "github.com/openshift/origin/pkg/template/api" | ||
) | ||
|
||
func (b *Broker) ensureSecret(impersonatedKC internalversion.SecretsGetter, namespace string, instanceID string, preq *api.ProvisionRequest, didWork *bool) (*kapi.Secret, *api.Response) { | ||
func (b *Broker) ensureSecret(u user.Info, namespace string, instanceID string, preq *api.ProvisionRequest, didWork *bool) (*kapi.Secret, *api.Response) { |
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
@@ -50,26 +67,44 @@ func (b *Broker) ensureSecret(impersonatedKC internalversion.SecretsGetter, name | |||
return nil, api.InternalServerError(err) | |||
} | |||
|
|||
func (b *Broker) ensureTemplateInstance(impersonatedTemplateclient internalversiontemplate.TemplateInterface, namespace string, instanceID string, template *templateapi.Template, secret *kapi.Secret, impersonate string, didWork *bool) (*templateapi.TemplateInstance, *api.Response) { | |||
func (b *Broker) ensureTemplateInstance(u user.Info, namespace string, instanceID string, template *templateapi.Template, secret *kapi.Secret, didWork *bool) (*templateapi.TemplateInstance, *api.Response) { |
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
b.kc = kc | ||
b.templateclient = templateclientset.NewForConfigOrDie(restconfig).Template() | ||
|
||
glog.Infof("Template service broker: waiting for informer sync") |
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.
lower log level.
} | ||
|
||
func (b *Broker) WaitForReady() error { | ||
timer := time.NewTimer(10 * time.Second) |
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.
10s sounds short. how confident are we this will always be sufficient?
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.
Any incoming TSB API request would be held up for maximally that length of time before giving up if the broker hasn't initialised. Initialisation is a one-off event at startup and should be rapid. 10s is in use elsewhere in our codebase in similar scenarios. At the moment we return status code 500 if the timer expires, arguably that should be a different code, but there's nothing to cover this in the OSB spec and I'm not aware of a specific standardised "try again later" status code to use.
I think I should add some code documentation here :)
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.
ok, if this is a check that's being run on each request that's fine. I was under the (mistaken, I guess) impression that if we hit this timeout, the broker api wasn't going to start up at all.
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.
yes; that is not the case.
} | ||
|
||
func NewBroker(restconfig restclient.Config, localSAR client.LocalSubjectAccessReviewsNamespacer, secretsGetter internalversion.SecretsGetter, informers shared.InformerFactory, namespaces []string) *Broker { | ||
func NewBroker(privrestconfig restclient.Config, privkc kclientset.Interface, infraNamespace string, informer templateinformer.TemplateInformer, namespaces []string) *Broker { |
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
@@ -109,14 +109,14 @@ var _ = g.Describe("[templates] templateinstance security tests", func() { | |||
}, | |||
}, | |||
{ | |||
by: "checking adminuser can create a privileged object", | |||
by: "checking adminuser can't create a privileged object", |
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 is probably worth a comment that the reason this should fail is because the controller itself should not have these permissions, even though the adminuser does.
[merge] |
Evaluated for origin merge up to 1f0aba8 |
continuous-integration/openshift-jenkins/merge SUCCESS (https://ci.openshift.redhat.com/jenkins/job/merge_pull_request_origin/765/) (Base Commit: 50b7254) (Image: devenv-rhel7_6260) |
informerFactory := shared.NewInformerFactory(internalkubeInformerFactory, externalkubeInformerFactory, privilegedLoopbackKubeClientsetInternal, privilegedLoopbackOpenShiftClient, customListerWatchers, defaultInformerResyncPeriod) | ||
|
||
err = templateInformers.Template().InternalVersion().Templates().Informer().AddIndexers(cache.Indexers{ |
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 causes the template informer to always be started, even if the template broker is off. This needs to be guarded.
https://trello.com/c/ursxF5mB/1209-8-template-broker-should-use-sar-not-impersonation-template-broker