cloudfoundry-incubator/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/store/reducers/cf-users.reducer.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import { DISCONNECT_ENDPOINTS_SUCCESS, DisconnectEndpoint } from '../../../../store/src/actions/endpoint.actions';
import { IRequestEntityTypeState } from '../../../../store/src/app-state';
import { deepMergeState } from '../../../../store/src/helpers/reducer.helper';
import { APIResource, NormalizedResponse } from '../../../../store/src/types/api.types';
import { APISuccessOrFailedAction } from '../../../../store/src/types/request.types';
import { GET_ORGANIZATION_USERS_SUCCESS, GetAllOrgUsers } from '../../actions/organization.actions';
import {
  ADD_CF_ROLE_SUCCESS,
  ChangeCfUserRole,
  createDefaultCfUserRelations,
  REMOVE_CF_ROLE_SUCCESS,
} from '../../actions/users.actions';
import { IOrganization, ISpace } from '../../cf-api.types';
import { cfUserEntityType } from '../../cf-entity-types';
import { CF_ENDPOINT_TYPE } from '../../cf-types';
import {
  CfUser,
  CfUserMissingOrgRoles,
  CfUserMissingSpaceRoles,
  CfUserRoleParams,
  getDefaultCfUserMissingRoles,
  OrgUserRoleNames,
  SpaceUserRoleNames,
} from '../types/cf-user.types';

const properties = {
  org: {
    [OrgUserRoleNames.MANAGER]: CfUserRoleParams.MANAGED_ORGS,
    [OrgUserRoleNames.BILLING_MANAGERS]: CfUserRoleParams.BILLING_MANAGER_ORGS,
    [OrgUserRoleNames.AUDITOR]: CfUserRoleParams.AUDITED_ORGS,
    [OrgUserRoleNames.USER]: CfUserRoleParams.ORGANIZATIONS,
  },
  space: {
    [SpaceUserRoleNames.MANAGER]: CfUserRoleParams.MANAGED_SPACES,
    [SpaceUserRoleNames.AUDITOR]: CfUserRoleParams.AUDITED_SPACES,
    [SpaceUserRoleNames.DEVELOPER]: CfUserRoleParams.SPACES
  }
};

export function cfUserReducer(state: IRequestEntityTypeState<APIResource<CfUser>>, action: APISuccessOrFailedAction) {
  switch (action.type) {
    case ADD_CF_ROLE_SUCCESS:
    case REMOVE_CF_ROLE_SUCCESS:
      // Ensure that a user's roles collections are updated when we call add/remove
      const permAction = action.apiAction as ChangeCfUserRole;
      if (permAction.username) {
        return state;
      }
      const { entityGuid, isSpace, permissionTypeKey, userGuid } = permAction;
      return {
        ...state,
        [userGuid]: {
          ...state[userGuid],
          entity: updatePermission(state[userGuid].entity, entityGuid, isSpace, permissionTypeKey, action.type === ADD_CF_ROLE_SUCCESS),
        }
      };
    case GET_ORGANIZATION_USERS_SUCCESS:
      // Determine if any of the user's roles have not been provided
      return updateUserMissingRoles(state, action);
  }
  return state;
}

export function endpointDisconnectUserReducer(state: IRequestEntityTypeState<APIResource<CfUser>>, action: DisconnectEndpoint) {
  if (action.endpointType === CF_ENDPOINT_TYPE) {
    switch (action.type) {
      case DISCONNECT_ENDPOINTS_SUCCESS:
        const cfGuid = action.guid;
        // remove users that belong to this CF
        const newUsers = {};
        Object.values(state)
          .filter(u => u.entity.cfGuid !== cfGuid)
          .forEach(u => newUsers[u.metadata.guid] = u);
        return newUsers;
    }
  }
  return state;
}

function updatePermission(
  user: CfUser,
  entityGuid: string,
  isSpace: boolean,
  permissionType: OrgUserRoleNames | SpaceUserRoleNames,
  add = false) {
  const type = isSpace ? 'space' : 'org';
  const paramName = properties[type][permissionType];
  const newCollection = add ?
    [...user[paramName], entityGuid] :
    user[paramName].filter(guid => guid !== entityGuid);
  return {
    ...user,
    [paramName]: newCollection
  };
}

type StateEntity = ISpace | IOrganization;
type StateEntities<T> = IRequestEntityTypeState<APIResource<T>>;

export function userSpaceOrgReducer<T = StateEntity>(isSpace: boolean) {
  return (state: StateEntities<T>, action: APISuccessOrFailedAction) => {
    switch (action.type) {
      case ADD_CF_ROLE_SUCCESS:
      case REMOVE_CF_ROLE_SUCCESS:
        // Ensure that an org or space's roles lists are updated when we call add/remove
        const permAction = action.apiAction as ChangeCfUserRole;
        const isAdd = action.type === ADD_CF_ROLE_SUCCESS ? true : false;
        return (isSpace && !!permAction.isSpace) || (!isSpace && !permAction.isSpace) ? newEntityState<T>(state, permAction, isAdd) : state;
    }
    return state;
  };
}

function newEntityState<T = StateEntity>(state: StateEntities<T>, action: ChangeCfUserRole, add: boolean): StateEntities<T> {
  const apiResource: APIResource<T> = state[action.guid];
  if (!apiResource) {
    return state;
  }
  let roles: string[] = apiResource.entity[action.permissionTypeKey];
  if (!roles) {
    // No roles in entity, we can't modify them if they don't exist
    return state;
  }

  const index = roles.findIndex(guid => guid === action.userGuid);
  const exists = index >= 0;

  if (add && !exists) {
    // Add the user to the role as it doesn't exist already
    roles = [
      ...roles,
      action.userGuid
    ];
  } else if (!add && !exists) {
    // Remove the user from the role... but only if it exists already
    roles = [...roles];
    roles.splice(index, 1);
  } else {
    // There's been no change, return the original state
    return state;
  }

  return {
    ...state,
    [action.guid]: {
      ...apiResource,
      entity: Object.assign({}, apiResource.entity, {
        [action.permissionTypeKey]: roles
      })
    }
  };
}

/**
 * Determine if the user entity is missing any roles. If so track them in an array
 */
function updateUserMissingRoles(users: IRequestEntityTypeState<APIResource<CfUser>>, action: APISuccessOrFailedAction<NormalizedResponse>) {
  // At this point in the flow the request flow (APISuccessOrFailedAction), the users may or may not be in the store yet
  // (via WrapperRequestActionSuccess). Therefore in order to avoid partial entities we need to stick the whole user set into the store
  // including `missingRoles`.
  const usersInResponse: IRequestEntityTypeState<APIResource<CfUser>> = action.response.entities[cfUserEntityType];
  if (!usersInResponse) {
    return users;
  }

  // The GetAllOrgUsers request should contain the required roles, if not ignore this action (this will occur when we're fetching total
  // number of users in org and junking the result)
  const getOrgUsersAction: GetAllOrgUsers = action.apiAction as GetAllOrgUsers;
  const requiredRelations = createDefaultCfUserRelations();
  const hasRequiredRelations = requiredRelations.reduce((reqRels, rel) => {
    if (getOrgUsersAction.includeRelations.find(includedRel => includedRel === rel)) {
      reqRels.push(rel);
    }
    return reqRels;
  }, []).length === requiredRelations.length;
  if (!hasRequiredRelations) {
    return users;
  }

  // Create a delta of the changes, this will ensure we only return an updated state if there are updates
  const haveUpdatedUsers: boolean = Object.values(usersInResponse).reduce((changes, user) => {
    const oldMissingRoles = (users[user.metadata.guid] ? users[user.metadata.guid].entity.missingRoles : null)
      || getDefaultCfUserMissingRoles();
    const newMissingRoles = getDefaultCfUserMissingRoles();
    Object.values(CfUserRoleParams).forEach((roleParam) => {
      if (user.entity[roleParam]) {
        return;
      }
      // What's with all the `as`? Typing fun...
      if (isOrgRole(roleParam) ? oldMissingRoles.org.indexOf(roleParam as CfUserMissingOrgRoles) < 0 :
        oldMissingRoles.space.indexOf(roleParam as CfUserMissingSpaceRoles) < 0) {
        if (isOrgRole(roleParam)) {
          newMissingRoles.org.push(roleParam as CfUserMissingOrgRoles);
        } else {
          newMissingRoles.space.push(roleParam as CfUserMissingSpaceRoles);
        }
      }
    });
    user.entity.missingRoles = newMissingRoles;
    return changes || !!newMissingRoles.org.length || !!newMissingRoles.space.length;
  }, false);

  return haveUpdatedUsers ? deepMergeState(users, usersInResponse) : users;
}

function isOrgRole(role: string) {
  return role === CfUserRoleParams.AUDITED_ORGS ||
    role === CfUserRoleParams.BILLING_MANAGER_ORGS ||
    role === CfUserRoleParams.MANAGED_ORGS ||
    role === CfUserRoleParams.ORGANIZATIONS;
}