GCTC-NTGC / gc-digital-talent

GC Digital Talent is the new recruitment platform for digital and tech jobs in the Government of Canada. // Talents numériques du GC est la nouvelle plateforme de recrutement pour les emplois numériques et technologiques au gouvernement du Canada.
https://talent.canada.ca
GNU Affero General Public License v3.0
16 stars 7 forks source link

✨ Restrict privileged api actions behind network firewall #9866

Closed tristan-orourke closed 2 months ago

tristan-orourke commented 3 months ago

✨ Feature

We currently restrict admin pages behind the network firewall. However, the full API can be accessed from anywhere, and that is how any real interaction with the database happens. We want to be able to ensure that privileged roles and actions are restricted to government employees. The easiest way to do that is restrict those actions to the government network, which is already restricted to government employees.

🕵️ Details

The plan is to host a second API endpoint which behind the firewall. /admin/graphql would work. And then, anywhere we're checking for permissions, we somehow check which api endpoint is being used. If the request came from the public endpoint, then we only allow permissions which are available to Guest, Base, or Applicant users to pass authorization.

We also need to be very careful on any authorizedToView scopes.

We will also need to change the client-side admin pages to use the new admin endpoint.

Helpful documentation:

🙋‍♀️ Proposed Implementation

How to check for allowed permissions/roles

From the public access point, we only want to consider permissions that come from Guest, Base User, or Applicant roles. I've been thinking how to implement this, and I think you'd have to check if the user has the permission (what already happens), and then additionally check if the Role has that permission, and the user has the role. It would look something like the following:

isAbleTo( $permission, $team, $requireAll) {
  $usingSecureApi= isUsingSecureApi() ; // Check if we're on the protected endpoint somehow
  if ($usingSecureApi) {
    return parent::isAbleTo($permission, $team, $requireAll);
  } else {
    return self::canUsePublicPermission($permission) && parent::isAbleTo($permission, $team, $requireAll);  // what order of checking is likely to be most performant?
  }
}

canUsePublicPermission($permission) {
  $publicRoles = Role::whereIn('key', ['guest', 'base_user', 'applicant'])->get();
  $user = Auth::user();
  foreach($publicRoles as $role) {
    if ($role->hasPermission($permission) && $user->hasRole($role)) { // what order of checking is likely to be most performant?
      return true;
    }
  }
  return false;
}

isUsingSecureApi(Request $request) { 
  // How do we get the request object? Can you inject the request into any method, or only route controller methods? Is there a facade for accessing it? https://laravel.com/docs/11.x/requests#accessing-the-request
  return $request->is('admin/*'); 
}

Any authorizedToViewScope functions will have to work similarly. They'll have to check which endpoint was used independently.

How to override the permission check

Instead of overriding the method on User model, we may want to create our own Laratrust checker. We could use different checkers depending on the endpoint used, or check if we're on the admin endpoint within the checker code.

Hosting the api at multiple endpoints

There's a thread [here](https://github.com/nuwave/lighthouse/issues/273 about hosting multiple graphql schemas with Lighthouse. We want to host the same schema at two endpoints, but I think we can use a similar solution to what is proposed there. Lighthouse config file has a route.uri variable, which tells lighthouse which route to respond to. But we can use middleware to edit that config variable if going through a second endpoint, so Lighthouse handles that endpoint instead. This middleware might be a good time to do other things as well, like overriding the Laratrust checker.

If this strategy doesn't work, @petertgiles had an idea for using a Nginx fastcgi parameter to record that we came through one or the other endpoint, before overriding or redirecting to the main endpoint.

Laravel's context facade might be a good way of sharing any info that gets determined in middleware like this.

✅ Acceptance Criteria

tristan-orourke commented 3 months ago

Another option for hosting the second endpoint could be a new subdomain.

Will it be a challenge to hit two endpoints with urql client? Do we have two client providers? Or do we have a way to change state somehow?