Skip to content

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

Merged
merged 2 commits into from
Feb 19, 2024

Conversation

yiraeChristineKim
Copy link
Contributor

@yiraeChristineKim yiraeChristineKim commented Jan 23, 2024

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)

  • The read API endpoint is restricted based on a user's "get" access to corresponding ManagedCluster objects. This result will filter the requests returned in the SQL queries.

Ref: https://issues.redhat.com/browse/ACM-6866
Signed-off-by: Yi Rae Kim [email protected]

if !isAuth {
log.Error(err, "Do not have the Read authorization for "+ce.Cluster.Name)

continue
Copy link
Contributor Author

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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

jwt

@yiraeChristineKim yiraeChristineKim force-pushed the ACM-6866 branch 2 times, most recently from 3e70f0f to 03309b1 Compare January 23, 2024 13:19
}

ssar := &authorizationv1.SubjectAccessReview{
Spec: authorizationv1.SubjectAccessReviewSpec{
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 this correct? do we want to check replicated policies in managedcluster namespace?

Copy link
Member

@JustinKuli JustinKuli left a 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.

}

if !isAuth {
log.Error(err, "Do not have the Read authorization for "+complianceEvent.Cluster.Name)
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

@JustinKuli JustinKuli left a 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.

@yiraeChristineKim
Copy link
Contributor Author

yiraeChristineKim commented Feb 1, 2024

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 SelfSubjectRulesReview(github.com/stolostron/rbac-api-utils) to get all roles at once. That needs to impersonate. Otherwise, like using SubjectAccessReview, I have to ping kubernetes API per a line. Do you have a good idea?

@yiraeChristineKim
Copy link
Contributor Author

yiraeChristineKim commented Feb 1, 2024

Hi all, since I opened #167 after 167 merged then I will work on this 164 PR Thanks

@yiraeChristineKim yiraeChristineKim force-pushed the ACM-6866 branch 3 times, most recently from b55fa91 to 9e442f6 Compare February 2, 2024 15:36
@JustinKuli
Copy link
Member

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 SelfSubjectRulesReview(github.com/stolostron/rbac-api-utils) to get all roles at once. That needs to impersonate. Otherwise, like using SubjectAccessReview, I have to ping kubernetes API per a line. Do you have a good idea?

Limiting the number of API calls is a good reason to favor SelfSubjectRulesReview, I wasn't thinking about that.

return nil, fmt.Errorf("failed to parse token")
}
// Token include Bearer prefix
config.Impersonate = rest.ImpersonationConfig{
Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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?

Copy link
Contributor Author

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) {
Copy link
Member

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?

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 did line 1802

@yiraeChristineKim yiraeChristineKim force-pushed the ACM-6866 branch 2 times, most recently from 450f9e6 to 2ee8a7e Compare February 16, 2024 19:31
return parsed, nil
}

// getAuthorizedClusterNames verifies that if a cluster filter is provided,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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],
Copy link
Member

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.

Copy link
Contributor Author

@yiraeChristineKim yiraeChristineKim Feb 19, 2024

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 {
Copy link
Member

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 {
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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]>
userConfig, err := getUserKubeConfig(s.cfg, r)
if err != nil {
if errors.Is(err, ErrNotAuthorized) {
writeErrMsgJSON(w, "The Authorization header is not set", http.StatusForbidden)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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

Comment on lines +536 to +543
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{}
Copy link
Member

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:

Suggested change
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{}

Comment on lines +575 to +578
starRules, ok := allRules["*"]
if ok && slices.Contains(starRules, "get") || slices.Contains(starRules, "*") {
return parsed, nil
}
Copy link
Member

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.

Suggested change
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, ", "))
Copy link
Member

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.

Copy link
Member

@mprahl mprahl left a 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.

@openshift-ci openshift-ci bot added the lgtm label Feb 19, 2024
Copy link

openshift-ci bot commented Feb 19, 2024

[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:
  • OWNERS [mprahl,yiraeChristineKim]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mprahl mprahl dismissed JustinKuli’s stale review February 19, 2024 18:09

Will fix in follow up PR

@mprahl mprahl merged commit 2cc5ff6 into open-cluster-management-io:main Feb 19, 2024
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Filter out invalid cluster IDs from the query
continue
Copy link
Member

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.

@yiraeChristineKim yiraeChristineKim deleted the ACM-6866 branch February 20, 2024 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants