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

[A2-835] make custom endpoints projects aware for iam v2.1 #380

Merged

Conversation

srenatus
Copy link
Contributor

@srenatus srenatus commented May 17, 2019

🔩 Description

When we've introduced projects getting passed along from authz to the domain services, we've done that for the general case (read: GRPC). This PR extends that treatment to the special case, our hand-written HTTP endpoints defined in automate-gateway.

Caveats

⚠️ This is in the code path of data ingestion, so let's keep an eye on performance after merging this.

⚠️ Tests pass in CI, but I have only done some minimal smoke tests so far: ensuring that when IAM v2.1 is selected, the projects are passed along to the domain-service that is sitting behind one of the special endpoints (exports of compliance reports). I was able to verify that the domain-service gets them alright. However, there's probably dozens of edge cases, especially with audit-cookbook and inspec involved, and I think we need superteam-help here for review.

✔️ There's some TODO in the code, and a small refactoring I'd like to wrap into this. I'll tackle that next week.

👍 Definition of Done

🚀 The custom endpoints are projects-aware (if the domain-services (downstream) care about the projects).

👟 Demo Script / Repro Steps

  • deployinate
  • rebuild components/automate-gateway
  • chef-automate iam upgrade-to-v2 --beta2.1
  • pick a custom endpoint where the domain-service method is projects aware
  • send a request with projects

For testing,

  1. I've changed this:
