-
Notifications
You must be signed in to change notification settings - Fork 22
Add authorization to the compliance event API endpoints #164
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
Add authorization to the compliance event API endpoints #164
Conversation
315fb32
to
968ee63
Compare
if !isAuth { | ||
log.Error(err, "Do not have the Read authorization for "+ce.Cluster.Name) | ||
|
||
continue |
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.
Continue or Break?
go.mod
Outdated
@@ -15,6 +15,7 @@ require ( | |||
github.com/stolostron/go-log-utils v0.1.2 | |||
github.com/stolostron/go-template-utils/v4 v4.0.1-0.20231212190701-4dc096ec1b40 | |||
github.com/stolostron/kubernetes-dependency-watches v0.5.2-0.20231212185913-628ab39622b8 | |||
gopkg.in/square/go-jose.v2 v2.6.0 |
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.
jwt
3e70f0f
to
03309b1
Compare
} | ||
|
||
ssar := &authorizationv1.SubjectAccessReview{ | ||
Spec: authorizationv1.SubjectAccessReviewSpec{ |
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.
Is this correct? do we want to check replicated policies in managedcluster namespace?
ef6b25c
to
510f84d
Compare
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.
Some early comments I had last week - I will plan to re-review this soon anyway.
controllers/complianceeventsapi/complianceeventsapi_controller.go
Outdated
Show resolved
Hide resolved
} | ||
|
||
if !isAuth { | ||
log.Error(err, "Do not have the Read authorization for "+complianceEvent.Cluster.Name) |
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 if we want to log every time an unauthorized request comes in - and if we do, I think it would not be at the error
level since this is a user problem, not an app problem.
Maybe Matt or Patrick knows if there's a guideline we need to follow about logging unauthorized access attempts?
return true, nil | ||
} | ||
|
||
log.Info("The service account does not have GET authorization for " + managedClusterName) |
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 think this log message is necessary here - if there were a lot of unauthorized requests, this could pollute the app logs.
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 we need to figure out the service account stuff for the tests.
SubjectAccessReview needs clusterrole or role so if the user doesn't have SubjectAccessReview GET role. That user will fail. I am not sure which one is better. Also, I should use |
Hi all, since I opened #167 after 167 merged then I will work on this 164 PR Thanks |
b55fa91
to
9e442f6
Compare
Limiting the number of API calls is a good reason to favor |
return nil, fmt.Errorf("failed to parse token") | ||
} | ||
// Token include Bearer prefix | ||
config.Impersonate = rest.ImpersonationConfig{ |
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.
We don't want to use impersonation because then group membership does not get taken into account.
We want to instead use the user token to make an SelfSubjectAccessReview.
For example, see how the Kubernetes client is generated using the input token:
https://github.com/stolostron/rbac-api-utils/blob/159deac7d398c3470ae730c3fbb8ffbbcca1ac5a/pkg/rbac/rbac.go#L91-L104
Then you can call GetResourceAccess
with the Kubernetes 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.
Thanks!
|
||
Describe("Test authorization", func() { | ||
Describe("Test method Get", func() { | ||
It("Should return StatusForbidden when it is empty Authorization", func(ctx context.Context) { |
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.
Shouldn't it be unauthorized when no authentication info is provided?
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 is empty token.. maybe I should change the title
|
||
Describe("Test authorization", func() { | ||
Describe("Test method Get", func() { | ||
It("Should return StatusForbidden when it is empty Authorization", func(ctx context.Context) { |
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.
Could you also test the CSV endpoint?
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 did line 1802
450f9e6
to
2ee8a7e
Compare
return parsed, nil | ||
} | ||
|
||
// getAuthorizedClusterNames verifies that if a cluster filter is provided, |
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.
// getAuthorizedClusterNames verifies that if a cluster filter is provided, | |
// setAuthorizedClusters verifies that if a cluster filter is provided, |
clusterName, err := getClusterNameFromID(ctx, db, id) | ||
if err != nil { | ||
if errors.Is(err, sql.ErrNoRows) { | ||
// This leads to send forbidden error to the user |
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 leads to send forbidden error to the user | |
// Filter out invalid cluster IDs from the query |
if err != nil { | ||
if errors.Is(err, sql.ErrNoRows) { | ||
// This leads to send forbidden error to the user | ||
parsed.Filters["clusters.cluster_id"] = append(parsed.Filters["clusters.cluster_id"][:index], |
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 wouldn't work if there was more than one invalid cluster ID since index no longer matches. My suggestion is traverse clusterIDs
in reverse so that removing any cluster ID does not impact the current index.
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.
isn't better we reset parsed.Filters["clusters.cluster_id"] then append? more readable
// Deep copy parsed.Filters["clusters.cluster_id"] | ||
copy(clusterIDs, tempClusterIDs) | ||
|
||
if okClusterIds { |
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 if statement is not needed since ranging over an empty/uninitialized slice will just be skipped.
} | ||
|
||
parsedClusterNames, okClusterNames := parsed.Filters["clusters.name"] | ||
if okClusterNames { |
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 if statement is not needed since ranging over an empty/uninitialized slice will just be skipped.
} | ||
|
||
// There is no cluster.id or clusterName arg from the url query | ||
// In other words, the user requests 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.
// In other words, the user requests all | |
// In other words, the user requests all they have access to. |
Add authorization to the compliance event "GET" API endpoints Add authorization to the compliance event API endpoints Add authorization to the compliance event "GET" API endpoints after "POST" merged Ref: https://issues.redhat.com/browse/ACM-6866 Signed-off-by: Yi Rae Kim <[email protected]>
2ee8a7e
to
02a0023
Compare
userConfig, err := getUserKubeConfig(s.cfg, r) | ||
if err != nil { | ||
if errors.Is(err, ErrNotAuthorized) { | ||
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) |
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.
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) | |
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) |
userConfig, err := getUserKubeConfig(s.cfg, r) | ||
if err != nil { | ||
if errors.Is(err, ErrNotAuthorized) { | ||
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) |
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.
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) | |
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) |
userConfig, err := getUserKubeConfig(s.cfg, r) | ||
if err != nil { | ||
if errors.Is(err, ErrNotAuthorized) { | ||
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) |
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.
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden) | |
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusUnauthorized) |
} | ||
} | ||
|
||
// There is no cluster.id or clusterName arg from the url query |
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 is no cluster.id or clusterName arg from the url query | |
// There is no cluster.cluster_id or cluster.name arg from the url query |
tempClusterIDs := parsed.Filters["clusters.cluster_id"] | ||
clusterIDs := make([]string, len(tempClusterIDs)) | ||
|
||
// Deep copy parsed.Filters["clusters.cluster_id"] | ||
copy(clusterIDs, tempClusterIDs) | ||
|
||
// Reset clusters.cluster_id | ||
parsed.Filters["clusters.cluster_id"] = []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.
I think this would work and be more efficient:
tempClusterIDs := parsed.Filters["clusters.cluster_id"] | |
clusterIDs := make([]string, len(tempClusterIDs)) | |
// Deep copy parsed.Filters["clusters.cluster_id"] | |
copy(clusterIDs, tempClusterIDs) | |
// Reset clusters.cluster_id | |
parsed.Filters["clusters.cluster_id"] = []string{} | |
clusterIDs := parsed.Filters["clusters.cluster_id"] | |
// Reset clusters.cluster_id | |
parsed.Filters["clusters.cluster_id"] = []string{} |
starRules, ok := allRules["*"] | ||
if ok && slices.Contains(starRules, "get") || slices.Contains(starRules, "*") { | ||
return parsed, 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.
The beginning of the function already checks this.
starRules, ok := allRules["*"] | |
if ok && slices.Contains(starRules, "get") || slices.Contains(starRules, "*") { | |
return parsed, nil | |
} |
|
||
if len(unAuthorizedClusters) > 0 { | ||
return parsed, fmt.Errorf("%w: the following cluster filters are not authorized: %s", | ||
ErrNotAuthorized, strings.Join(unAuthorizedClusters, ", ")) |
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 we should have a new error called ErrForbidden
to match the HTTP status codes we return. It's getting a bit confusing to use this for both cases.
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.
All my remaining comments are minor. I'll address them in a separate PR.
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: mprahl, yiraeChristineKim The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
if err != nil { | ||
if errors.Is(err, sql.ErrNoRows) { | ||
// Filter out invalid cluster IDs from the query | ||
continue |
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.
After thinking about this, I think this allows a user to bypass all authorization checks and have everything returned if they provide an invalid cluster ID.
I'll fix this in an upcoming PR.
I need to ensure that the compliance history can only be written by authorized service accounts and viewed by users with appropriate ACM access.
Definition of Done for Engineering Story Owner (Checklist)
Ref: https://issues.redhat.com/browse/ACM-6866
Signed-off-by: Yi Rae Kim [email protected]