cloudfoundry/stratos

View on GitHub
src/frontend/packages/cloud-foundry/src/features/cf/users/manage-users/cf-roles.service.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import {
  combineLatest as combineLatestOperators,
  distinctUntilChanged,
  filter,
  first,
  map,
  publishReplay,
  refCount,
  startWith,
  switchMap,
} from 'rxjs/operators';

import { CurrentUserPermissionsService } from '../../../../../../core/src/core/permissions/current-user-permissions.service';
import { endpointEntityType } from '../../../../../../store/src/helpers/stratos-entity-factory';
import { APIResource, EntityInfo } from '../../../../../../store/src/types/api.types';
import { UsersRolesSetChanges } from '../../../../actions/users-roles.actions';
import { IOrganization, ISpace } from '../../../../cf-api.types';
import { CFAppState } from '../../../../cf-app-state';
import { cfEntityCatalog } from '../../../../cf-entity-catalog';
import { organizationEntityType, spaceEntityType } from '../../../../cf-entity-types';
import {
  createEntityRelationKey,
  createEntityRelationPaginationKey,
} from '../../../../entity-relations/entity-relations.types';
import { CfUserService } from '../../../../shared/data-services/cf-user.service';
import { createDefaultOrgRoles, createDefaultSpaceRoles } from '../../../../store/reducers/cf-users-roles.reducer';
import {
  selectCfUsersRolesCf,
  selectCfUsersRolesPicked,
  selectCfUsersRolesRoles,
} from '../../../../store/selectors/cf-users-roles.selector';
import { CfUser, IUserPermissionInOrg, UserRoleInOrg, UserRoleInSpace } from '../../../../store/types/cf-user.types';
import { CfRoleChange, CfUserRolesSelected } from '../../../../store/types/users-roles.types';
import { CfUserPermissionsChecker } from '../../../../user-permissions/cf-user-permissions-checkers';
import { canUpdateOrgSpaceRoles } from '../../cf.helpers';

@Injectable()
export class CfRolesService {

  existingRoles$: Observable<CfUserRolesSelected>;
  newRoles$: Observable<IUserPermissionInOrg>;
  loading$: Observable<boolean>;
  cfOrgs: { [cfGuid: string]: Observable<APIResource<IOrganization>[]>, } = {};

  /**
   * Given a list of orgs or spaces remove those that the connected user cannot edit roles in.
   */
  static filterEditableOrgOrSpace<T extends IOrganization | ISpace>(
    userPerms: CurrentUserPermissionsService,
    isOrg: boolean,
    orgOrSpaces$: Observable<APIResource<T>[]>
  ): Observable<APIResource<T>[]> {
    return orgOrSpaces$.pipe(
      // Create an observable containing the original list of organisations and a corresponding list of whether an org can be edited
      switchMap(orgsOrSpaces => {
        return combineLatest(
          observableOf(orgsOrSpaces),
          combineLatest(orgsOrSpaces.map(orgOrSpace => CfRolesService.canEditOrgOrSpace(
            userPerms,
            orgOrSpace.metadata.guid,
            orgOrSpace.entity.cfGuid,
            isOrg ? orgOrSpace.metadata.guid : (orgOrSpace as APIResource<ISpace>).entity.organization_guid,
            isOrg ? CfUserPermissionsChecker.ALL_SPACES : orgOrSpace.metadata.guid,
          ))));
      }),
      // Filter out orgs than the current user cannot edit
      map(([orgs, canEdit]) => orgs.filter(org => canEdit.find(canEditOrgOrSpace => canEditOrgOrSpace.guid === org.metadata.guid).canEdit)),
    );
  }

  /**
   * Create an observable with an org/space guids and whether it can be edited by the connected user
   */
  static canEditOrgOrSpace<T>(
    userPerms: CurrentUserPermissionsService,
    guid: string,
    cfGuid: string,
    orgGuid: string,
    spaceGuid): Observable<{ guid: string, canEdit: boolean, }> {
    return canUpdateOrgSpaceRoles(userPerms, cfGuid, orgGuid, spaceGuid).pipe(
      first(),
      map(canEdit => ({ guid, canEdit }))
    );
  }

  constructor(
    private store: Store<CFAppState>,
    private cfUserService: CfUserService,
    private userPerms: CurrentUserPermissionsService,
  ) {
    this.existingRoles$ = this.store.select(selectCfUsersRolesPicked).pipe(
      combineLatestOperators(this.store.select(selectCfUsersRolesCf)),
      filter(([users, cfGuid]) => !!cfGuid),
      switchMap(([users, cfGuid]) => this.populateRoles(cfGuid, users)),
      distinctUntilChanged(),
      publishReplay(1),
      refCount()
    );
    this.newRoles$ = this.store.select(selectCfUsersRolesRoles).pipe(
      distinctUntilChanged(),
      publishReplay(1),
      refCount()
    );

    this.loading$ = this.existingRoles$.pipe(
      combineLatestOperators(this.newRoles$),
      map(([existingRoles, newRoles]) => !existingRoles || !newRoles),
      startWith(true),
    );
  }

  /**
   * Take the structure that cf stores user roles in (per user and flat) and convert into a format that's easier to use and compare with
   * (easier to access at specific levels, easier to parse pieces around)
   */
  populateRoles(cfGuid: string, selectedUsers: CfUser[]): Observable<CfUserRolesSelected> {
    if (!cfGuid || !selectedUsers || selectedUsers.length === 0) {
      return observableOf({});
    }

    const userGuids = selectedUsers.map(user => user.guid);
    return this.cfUserService.getUsers(cfGuid).pipe(
      map(users => {
        const roles = {};
        // For each user (excluding those that are not selected)....
        users.forEach(user => {
          if (userGuids.indexOf(user.metadata.guid) >= 0) {
            this.populateUserRoles(user, roles);
          }
        });
        return roles;
      }),
    );
  }