diff --git a/components/compliance-service/api/reporting/server/server.go b/components/compliance-service/api/reporting/server/server.go
index d2846aa1..197fedf6 100644
--- a/components/compliance-service/api/reporting/server/server.go
+++ b/components/compliance-service/api/reporting/server/server.go
@@ -5,6 +5,7 @@ import (
        "encoding/json"
        "fmt"
        "io"
+       "os"
        sorter "sort"
        "strings"

@@ -344,6 +345,7 @@ func filterByProjects(ctx context.Context, filters map[string][]string) (map[str
        if err != nil {
                return nil, err
        }
+       fmt.Fprintf(os.Stdout, "projects: %v\n\n", projectsFilter)
        if auth_context.AllProjectsRequested(projectsFilter) {
                return filters, nil
        }
  1. rebuilt components/compliance-service, and
  2. sent a request (where $TOK is an admin token)
$ curl -kH "api-token: $TOK" https://127.0.0.1/api/v0/compliance/reporting/export -d '{"type":"json","filters":[{"type":"start_time","values":["2019-05-07T00:00:00Z"]},{"type":"end_time","values":["2019-05-17T23:59:59Z"]}]}'  -H "projects: foo,bar" -XGET
  1. observed the logs:
[2019-05-20T08:10:21+00:00] authz-service.default(O): time="2019-05-20T08:10:21Z" level=warning msg="finished unary call with code FailedPrecondition" error="rpc error: code = FailedPrecondition desc = authz-service set to v2" grpc.code=FailedPrecondition grpc.method=IsAuthorized grpc.service=chef.automate.domain.authz.Authorization grpc.start_time="2019-05-20T08:10:21Z" grpc.time_ms=0.029 span.kind=server system=grpc
[2019-05-20T08:10:21+00:00] authz-service.default(O): time="2019-05-20T08:10:21Z" level=info msg="Projects Authorized Query" action="compliance:reports:export" projects="[foo bar]" resource="compliance:reporting:reports" result="[foo bar]" subject="[token:55e8020c-3100-4863-be85-65f9d6c53d7c]"
[2019-05-20T08:10:21+00:00] compliance-service.default(O): projects: [foo bar]

Note:

  1. the FailedPrecondition denotes the moment where the "authorizer" package in automate-gateway realises that sending a v1 authz query is wrong (authz-service will return FailedPreconditon), and switches itself to using v2.
  2. the v2 query, executed from the custom endpoint helper method changed in this PR, looks correct:
    Projects Authorized Query" action="compliance:reports:export" projects="[foo bar]" resource="compliance:reporting:reports" result="[foo bar]" subject="[token:55e8020c-3100-4863-be85-65f9d6c53d7c]"
    
  3. the domain-service can retrieve the projects:
    [2019-05-20T08:10:21+00:00] compliance-service.default(O): projects: [foo bar]
    

⛓️ Related Resources

✅ Checklist

  • Necessary tests added/updated?
  • Necessary docs added/updated?
  • Code actually executed?
  • Vetting performed (unit tests, lint, etc.)?

@srenatus srenatus added WIP automate-auth automate-gateway iamv2 This issue or pull request applies to iamv2 work for Automate labels May 17, 2019
@srenatus srenatus self-assigned this May 17, 2019
@srenatus srenatus force-pushed the sr/a2-835/make-custom-endpoints-projects-aware-for-iam-v2.1 branch 2 times, most recently from 2d08d13 to a2a442f Compare May 20, 2019 08:15
@srenatus srenatus removed the WIP label May 20, 2019
Copy link
Contributor

@tylercloke tylercloke left a comment

Choose a reason for hiding this comment

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

Looking like Handle and IsAuthorized are getting pretty redundant and I'm wondering if we still need both? I'd love a few function comments for both of those two explaining when they are used and how they are different 😅

Subjects: subjects,
Resource: resource,
Action: action,
ProjectsFilter: projectsToFilter,
Copy link
Contributor

Choose a reason for hiding this comment

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

This is looking similar to the Handle function. When do we hit Handle vs IsAuthorized?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it? Handle is to be used in a GRPC interceptor, and depends on
a) the grpc method invoked, and
b) the policy info being mapped in our proto files
i.e. this half of the method, neither of which exist for custom handlers.

I'll outline how the authorizer is supposed to work in another comment, give me a minute 😉

@srenatus
Copy link
Contributor Author

srenatus commented May 21, 2019

ℹ️ That's the authorizer machinery in a nutshell:

  • It wraps an authorizer for v1 and one for v2; and exposes roughly the same methods that these do:
  1. Handle is for use in the GRPC interceptor: it reads the method from the context, and retrieves the policy mappings (v1 or v2 respectively, in the separate authv{1,2} packages); and uses that to constrict an authz query to the respective service, interpreting the result accordingly. v2 will take care of injecting projects into the context
  2. IsAuthorized is a (bolted-on) method for using the authorizer construct in our custom handlers: it needs to be passed the resource/action directly.
  3. Since they also need switching, the authz introspection calls are similar wrapped and exposed.
  • The authorizer package wrapping the v1 and v2 authorizers takes care of switching between v1 and v2: it has the version in its state, and if the authorizer for v1 fails with FailedPrecondition, switches to v2, and vice-versa. This is how we've achieved that the gateway config doesn't have to be changed for IAM v2 upgrade, but authz-service is the single source of truth.
  • To expose versioned error messages, the authorizer interface needs both v1 and v2 info; whereas the versioned authv{1,2} packages only take one. This is because there's no way for the IsAuthorized methods to retrieve the info themselves.
  • The relevant types are defined here

@srenatus
Copy link
Contributor Author

Everything would be nice and tidy if there was only GRPC (and if we rearranged how introspection works, maybe?). 😉

Also: please note that I'm not particularly attached to how this currently works (I entirely blame my lack of imagination and not my tools) -- if anyone comes up with some better means to reach the same end, I'm all ears.

@@ -42,7 +41,7 @@ type SwitchingAuthorizationHandler interface {
GRPCAuthorizationHandler
SwitchingFilterHandler
IsAuthorized(ctx context.Context, subjects []string,
resourceV1, actionV1, resourceV2, actionV2 string) (AnnotatedAuthorizationResponse, error)
resourceV1, actionV1, resourceV2, actionV2 string, projects []string) (AnnotatedAuthorizationResponse, error)

Choose a reason for hiding this comment

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

I suppose this is the "last param" but oof we're passing around a lot of args at this point!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah -- the root cause is that this is used for authorizing requests in the custom endpoints' helper function -- and there's no way they can retrieve the v1 or v2 mappings (no grpc/proto files involved). So, it'll have to be presented up front.

@@ -182,7 +182,7 @@ func getProjectsFromMetadata(projectHeaderEntries []string) []string {
for _, entry := range projectHeaderEntries {
for _, project := range strings.Split(entry, ",") {
newProject := strings.TrimSpace(project)
if _, value := keys[newProject]; !value {
if !keys[newProject] {
Copy link

@blakestier blakestier May 23, 2019

Choose a reason for hiding this comment

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

I'm sort of surprised this worked-- I would expect that if the value of keys[newproject] was nil, this would fail or that we'd get "invalid operation".

Don't we need to both check the value and the success of extracting it (if val, ok := keys[newProject]; ok {)?

Maybe I'm missing something 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

keys[newProject], if not present, will return the zero-value for the type, in this case false. So this statement is still asserting "if there is no newProject entry in keys then..."

Copy link

@blakestier blakestier May 23, 2019

Choose a reason for hiding this comment

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

@msorens I know that it reads that way, but I was under the impression that golang didn't necessarily behave how we'd expect here. I've tried this in a playground and I keep getting the error I'd expect (invalid operation)-- would you mind linking me to an example playground?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

https://play.golang.org/p/y1GXa4D4vdv does that do the trick? Go really isn't dynamic at all, but non-existing values in maps will return their zero value, as @msorens suggested. Here's the spec section:

For a of map type M:

  • x's type must be assignable to the key type of M
  • if the map contains an entry with key x, a[x] is the map element with key x and the type of a[x] is the element type of M
  • if the map is nil or does not contain such an entry, a[x] is the zero value for the element type of M

Choose a reason for hiding this comment

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

Ugh thank you for the example-- I'd read the docs but was having trouble manually testing . . . I had my map constructed incorrectly (the value as a string, not a bool) 🙄

return nil, err
}
log.WithError(err).Error("error authorizing request")
return nil, status.Errorf(codes.PermissionDenied,

Choose a reason for hiding this comment

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

Should this message/error also include projects?

Copy link
Contributor

Choose a reason for hiding this comment

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

As this is the gatekeeper for both v2 and v2.1, and it does not know which for any given call, we cannot appropriately include projects.

if status.Convert(err).Code() == codes.FailedPrecondition {
return nil, err
}
log.WithError(err).Error("error authorizing request")

Choose a reason for hiding this comment

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

I'm curious about the choice to log here-- can we not guarantee that the error itself will surface in the logs?

If that's the case, I imagine it would be helpful to have the subj/resource/etc in the log message as well as the error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😄 a) I don't think I've spent much time deliberating the choice of log here.
b) ANY GRPC response that has a status !=OK will be logged by the gateway. So, we should end up having most of the stuff in the logs because of the status message below.

Good enough? 😉

@@ -662,10 +665,21 @@ func (s *Server) authRequest(r *http.Request,
return authzResp.Err()
}

func logError(err error, resource, action string) {
log.WithFields(log.Fields{

Choose a reason for hiding this comment

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

Did we move away from WithFields?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, just realised that the function hadn't been used 🤷‍♂

}).Error("error authorizing request")
// XXX needs headers, doesn't it?
func getProjectsFromMetadata(projectHeaderEntries []string) []string {
if projectHeaderEntries == nil {

Choose a reason for hiding this comment

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

👍 but also 😢

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry that one should have been deleted -- I've done the refactor here, after all. 😄

srenatus added 4 commits May 24, 2019 08:46
There's no need to translate incoming MD to outgoing MD. Outgoing
metadata is only required if another grpc CLIENT call uses that context,
and the key-val pairs are supposed to be sent along.

Also fixes the comment.

Signed-off-by: Stephan Renatus <[email protected]>
@srenatus srenatus force-pushed the sr/a2-835/make-custom-endpoints-projects-aware-for-iam-v2.1 branch from a2a442f to 44e870e Compare May 24, 2019 07:06
Copy link

@vjeffrey vjeffrey left a comment

Choose a reason for hiding this comment

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

tested with audit cookbook and inspec -> automate direct. all working as expected! 😃

@srenatus srenatus merged commit 21e2d9c into master May 28, 2019
@chef-ci chef-ci deleted the sr/a2-835/make-custom-endpoints-projects-aware-for-iam-v2.1 branch May 28, 2019 14:10
@susanev susanev added the auth-team anything that needs to be on the auth team board label Jul 20, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auth-team anything that needs to be on the auth team board automate-auth automate-gateway iamv2 This issue or pull request applies to iamv2 work for Automate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants