Skip to content

Impersonate user for ns update during project update #11647

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

Closed
wants to merge 1 commit into from

Conversation

enj
Copy link
Contributor

@enj enj commented Oct 28, 2016

}

return projectutil.ConvertNamespace(namespace), false, nil
}

func (s *REST) getImpersonatingClientFor(user user.Info) kclient.NamespaceInterface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get a client config and do this:

    impersonatingConfig := a.privilegedRESTClientConfig
    oldWrapTransport := impersonatingConfig.WrapTransport
    impersonatingConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
        return authenticationclient.NewImpersonatingRoundTripper(attributes.GetUserInfo(), oldWrapTransport(rt))
    }

or make a helper down in the pkg/auth/client package.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that reconstructs from config on every request, potentially down to the point of re-reading and re-parsing CA and cert files... needs to be split up to only build the base transport once

@@ -154,19 +158,38 @@ func (s *REST) Update(ctx kapi.Context, name string, objInfo rest.UpdatedObjectI
return nil, false, fmt.Errorf("not a project: %#v", obj)
}

s.updateStrategy.PrepareForUpdate(ctx, obj, oldObj)
if errs := s.updateStrategy.ValidateUpdate(ctx, obj, oldObj); len(errs) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you get a validation error and it is in metadata, you can try the impersonating update on the namespace.

Otherwise, you can escalate using your privileged client.

@enj enj force-pushed the enj/i/ns_project_auth/1386401 branch from b7a64c3 to 81c81d0 Compare October 31, 2016 19:45
@enj
Copy link
Contributor Author

enj commented Oct 31, 2016

@deads2k @liggitt PTAL. I will add some oc tests to verify the behavior. Config copying / new client creation only happens on metadata errors.

@enj
Copy link
Contributor Author

enj commented Oct 31, 2016

[test]

@enj enj force-pushed the enj/i/ns_project_auth/1386401 branch 2 times, most recently from 97880d7 to a80f4d4 Compare November 1, 2016 13:15
@enj
Copy link
Contributor Author

enj commented Nov 1, 2016

re[test] Flake #11661

@enj enj force-pushed the enj/i/ns_project_auth/1386401 branch from a80f4d4 to c1641a0 Compare November 1, 2016 18:14
# test that standard user still cannot edit immutable fields
os::cmd::expect_failure_and_text 'oc annotate project nstestproj key1=val1 --overwrite' 'The Project "nstestproj" is invalid: metadata.annotations\[key1\]: Invalid value: "val1": field is immutable, try updating the namespace'
os::cmd::expect_failure_and_text 'oc annotate namespace nstestproj key2=val2 --overwrite' 'Error from server: User "nstest" cannot "patch" "namespaces" with name "nstestproj" in project "nstestproj"'
# this will now be successful because there is no diff between the old and new project
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be curious to know if you guys consider this behavior valid (seems a bit confusing).

@enj enj force-pushed the enj/i/ns_project_auth/1386401 branch 2 times, most recently from 5d97d57 to db3ccba Compare November 1, 2016 19:36
os::cmd::expect_success_and_text 'oc login -u nstest -p nstest' 'Login successful.'
os::cmd::expect_failure_and_text 'oc label project nstestproj keyA=valA --overwrite' 'The Project "nstestproj" is invalid: metadata.labels\[keyA\]: Invalid value: "valA": field is immutable, try updating the namespace'
os::cmd::expect_failure_and_text 'oc label namespace nstestproj keyB=valB --overwrite' 'Error from server: User "nstest" cannot "patch" "namespaces" with name "nstestproj" in project "nstestproj"'
os::cmd::expect_success_and_text 'oc label project nstestproj keyC=valC --overwrite' 'project "nstestproj" not labeled'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oc label (unlike oc annotate) checks to see if there is a diff between the two objects when printing the success message.

@openshift-bot openshift-bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Nov 26, 2016
@openshift-bot openshift-bot added needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. and removed needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Jan 20, 2017
@pweil-
Copy link

pweil- commented Feb 2, 2017

bump

@openshift-bot openshift-bot added needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. and removed needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Feb 23, 2017
@@ -48,3 +51,21 @@ func cloneRequest(r *http.Request) *http.Request {
}
return r2
}

func buildImpersonatingConfig(user user.Info, impersonatingConfig *restclient.Config) *restclient.Config {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why a pointer? It makes reasoning about whether you're wrapping the same transport deeper and deeper over time harder since I have to find the callers.


// PrivilegedLoopbackOpenShiftClientConfig is the client configuration used to call OpenShift APIs from system components
// To apply different access control to a system component, create a client config specifically for that component.
PrivilegedLoopbackOpenShiftClientConfig restclient.Config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this one actually different? We no longer allow the proxy to kube configuration.

if err != nil {
return nil, false, err
}

return projectutil.ConvertNamespace(namespace), false, nil
}

func (s *REST) tryImpersonatingNamespaceUpdateOnMetadataError(namespace *kapi.Namespace, ctx kapi.Context, errs field.ErrorList) (*kapi.Namespace, error) {
// If the error is in the metadata's annotations or labels, we try to impersonate the user and update the namespace
if containsAnnotationOrLabelMetadataError(errs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decision should be made before the method call. Maybe a switch/case after checking the strategy.

@deads2k
Copy link
Contributor

deads2k commented Mar 1, 2017

rebase, update your description, decide if its still a WIP

@openshift-bot openshift-bot added needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. and removed needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Mar 24, 2017
@mfojtik
Copy link
Contributor

mfojtik commented Mar 31, 2017

@enj ping :)

@enj enj mentioned this pull request Apr 5, 2017
@enj enj force-pushed the enj/i/ns_project_auth/1386401 branch from db3ccba to a7aae0e Compare April 6, 2017 14:58
@enj enj changed the title WIP: Impersonate user for ns update during project update Impersonate user for ns update during project update Apr 6, 2017
@enj enj removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 6, 2017
@openshift-bot
Copy link
Contributor

Evaluated for origin test up to a7aae0e

@enj
Copy link
Contributor Author

enj commented Apr 6, 2017

@deads2k Ready for another review.

I have reservations about adding something like this. Is the slight extra convenience really worth all this complexity? Very few people have access to mutate namespaces, so I feel like they should be knowledgeable enough to know the difference. My biggest concern is that any mistake on my part here could break multitenancy.

cc @liggitt @mfojtik @soltysh @smarterclayton @pweil- for thoughts.

@pweil-
Copy link

pweil- commented Apr 6, 2017

Is the fundamental question here whether or not the project API is supposed to be a proxy to the (highly privileged) namespace API? I would argue no but agree that the project vs namespace is confusing. Is there a better way to display the error message if the user has access to the namespace API?

@enj
Copy link
Contributor Author

enj commented Apr 6, 2017

Is the fundamental question here whether or not the project API is supposed to be a proxy to the (highly privileged) namespace API? I would argue no but ...

We literally call the file proxy.go so I would say yes.

... agree that the project vs namespace is confusing.

I agree that it is confusing but I am not sure how to make it less so.

Is there a better way to display the error message if the user has access to the namespace API?

We could certainly do the check, but it would be an expensive loopback REST call.

@openshift-bot
Copy link
Contributor

continuous-integration/openshift-jenkins/test FAILURE (https://ci.openshift.redhat.com/jenkins/job/test_pull_request_origin/606/) (Base Commit: 4b16943)

@enj enj mentioned this pull request Apr 6, 2017
@enj
Copy link
Contributor Author

enj commented Apr 6, 2017

Flake #13662

Copy link
Contributor

@soltysh soltysh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit test please, all the rest looks ok. Just a few questions, mostly.

@@ -16,7 +16,7 @@ import (
restful "github.com/emicklei/go-restful"
"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
"gopkg.in/natefinch/lumberjack.v2"
lumberjack "gopkg.in/natefinch/lumberjack.v2" // goimports will remove this if the alias is not set
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoa, that's interesting... I don't see that problem locally, but I might be using an older version of goimports.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried upgrading my goimports but it kept removing this...

@@ -156,6 +163,14 @@ func (s *REST) Update(ctx kapi.Context, name string, objInfo rest.UpdatedObjectI

s.updateStrategy.PrepareForUpdate(ctx, obj, oldObj)
if errs := s.updateStrategy.ValidateUpdate(ctx, obj, oldObj); len(errs) > 0 {
// If the user has the ability to update the namespace directly, they can circumvent most of the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a stupid question, but I was thought that stupid are only those questions that weren't asked ;) Is it possible to have edit rights on project but not on namespace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. As far as the authorizer is concerned, they are completely separate resources. Almost no one has rights to do anything with namespaces. Also, almost no one can create projects. This is how the projectrequest flow works (you say I want a project, the system makes a project under specific constraints, and then the underlying namespace is created).

return nil, false
}

impersonatingClient, err := authclient.NewImpersonatingKubernetesClientset(user, s.config)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an audit standpoint, this will look weird. Normally, you'd get:

  1. request to update project
  2. request to update namespace

Now you're getting:

  1. request to update project
  2. request to update project as someone else

This might look fishy in the logs and for someone doing security audit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess both you and @pweil- voiced that concern in the main thread. I'm looking from an audit point of view ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audit flow as I see it now:

Success case

  1. Request by user to update project
  2. No validation error
  3. Request by loopback client to update namespace
  4. Success

Failure case

  1. Request by user to update project
  2. Validation error
  3. Failure

The audit flow if this PR is merged:

Success case 1

  1. Request by user to update project
  2. No validation error
  3. Request by loopback client to update namespace
  4. Success

Success case 2

  1. Request by user to update project
  2. Immutable field error
  3. Request by loopback client that is impersonating the user to update namespace
  4. User can update namespaces
  5. Success

Failure case 1

  1. Request by user to update project
  2. Non immutable field error
  3. Failure

Failure case 2

  1. Request by user to update project
  2. Immutable field error
  3. Request by loopback client that is impersonating the user to update namespace
  4. User cannot update namespaces
  5. Failure

@@ -100,7 +101,7 @@ func TestCreateProjectOK(t *testing.T) {

func TestGetProjectOK(t *testing.T) {
mockClient := fake.NewSimpleClientset(&kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "foo"}})
storage := NewREST(mockClient.Core().Namespaces(), &mockLister{}, nil, nil)
storage := NewREST(mockClient.Core().Namespaces(), restclient.Config{}, &mockLister{}, nil, nil)
project, err := storage.Get(kapi.NewContext(), "foo")
if project == nil {
t.Error("Unexpected nil project")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have an explicit test for that new functionality here. The more tests, the better, especially when it comes to security.

@smarterclayton
Copy link
Contributor

Rather than spending a bunch of time on this - I'd rather let users be able to edit a set of "safe" annotations on their project. I don't really care that admins have to use namespace to edit their namespace. Project is a limited permission view, with specific constraints. Why add complexity?

@smarterclayton
Copy link
Contributor

I wish I'd seen this sooner. I would really have just preferred to add a better message.

@enj
Copy link
Contributor Author

enj commented Apr 8, 2017

Adds too much complexity for no real gains.

@enj enj closed this Apr 8, 2017
@deads2k
Copy link
Contributor

deads2k commented Apr 10, 2017

Adds too much complexity for no real gains.

Looked pretty simple to me. Oh well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants