Skip to content
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

Rewrite from header #104

Merged
merged 10 commits into from
Jan 22, 2021
Merged

Rewrite from header #104

merged 10 commits into from
Jan 22, 2021

Conversation

marpio
Copy link
Contributor

@marpio marpio commented Dec 3, 2020

Description
This PR extends the SubjectAccessReviews rewrite functionality by allowing to rewrite the attributes using a value from http header.

Motivation
I would like to put kube-rbac-proxy in front of Loki (https://grafana.com/docs/loki/latest/). Loki, when deployed in multi-tenant mode, requires X-Scope-OrgID header containing the tenantID. In my case, the tenantID is just the namespace and so I could check if a given user/SA has access to a particular resource in a given namespace provided in the X-Scope-OrgID header. This way I could separate the logs on a per namespace basis.

@brancz
Copy link
Owner

brancz commented Dec 3, 2020

cc @s-urbaniak @simonpasquier I don't have time to think through whether this is safe or not. At first glance it seems fine, but don't have the time to spend on this right now, so if you could have a look that would be amazing!

}

if len(params) == 0 {
return []authorizer.Attributes{}
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: i would continue returning nil here, there is no need to allocate an empty slice, as the zero value (nil) of a slice also returns 0 for its capacity and length.

params, ok := r.URL.Query()[n.authzConfig.Rewrites.ByQueryParameter.Name]
if !ok {
return nil
if n.authzConfig.Rewrites != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: let's untangle those nested if/else statements, it starts being complex. We can have a more linear code if we change the method signature like so:

// GetRequestAttributes populates authorizer attributes for the requests to kube-rbac-proxy.
func (n krpAuthorizerAttributesGetter) GetRequestAttributes(u user.Info, r *http.Request) (allAttrs []authorizer.Attributes) {
	apiVerb := ""
	switch r.Method {
	case "POST":
		apiVerb = "create"
	case "GET":
		apiVerb = "get"
	case "PUT":
		apiVerb = "update"
	case "PATCH":
		apiVerb = "patch"
	case "DELETE":
		apiVerb = "delete"
	}

	defer func() {
		for attrs := range allAttrs {
			klog.V(5).Infof("kube-rbac-proxy request attributes: attrs=%#v", attrs)
		}
	}()

	if n.authzConfig.ResourceAttributes == nil {
		// Default attributes mirror the API attributes that would allow this access to kube-rbac-proxy
		allAttrs = append(allAttrs, authorizer.AttributesRecord{
			User:            u,
			Verb:            apiVerb,
			Namespace:       "",
			APIGroup:        "",
			APIVersion:      "",
			Resource:        "",
			Subresource:     "",
			Name:            "",
			ResourceRequest: false,
			Path:            r.URL.Path,
		})
		return
	}

	if n.authzConfig.Rewrites == nil {
		allAttrs = append(allAttrs, authorizer.AttributesRecord{
			User:            u,
			Verb:            apiVerb,
			Namespace:       n.authzConfig.ResourceAttributes.Namespace,
			APIGroup:        n.authzConfig.ResourceAttributes.APIGroup,
			APIVersion:      n.authzConfig.ResourceAttributes.APIVersion,
			Resource:        n.authzConfig.ResourceAttributes.Resource,
			Subresource:     n.authzConfig.ResourceAttributes.Subresource,
			Name:            n.authzConfig.ResourceAttributes.Name,
			ResourceRequest: true,
		})

		return
	}

	params := []string{}

	if n.authzConfig.Rewrites.ByQueryParameter != nil && n.authzConfig.Rewrites.ByQueryParameter.Name != "" {
		if ps, ok := r.URL.Query()[n.authzConfig.Rewrites.ByQueryParameter.Name]; ok {
			params = append(params, ps...)
		}
	}

	if n.authzConfig.Rewrites.ByHTTPHeader != nil && n.authzConfig.Rewrites.ByHTTPHeader.Name != "" {
		if p := r.Header.Get(n.authzConfig.Rewrites.ByHTTPHeader.Name); p != "" {
			params = append(params, p)
		}
	}

	if len(params) == 0 {
		return
	}

	for _, param := range params {
		allAttrs = append(allAttrs, authorizer.AttributesRecord{
			User:            u,
			Verb:            apiVerb,
			Namespace:       templateWithValue(n.authzConfig.ResourceAttributes.Namespace, param),
			APIGroup:        templateWithValue(n.authzConfig.ResourceAttributes.APIGroup, param),
			APIVersion:      templateWithValue(n.authzConfig.ResourceAttributes.APIVersion, param),
			Resource:        templateWithValue(n.authzConfig.ResourceAttributes.Resource, param),
			Subresource:     templateWithValue(n.authzConfig.ResourceAttributes.Subresource, param),
			Name:            templateWithValue(n.authzConfig.ResourceAttributes.Name, param),
			ResourceRequest: true,
		})
	}

	return
}

Copy link
Collaborator

@s-urbaniak s-urbaniak left a comment

Choose a reason for hiding this comment

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

thank you very much for the contribution! Generally this lgtm, i have just one suggestion to make the code linear.

Also, do you mind to add some unit tests?

Thank you again! 🎉

@marpio
Copy link
Contributor Author

marpio commented Dec 16, 2020

@s-urbaniak thanks for the review and suggestions!
I've refactored the func and added unit tests for it.
I had to adjust your proposal a bit b/c of some strange e2e tests failures (see i.e. https://github.com/brancz/kube-rbac-proxy/runs/1558365978)
I didn't have the time to investigate but the test were passing as soon as I removed the named return param.

for attrs := range allAttrs {
klog.V(5).Infof("kube-rbac-proxy request attributes: attrs=%#+v", attrs)
if len(params) == 0 {
return allAttrs
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: we can return nil here

@s-urbaniak
Copy link
Collaborator

@marpio yeah agreed, the non-named-return version looks better to me as well. I really have just one nit, else lgtm, thank you so much! 💚

@marpio
Copy link
Contributor Author

marpio commented Dec 17, 2020

@s-urbaniak hmm.. returning nil causes the e2e test failure (see https://github.com/brancz/kube-rbac-proxy/pull/104/checks?check_run_id=1566450831)
I'm not sure what's the reason for that though...
Anyway returning allAttrs here sholdn't have any penalty b/c it's not initialized at this point.

@s-urbaniak
Copy link
Collaborator

@marpio interesting, let me inspect why that is the case 🤔

@marpio
Copy link
Contributor Author

marpio commented Jan 10, 2021

@s-urbaniak have you been able to find anything?

@dbluxo
Copy link

dbluxo commented Jan 18, 2021

Amazing @marpio this is exactly what we were looking for and already wanted to implement ourselves. Our usecase is the same as you describe to also enable the multi-tenancy capability with Grafana Loki. Would this configuration work after your PR is merged?:

"authorization":
  "resourceAttributes":
    "apiVersion": "metrics.k8s.io/v1beta1"
    "namespace": "{{ .Value }}"
    "resource": "pods"
  "rewrites":
    "byHTTPHeader":
      "name": "X-Scope-OrgID"

EDIT: I'll answer the question myself, yes it works that way. Tested it with this PR 🎉

@s-urbaniak
Copy link
Collaborator

s-urbaniak commented Jan 22, 2021

I think the failing e2e test was a flake as it passes for me locally with the return nil patch:

$ make test-e2e
go test -timeout 55m -v ./test/e2e/  --kubeconfig=/home/sur/.kube/config
=== RUN   Test
=== RUN   Test/Basics
=== RUN   Test/Basics/NoRBAC
=== RUN   Test/Basics/WithRBAC
=== RUN   Test/TokenAudience
=== RUN   Test/TokenAudience/IncorrectAudience
=== RUN   Test/TokenAudience/CorrectAudience
=== RUN   Test/AllowPath
=== RUN   Test/AllowPath/WithPathhNotAllowed
=== RUN   Test/AllowPath/WithPathAllowed
=== RUN   Test/IgnorePath
=== RUN   Test/IgnorePath/WithIgnorePathMatch
=== RUN   Test/IgnorePath/WithIgnorePathNoMatch
--- PASS: Test (186.21s)
    --- PASS: Test/Basics (50.74s)
        --- PASS: Test/Basics/NoRBAC (47.64s)
        --- PASS: Test/Basics/WithRBAC (3.01s)
    --- PASS: Test/TokenAudience (58.21s)
        --- PASS: Test/TokenAudience/IncorrectAudience (53.02s)
        --- PASS: Test/TokenAudience/CorrectAudience (5.05s)
    --- PASS: Test/AllowPath (39.05s)
        --- PASS: Test/AllowPath/WithPathhNotAllowed (3.94s)
        --- PASS: Test/AllowPath/WithPathAllowed (34.96s)
    --- PASS: Test/IgnorePath (38.21s)
        --- PASS: Test/IgnorePath/WithIgnorePathMatch (4.00s)
        --- PASS: Test/IgnorePath/WithIgnorePathNoMatch (34.12s)
PASS
ok  	github.com/brancz/kube-rbac-proxy/test/e2e	186.229s

If you want please submit a follow-up PR. However, this being a nit is not a stopper, hence merging. Sorry for the delay and thank you for the contribution!

@s-urbaniak s-urbaniak merged commit e4b3175 into brancz:master Jan 22, 2021
@marpio
Copy link
Contributor Author

marpio commented Jan 26, 2021

@s-urbaniak thanks for taking the time!

@s-urbaniak
Copy link
Collaborator

@brancz I think this deservers a new release, however I have not the powers to publish new images. Happy to help out. wdyt?

@paulfantom
Copy link
Collaborator

Seems like after merging this, our e2e tests started failing 🤔

More in https://github.com/brancz/kube-rbac-proxy/runs/1748074270

Personally, I would like to know why those are failing before creating a release.

@s-urbaniak
Copy link
Collaborator

hmm, let's open an issue then, it seems to be related with certs rotation so i can have a look.

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.

None yet

5 participants