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

DEVPROD-5753: Implement and apply requireHostAccess GQL directive #8337

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

SupaJoon
Copy link
Contributor

@SupaJoon SupaJoon commented Sep 23, 2024

DEVPROD-5753

Description

The actual added code to review is less than 540 lines.
These code changes implement and apply the @requireHostAccess directive to these mutations:

  • updateSpawnHostStatus
  • editSpawnHost
  • reprovisionToNew
  • restartJasper
  • updateHostStatus

and these queries:

  • host
  • hostEvents

I also applied the directive permission logic to the hosts query resolver instead of the schema since there is are no host IDs available before the host db query takes place.

These code changes combine the permission for editing spawn hosts and non-spawn hosts. Currently in Evergreen, the difference between the two implementations is that spawn hosts permission is granted if the host startedBy field is the same as the requesting user and the user has the host edit permission (code) whereas modifying a non-spawn host only checks the edit permission (code). I confirmed in staging that I am able to modify my spawn host with the current code changes and I could use edge case clarification from an App engineer.

e2e test is fixed in this UI PR: evergreen-ci/ui#410

@SupaJoon SupaJoon requested review from a team September 24, 2024 13:59
graphql/query_resolver.go Outdated Show resolved Hide resolved
Comment on lines 342 to 349
forbiddenHosts := []string{}
for _, h := range hosts {
if !userHasHostPermission(usr, h.Distro.Id, evergreen.HostsView.Value) {
forbiddenHosts = append(forbiddenHosts, h.Id)
}
}
if len(forbiddenHosts) > 0 {
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access one or more hosts.", usr.Username()))
Copy link
Contributor

Choose a reason for hiding this comment

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

since we don't really do anything with the forbiddenHosts, I wonder if it makes sense to do this check in the same loop where we're building the APIHosts below (and just return immediately if you encounter no permissions)

Copy link
Contributor

Choose a reason for hiding this comment

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

You can either do that or include a formatted list of all the forbidden hosts in the error message here

Copy link
Contributor Author

@SupaJoon SupaJoon Sep 27, 2024

Choose a reason for hiding this comment

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

I originally considered returning a list of hosts that are inaccessible but I think that is insecure since the query parameters match the hostIds in the error message which makes the error message really useful to someone who doesn't have host permissions

graphql/resolver.go Outdated Show resolved Hide resolved
return nil, ResourceNotFound.Send(ctx, "host not specified")
}
hostId, hasHostId := args["hostId"].(string)
hostIdsInterface, hasHostIds := args["hostIds"].([]interface{})
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to do this so you can skip the extra loop for hostIds 👀

Suggested change
hostIdsInterface, hasHostIds := args["hostIds"].([]interface{})
hostIds, hasHostIds := args["hostIds"].([]string)

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 this and hostIds was always empty

@@ -218,7 +234,87 @@ func setupPermissions(t *testing.T) {
}
require.NoError(t, roleManager.UpdateRole(logViewRole))
}

func TestRequireHostAccess(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The references to .Equal or .EqualError in this file should use assert rather than require for codebase style consistency.

Comment on lines 342 to 349
forbiddenHosts := []string{}
for _, h := range hosts {
if !userHasHostPermission(usr, h.Distro.Id, evergreen.HostsView.Value) {
forbiddenHosts = append(forbiddenHosts, h.Id)
}
}
if len(forbiddenHosts) > 0 {
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access one or more hosts.", usr.Username()))
Copy link
Contributor

Choose a reason for hiding this comment

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

You can either do that or include a formatted list of all the forbidden hosts in the error message here

if !hasHostId && !hasHostIds {
return nil, ResourceNotFound.Send(ctx, "host not specified")
}
hostIdsToCheck := []string{}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: initialize as []string{hostId} and remove L56

var requiredLevel int
if access == HostAccessLevelEdit {
requiredLevel = evergreen.HostsEdit.Value
} else if access == HostAccessLevelView {
Copy link
Contributor

Choose a reason for hiding this comment

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

is access not being specified a valid case? If not I would use else rather than else if

return nil, ResourceNotFound.Send(ctx, "No matching hosts found")
}
for _, h := range hostsToCheck {
if !userHasHostPermission(user, h.Distro.Id, requiredLevel) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be consistent with the logic here (as in whether we keep track of a list of forbidden hosts or not)

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 omitted the forbidden hosts from the error message from your link because those hostIDs are the result of a query. I think it makes sense to include the forbidden hostIDs in the directive error message because those hosts were supplied by the user. This could make it easier for the user to ask for help in the support channel.

SupaJoon and others added 9 commits September 27, 2024 10:27
Co-authored-by: minnakt <47064971+minnakt@users.noreply.github.com>
Co-authored-by: minnakt <47064971+minnakt@users.noreply.github.com>
@SupaJoon
Copy link
Contributor Author

SupaJoon commented Oct 2, 2024

evergreen retry

Comment on lines 66 to 68
if err != nil {
return nil, InternalServerError.Send(ctx, "Error getting hosts")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

should we include the err message here?

Comment on lines 78 to 83
if len(forbiddenHosts) == 1 {
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access host '%v'", user.Username(), forbiddenHosts[0]))
} else if len(forbiddenHosts) > 1 {
hostsString := strings.Join(forbiddenHosts, ", ")
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access hosts: '%v'", user.Username(), hostsString))
}
Copy link
Contributor

@minnakt minnakt Oct 3, 2024

Choose a reason for hiding this comment

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

could this be consolidated? I also think using %s might be slightly better than using %v since it's a string

Suggested change
if len(forbiddenHosts) == 1 {
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access host '%v'", user.Username(), forbiddenHosts[0]))
} else if len(forbiddenHosts) > 1 {
hostsString := strings.Join(forbiddenHosts, ", ")
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access hosts: '%v'", user.Username(), hostsString))
}
if len(forbiddenHosts) > 0 {
hostsString := strings.Join(forbiddenHosts, ", ")
return nil, Forbidden.Send(ctx, fmt.Sprintf("user '%s' does not have permission to access host(s): '%s'", user.Username(), hostsString))
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could be consolidated but I like how the copy is more specific in the original code. I updated %v to %s.

graphql/resolver.go Outdated Show resolved Hide resolved
SupaJoon and others added 3 commits October 3, 2024 15:47
Co-authored-by: minnakt <47064971+minnakt@users.noreply.github.com>
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.

3 participants