Vizzuality/landgriffon

View on GitHub
api/src/modules/authorization/formodule/scenario-ownership.interceptor.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import {
  applyDecorators,
  CallHandler,
  ExecutionContext,
  ForbiddenException,
  Injectable,
  NestInterceptor,
  SetMetadata,
  UseInterceptors,
} from '@nestjs/common';
import { ScenariosAccessControl } from 'modules/authorization/formodule/scenarios.access-control.service';
import { Observable } from 'rxjs';
import { Request } from 'express';
import { Reflector } from '@nestjs/core';

interface ScenarioCheckWhiteListOptions {
  bypassIfScenarioIsPublic?: boolean;

  isComparisonMode?: boolean;
}

const SCENARIO_CHECK_METADATA_KEY: string = 'scenarioChecks';

@Injectable()
export class ScenarioOwnershipInterceptor implements NestInterceptor {
  constructor(
    private readonly scenarioAcl: ScenariosAccessControl,
    private readonly reflector: Reflector,
  ) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    if (this.scenarioAcl.isUserAdmin()) {
      return next.handle();
    }
    const request: Request = context.switchToHttp().getRequest();
    const additionalChecks: ScenarioCheckWhiteListOptions =
      this.reflector.getAllAndOverride<ScenarioCheckWhiteListOptions>(
        SCENARIO_CHECK_METADATA_KEY,
        [context.getHandler(), context.getClass()],
      );
    if (additionalChecks?.isComparisonMode) {
      const isComparisonAuthorized: boolean =
        await this.isComparisonRequestAuthorized(request, additionalChecks);
      if (!isComparisonAuthorized) {
        throw new ForbiddenException();
      }
      return next.handle();
    }

    const scenarioId: string = request.params.id;
    let isPublic: boolean = false;
    const isOwned: boolean = await this.scenarioAcl.ownsScenario(scenarioId);
    if (additionalChecks?.bypassIfScenarioIsPublic) {
      isPublic = await this.scenarioAcl.isScenarioPublic(scenarioId);
    }

    if (!isPublic && !isOwned) {
      throw new ForbiddenException();
    }
    return next.handle();
  }

  /**
   * @description: Checks if the user is authorized to compare the scenarios
   *               the user must either own the scenarios or the scenarios must be public
   */

  private async isComparisonRequestAuthorized(
    request: Request,
    additionalChecks: ScenarioCheckWhiteListOptions,
  ): Promise<boolean> {
    const baseScenarioId: string = request.query.baseScenarioId as string;
    const comparedScenarioId: string = request.query
      .comparedScenarioId as string;

    const baseIsOwned: boolean = baseScenarioId
      ? await this.scenarioAcl.ownsScenario(baseScenarioId)
      : true;
    const comparedIsOwned: boolean = comparedScenarioId
      ? await this.scenarioAcl.ownsScenario(comparedScenarioId)
      : true;

    let baseIsPublic: boolean = false;
    let comparedIsPublic: boolean = false;
    if (additionalChecks?.bypassIfScenarioIsPublic) {
      if (baseScenarioId) {
        baseIsPublic = await this.scenarioAcl.isScenarioPublic(baseScenarioId);
      }
      if (comparedScenarioId) {
        comparedIsPublic = await this.scenarioAcl.isScenarioPublic(
          comparedScenarioId,
        );
      }
    }

    const authorizedBaseScenario: boolean = baseScenarioId
      ? baseIsPublic || baseIsOwned
      : true;
    const authorizedComparedScenario: boolean =
      comparedIsPublic || comparedIsOwned;

    return authorizedBaseScenario && authorizedComparedScenario;
  }
}

export function CheckUserOwnsScenario(
  options?: ScenarioCheckWhiteListOptions,
): any {
  return applyDecorators(
    SetMetadata(SCENARIO_CHECK_METADATA_KEY, options),
    UseInterceptors(ScenarioOwnershipInterceptor),
  );
}