  private populateUserRoles(user: APIResource<CfUser>, roles: CfUserRolesSelected) {
    const mappedUser: { [orgGuid: string]: IUserPermissionInOrg, } = {};
    const orgRoles = this.cfUserService.getOrgRolesFromUser(user.entity);
    const spaceRoles = this.cfUserService.getSpaceRolesFromUser(user.entity);
    // ... populate org roles ...
    orgRoles.forEach(org => {
      mappedUser[org.orgGuid] = {
        ...org,
        spaces: {}
      };
    });
    // ... and for each space, populate space roles
    spaceRoles.forEach(space => {
      if (!mappedUser[space.orgGuid]) {
        mappedUser[space.orgGuid] = createDefaultOrgRoles(space.orgGuid, space.orgName);
      }
      if (!space.orgName && mappedUser[space.orgGuid]) {
        space.orgName = mappedUser[space.orgGuid].name;
      }
      mappedUser[space.orgGuid].spaces[space.spaceGuid] = {
        ...space
      };
    });
    roles[user.metadata.guid] = mappedUser;
  }

  /**
   * Create a collection of role `change` items representing the diff between existing roles and newly selected roles.
   */
  createRolesDiff(orgGuid: string): Observable<CfRoleChange[]> {
    return this.existingRoles$.pipe(
      combineLatestOperators(
        this.newRoles$,
        this.store.select(selectCfUsersRolesPicked),
      ),
      first(),
      map(([existingRoles, newRoles, pickedUsers]) => {
        const changes = [];
        // For each user, loop through the new roles and compare with any existing. If there's a diff, add it to a changes collection to be
        // returned
        pickedUsers.forEach(user => {
          changes.push(...this.createRolesUserDiff(existingRoles, newRoles, changes, user, orgGuid));
        });
        this.store.dispatch(new UsersRolesSetChanges(changes));
        return changes;
      })
    );
  }

  private createRolesUserDiff(
    existingRoles: CfUserRolesSelected,
    newRoles: IUserPermissionInOrg,
    changes: CfRoleChange[],
    user: CfUser,
    orgGuid: string
  ): CfRoleChange[] {
    const existingUserRoles = existingRoles[user.guid] || {};
    const newChanges: CfRoleChange[] = [];

    // Compare org roles
    const existingOrgRoles = existingUserRoles[orgGuid] || createDefaultOrgRoles(orgGuid, newRoles.name);
    newChanges.push(...this.comparePermissions({
      userGuid: user.guid,
      orgGuid,
      orgName: newRoles.name,
      add: false,
      role: null
    },
      existingOrgRoles.permissions, newRoles.permissions));

    // Compare space roles
    Object.keys(newRoles.spaces).forEach(spaceGuid => {
      const newSpace = newRoles.spaces[spaceGuid];
      const oldSpace = existingOrgRoles.spaces[spaceGuid] || createDefaultSpaceRoles(orgGuid, newRoles.name, spaceGuid, newSpace.name);
      newChanges.push(...this.comparePermissions({
        userGuid: user.guid,
        orgGuid,
        orgName: newRoles.name,
        spaceGuid,
        spaceName: newSpace.name,
        add: false,
        role: null
      },
        oldSpace.permissions, newSpace.permissions));
    });

    return newChanges;
  }

  fetchOrg(cfGuid: string, orgGuid: string): Observable<EntityInfo<APIResource<IOrganization>>> {
    return cfEntityCatalog.org.store.getEntityService(orgGuid, cfGuid, { includeRelations: [], populateMissing: false })
      .waitForEntity$;
  }

  fetchOrgEntity(cfGuid: string, orgGuid: string): Observable<APIResource<IOrganization>> {
    return this.fetchOrg(cfGuid, orgGuid).pipe(
      filter(entityInfo => !!entityInfo.entity),
      map(entityInfo => entityInfo.entity),
    );
  }

  fetchOrgs(cfGuid: string): Observable<APIResource<IOrganization>[]> {
    if (!this.cfOrgs[cfGuid]) {
      const paginationKey = createEntityRelationPaginationKey(endpointEntityType, cfGuid);
      const orgs$ = cfEntityCatalog.org.store.getPaginationService(
        cfGuid,
        paginationKey,
        {
          includeRelations: [
            createEntityRelationKey(organizationEntityType, spaceEntityType)
          ], populateMissing: true
        }
      ).entities$;
      this.cfOrgs[cfGuid] = CfRolesService.filterEditableOrgOrSpace<IOrganization>(this.userPerms, true, orgs$).pipe(
        map(orgs => orgs.sort((a, b) => a.entity.name.localeCompare(b.entity.name))),
        publishReplay(1),
        refCount()
      );
    }
    return this.cfOrgs[cfGuid];
  }

  /**
   * Compare a set of org or space permissions and return the differences
   */
  private comparePermissions(
    template: CfRoleChange,
    oldPerms: UserRoleInOrg | UserRoleInSpace,
    newPerms: UserRoleInOrg | UserRoleInSpace)
    : CfRoleChange[] {
    const changes = [];
    Object.keys(oldPerms).forEach(permKey => {
      if (newPerms[permKey] === undefined) {
        // Skip this, the user hasn't set it
        return;
      }
      if (!!oldPerms[permKey] !== !!newPerms[permKey]) {
        changes.push({
          ...template,
          add: !!newPerms[permKey],
          role: permKey
        });
      }
    });
    return changes;

  }

}