cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/user-permissions/cf-user-roles-fetch.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { HttpClient } from '@angular/common/http';
import { Action, Store } from '@ngrx/store';
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, first, map, pairwise, share, skipWhile, switchMap, tap } from 'rxjs/operators';

import { AppState } from '../../../store/src/app-state';
import { entityCatalog } from '../../../store/src/entity-catalog/entity-catalog';
import {
  EntityUserRolesEndpoint,
  EntityUserRolesFetch,
} from '../../../store/src/entity-request-pipeline/entity-request-pipeline.types';
import {
  BaseHttpClientFetcher,
  flattenPagination,
  PaginationFlattener,
} from '../../../store/src/helpers/paginated-request-helpers';
import { ActionState } from '../../../store/src/reducers/api-request-reducer/types';
import { connectedEndpointsOfTypesSelector } from '../../../store/src/selectors/endpoint.selectors';
import { selectPaginationState } from '../../../store/src/selectors/pagination.selectors';
import { BasePaginatedAction, PaginationEntityState } from '../../../store/src/types/pagination.types';
import {
  CfUserRelationTypes,
  GET_CURRENT_CF_USER_RELATIONS,
  GET_CURRENT_CF_USER_RELATIONS_FAILED,
  GET_CURRENT_CF_USER_RELATIONS_SUCCESS,
  GetCfUserRelations,
  GetCurrentCfUserRelations,
  GetCurrentCfUserRelationsComplete,
} from '../actions/permissions.actions';
import { cfEntityCatalog } from '../cf-entity-catalog';
import { CF_ENDPOINT_TYPE } from '../cf-types';
import { CFResponse } from '../store/types/cf-api.types';

const createEndpointArray = (
  store: Store<AppState>,
  endpoints: string[] | EntityUserRolesEndpoint[]
): Observable<EntityUserRolesEndpoint[]> => {
  // If there's no endpoints get all from store. Alternatively fetch specific endpoint id's from store
  if (!endpoints || !endpoints.length || typeof (endpoints[0]) === 'string') {
    const endpointIds = endpoints as string[];
    return store.select(connectedEndpointsOfTypesSelector(CF_ENDPOINT_TYPE)).pipe(
      first(),
      map(cfEndpoints => endpointIds.length === 0 ?
        Object.values(cfEndpoints) :
        Object.values(cfEndpoints).filter(cfEndpoint => endpointIds.find(endpointId => endpointId === cfEndpoint.guid))
      ),
    );
  }
  return of(endpoints as EntityUserRolesEndpoint[]);
};

export const cfUserRolesFetch: EntityUserRolesFetch = (
  endpoints: string[] | EntityUserRolesEndpoint[],
  store: Store<AppState>,
  httpClient: HttpClient
) => {
  return createEndpointArray(store, endpoints).pipe(
    switchMap((cfEndpoints: EntityUserRolesEndpoint[]) => {
      const isAllAdmins = cfEndpoints.every(endpoint => !!endpoint.user.admin);
      // If all endpoints are connected as admin, there's no permissions to fetch. So only update the permission state to initialised
      if (isAllAdmins) {
        cfEndpoints.forEach(endpoint => store.dispatch(new GetCfUserRelations(endpoint.guid, GET_CURRENT_CF_USER_RELATIONS_SUCCESS)));
      } else {
        // If some endpoints are not connected as admin, go out and fetch the current user's specific roles
        const flagsAndRoleRequests = dispatchRoleRequests(cfEndpoints, store, httpClient);
        const allRequestsCompleted = handleCfRequests(flagsAndRoleRequests);
        return combineLatest(allRequestsCompleted).pipe(
          map(succeeds => succeeds.every(succeeded => !!succeeded)),
        );
      }
      return of(true);
    })
  );
};

interface CfsRequestState {
  [cfGuid: string]: Observable<boolean>[];
}

interface IEndpointConnectionInfo {
  guid: string;
  userGuid: string;
}

function dispatchRoleRequests(
  endpoints: EntityUserRolesEndpoint[],
  store: Store<AppState>,
  httpClient: HttpClient
): CfsRequestState {
  const requests: CfsRequestState = {};

  // Per endpoint fetch feature flags and user roles (unless admin, where we don't need to), then mark endpoint as initialised
  endpoints.forEach(endpoint => {
    if (endpoint.user.admin) {
      // We don't need permissions for admin users (they can do everything)
      requests[endpoint.guid] = [of(true)];
      store.dispatch(new GetCfUserRelations(endpoint.guid, GET_CURRENT_CF_USER_RELATIONS_SUCCESS));
    } else {
      // START fetching cf roles for current user
      store.dispatch(new GetCfUserRelations(endpoint.guid, GET_CURRENT_CF_USER_RELATIONS));

      // Dispatch feature flags fetch actions
      const ffAction = cfEntityCatalog.featureFlag.actions.getMultiple(endpoint.guid);
      requests[endpoint.guid] = [createPaginationCompleteWatcher(store, ffAction)];
      store.dispatch(ffAction);

      // Dispatch requests to fetch roles per role type for current user
      requests[endpoint.guid].push(...fetchCfUserRoles({ guid: endpoint.guid, userGuid: endpoint.user.guid }, store, httpClient));

      // FINISH fetching cf roles for current user
      combineLatest(requests[endpoint.guid]).pipe(
        first(),
        tap(succeeds => {
          store.dispatch(new GetCfUserRelations(
            endpoint.guid,
            succeeds.every(succeeded => !!succeeded) ? GET_CURRENT_CF_USER_RELATIONS_SUCCESS : GET_CURRENT_CF_USER_RELATIONS_FAILED)
          );
        }),
        catchError(err => {
          console.warn('Failed to fetch current user permissions for a cf: ', err);
          store.dispatch(new GetCfUserRelations(endpoint.guid, GET_CURRENT_CF_USER_RELATIONS_FAILED));
          return of(err);
        })
      ).subscribe();
    }
  });
  return requests;
}

function handleCfRequests(requests: CfsRequestState): Observable<boolean>[] {
  const allCompleted: Observable<boolean>[] = [];
  Object.keys(requests).forEach(cfGuid => {
    const successes = requests[cfGuid];
    allCompleted.push(...successes);
  });
  return allCompleted;
}

function fetchCfUserRoles(endpoint: IEndpointConnectionInfo, store: Store<AppState>, httpClient: HttpClient): Observable<boolean>[] {
  return Object.values(CfUserRelationTypes).map((type: CfUserRelationTypes) => {
    const relAction = new GetCurrentCfUserRelations(endpoint.userGuid, type, endpoint.guid);
    return fetchCfUserRole(store, relAction, httpClient);
  });
}

class PermissionFlattener extends BaseHttpClientFetcher<CFResponse> implements PaginationFlattener<CFResponse, CFResponse> {

  constructor(httpClient: HttpClient, public url, public requestOptions: { [key: string]: any, }) {
    super(httpClient, url, requestOptions, 'page');
  }
  public getTotalPages = (res: CFResponse) => res.total_pages;

  public mergePages = (res: CFResponse[]) => {
    const firstRes = res.shift();
    const final = res.reduce((finalRes, currentRes) => {
      finalRes.resources = [
        ...finalRes.resources,
      ];
      return finalRes;
    }, firstRes);
    return final;
  };
  public getTotalResults = (res: CFResponse): number => res.total_results;
  public clearResults = (res: CFResponse) => of(res);
}

export function fetchCfUserRole(store: Store<AppState>, action: GetCurrentCfUserRelations, httpClient: HttpClient): Observable<boolean> {
  const url = `pp/v1/proxy/v2/users/${action.guid}/${action.relationType}`;
  const params = {
    headers: {
      'x-cap-cnsi-list': action.endpointGuid,
      'x-cap-passthrough': 'true'
    },
    params: {
      'results-per-page': '100'
    }
  };
  const get$ = httpClient.get<CFResponse>(
    url,
    params
  );
  return flattenPagination(
    (flatAction: Action) => store.dispatch(flatAction),
    get$,
    new PermissionFlattener(httpClient, url, params)
  ).pipe(
    map(data => {
      store.dispatch(new GetCurrentCfUserRelationsComplete(action.relationType, action.endpointGuid, data.resources));
      return true;
    }),
    first(),
    catchError(err => of(false)),
    share()
  );
}

const fetchPaginationStateFromAction = (store: Store<AppState>, action: BasePaginatedAction) => {
  const entityKey = entityCatalog.getEntityKey(action);
  return store.select(selectPaginationState(entityKey, action.paginationKey));
};

/**
 * Using the given action wait until the associated pagination section changes from busy to not busy
 */
const createPaginationCompleteWatcher = (store: Store<AppState>, action: BasePaginatedAction): Observable<boolean> =>
  fetchPaginationStateFromAction(store, action).pipe(
    map((paginationState: PaginationEntityState) => {
      const pageRequest: ActionState =
        paginationState && paginationState.pageRequests && paginationState.pageRequests[paginationState.currentPage];
      return pageRequest ? pageRequest.busy : true;
    }),
    pairwise(),
    map(([oldFetching, newFetching]) => {
      return oldFetching === true && newFetching === false;
    }),
    skipWhile(completed => !completed),
    first(),
  );