Table of Contents
- UMRS Demo Quick Start
 - Setup
 - Frontend
 - UMRS Developers Guide
 - Add UMRS Role
 - Requesting and Sharing Access
 - Submit an Invitation for a Grant for a Resource
 - Submit a Request for a Grant for a Resource
 - Add Grant to User
 - Check all Grants for Access
 - Check an Individual Grant for Access
 - Retrieve all users with a specific grant
 - Retrieve all invitations for a user
 - Retrieve all requests for a user
 - Retrieve all grants
 - Retrieve all pending requests
 - Accept a pending request
 - Reject a pending request
 - Check a user for manager role
 
UMRS Demo Quick Start
Setup
In order to work with UMRS, you will need the following to be set up in your Angular application:
Frontend
Services
- A 
config.service.tsfile for configuring Auth 
import { Injectable } from '@angular/core';
import { UserManagerSettings } from 'oidc-client';
import { environment } from '../../environments/environment';
@Injectable({
    providedIn: 'root'
})
export class ConfigService {
    clientBaseUrl: string = environment.clientBaseUrl;
    serverBaseUrl: string = environment.serverBaseUrl;
    authority: string = environment.authority;
    clientId: string = environment.clientId;
    resource: string = environment.resource;
    public getClientSettings(): UserManagerSettings {
        return {
            authority: `${this.authority}`,
            resource: this.resource,
            client_id: this.clientId,
            redirect_uri: `${this.clientBaseUrl}/auth-callback`,
            post_logout_redirect_uri: `${this.clientBaseUrl}/auth-logout`,
            response_type: "code",
            scope: "openid profile email tenant",
            filterProtocolClaims: true,
            loadUserInfo: true,
            automaticSilentRenew: false,
        } as any;
    }
}
- A 
web-request.service.tsfile for sending CRUD calls to the backend 
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse} from '@angular/common/http';
import { throwError, Observable} from 'rxjs';
import { ConfigService } from 'src/app/Services/config.service';
@Injectable({
    providedIn: 'root'
})
export class WebRequestService {
    readonly ROOT_URL;
    
    constructor(private http: HttpClient, private configService:ConfigService) {
        this.ROOT_URL = configService.serverBaseUrl;
    }
// serverBaseUrl is set to the location of the database; in the examples, this is https://localhost:3000
// Get command for the DB
    get<T>(uri: string): Observable<T> {
        return this.http.get<T>(`${this.ROOT_URL}/${uri}`) as Observable<T>
    }
// Posting values to the DB
    post(uri: string, payload:Object) {
        return this.http.post(`${this.ROOT_URL}/${uri}`, payload)
    }
// Updating values on the DB
    patch(uri: string, payload:Object) {
        return this.http.patch(`${this.ROOT_URL}/${uri}`, payload)
    }
// Deleting values on the DB
    delete(uri: string) {
        return this.http.delete(`${this.ROOT_URL}/${uri}`)
    }
    handleError(error: HttpErrorResponse){
        console.log("You are unauthorized to access this");
        console.log(error)
        return throwError(error);
    }
}
- An 
auth.service.tsfile for login/logout functionality 
import { Injectable } from '@angular/core';
import { UserManager, User } from 'oidc-client';
import { ConfigService } from './config.service';
import { Observable, Subject } from 'rxjs';
export class AuthServiceEvent {
    constructor(private _message: string) {}
    
    get message(): string {
        return this._message;
    }
}
@Injectable({
    providedIn: 'root',
})
export class AuthService {
    private manager: UserManager;
    private user: User | null = null;
    private notifications: Subject<AuthServiceEvent> =
    new Subject<AuthServiceEvent>();
    
    constructor(private configService: ConfigService) {
        this.manager = new UserManager(configService.getClientSettings());
        this.manager.events.addUserSessionChanged(() => {
            this.notifications.next(new AuthServiceEvent('User session changed'));
            setInterval(() => {
                this.manager
                .signoutRedirect()
                .then(function (resp) {
                    console.log('Success');
                })
                .catch(function (err) {
                    console.log(err);
                });
            }, 2000);
        });
    
        this.manager.events.addAccessTokenExpired((ev) => {
            this.notifications.next(new AuthServiceEvent('Token Expired'));
            setInterval(() => {
                this.manager
                .signoutRedirect()
                .then(function (resp) {
                    console.log('Success');
                })
                .catch(function (err) {
                    console.log(err);});
            }, 2000);
        });
        
        this.manager.events.addSilentRenewError(() => {
            this.notifications.next(new AuthServiceEvent('User signed out'));
            setInterval(() => {
                this.manager
                .signoutRedirect()
                .then(function (resp) {
                    console.log('Success');
                })
                .catch(function (err) {
                    console.log(err);
                });
            }, 2000);
        });
        
        this.manager.events.addUserSignedOut(() => {
            this.notifications.next(new AuthServiceEvent('User signed out'));
            setInterval(() => {
                this.manager
                .signoutRedirect()
                .then(function (resp) {
                    console.log('Success');
                })
                .catch(function (err) {
                    console.log(err);
                });
            }, 2000);
        });
        
        this.manager.events.addAccessTokenExpired(() => {
            console.log('Token Expired');
        });
        
        this.manager.events.addUserUnloaded(() => {
            console.log('User unloaded');
        });
        
        this.manager.events.removeSilentRenewError(() => {
            console.log('Silent Renew Error');
        });
    }
    
    public get notifications$(): Observable<AuthServiceEvent> {
        return this.notifications.asObservable();
    }
    
    public get userInfo(): User | null {
        return this.user;
    }
    
    public getLoggedinUser() {
        return this.manager.getUser();
    }
    
    public getLoggedInUserId() {
        const key = `oidc.user:${this.configService.resource}:${this.configService.clientId}`;
        if(! sessionStorage[key]) {
            return '';
        }
        const val = JSON.parse(sessionStorage[key]);
        return val.profile.sub;
        }
// check for manager role for logged in user
    async isManager() {
        const user = await this.getLoggedinUser();
        return user?.profile?.roles?.includes("Manager");
    }
    
    isLoggedIn(): boolean {
        const key = `oidc.user:${this.configService.resource}:${this.configService.clientId}`;
        if(! sessionStorage[key]) {
            return false;
        }
        
        const val = JSON.parse(sessionStorage[key])
        return Boolean(val.access_token)
    }
    
    getAuthorizationHeaderValue(): string {
        const key = `oidc.user:${this.configService.resource}:${this.configService.clientId}`;
        
        if(! sessionStorage[key]) {
            return '';
        }
        
        const val = JSON.parse(sessionStorage[key]);
        return `bearer ${val.access_token}`;
    }
    
    startAuthentication(state:string=''): Promise<void> {
        return this.manager.signinRedirect({state});
    }
    
    logout(): Promise<any> {
        return this.manager.signoutRedirect({ state: '1234567890' });
    }
    
    forceLogout(): Promise<any> {
        let settings = this.configService.getClientSettings();
        (settings as any)['post_logout_redirect_uri'] = undefined;
        let manager = new UserManager(settings);
        return manager.signoutRedirect({ state: '1234567890' });
    }
    
    completeAuthentication(): Promise<void> {
        return this.manager.signinRedirectCallback().then((user) => {
            this.user = user;
        });
    }
}
- An 
umrs.service.tsfile for making UMRS calls 
import { Injectable } from '@angular/core';
import { WebRequestService } from './web-request.service';
import { AuthService } from './auth.service';
import { AccessRequest, UmrsResourceGrant } from '../models';
import { Observable, Subject, BehaviorSubject } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
    providedIn: 'root',
})
export class UmrsService {
    constructor(
        private webReqService: WebRequestService,
        private authService: AuthService,
    ) {
        const userId = this.authService.getLoggedInUserId();
        if(userId === undefined) {
            throw new Error('missing userId for logged in user');
        }
    }
}
- A 
demo.service.tsfile for utilizing UMRS calls within your component 
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators'
import { ResourceTreeNode, ResourceWithStatusFlagsTreeNode, UmrsResourceGrant } from '../models'
import { WebRequestService } from './web-request.service';
import { UmrsService} from './umrs.service';
import { AuthService } from 'src/app/Services/auth.service';
import { environment } from '../../environments/environment';
@Injectable({
    providedIn: 'root'
})
export class DemoService {
    constructor(
        private webReqService: WebRequestService,
        private umrsService: UmrsService,
        private authService: AuthService,
    ) {
        this.fetchResources();
        const resourceGrants$ =
        this.umrsService.filteredUserGrants(grnt=>grnt.resourceId.startsWith('resource_'))
        combineLatest([this._resources$, resourceGrants$, this.umrsService.userRequests$]).pipe(map(([resources,grants,accessRequest])=> {
// Combine resources (treeNodes array) with userResourceGrants and userRequests to create tree nodes enhanced with
// pending approval status and grant access status
            const readableStudies = new Set(
                grants.filter(
                    grnt=>grnt.roleName === environment.roles.umrsViewResourceData.name
                    && (!grnt.expiresAt || (new Date(grnt.expiresAt) > new Date()))
                ).map(grnt => grnt.resourceId)
            );
            const pendingStatus = new Set(
                accessRequest.filter(
                    accessRequest=>accessRequest.status === 'pending'
                ).map(accessRequest => accessRequest.resourceId)
            );
            const resourceId = (id:number)=>`resource_${id}`;
            const addFlags = (currNode:ResourceTreeNode):ResourceWithStatusFlagsTreeNode => {
                const isPendingApproval =
// curr node is pending
                (pendingStatus.has(resourceId(currNode.data._id)));
                const isGrantedAccess =
// curr node has grant
                (readableStudies.has(resourceId(currNode.data._id)));
                const nodeWithFlags = {
                    data:{
                        ...currNode.data,
                        isPendingApproval,
                        isGrantedAccess},
                }
                return nodeWithFlags;
            }
            const resourcesWithFlags = resources.map(resource=>addFlags(resource));
            return resourcesWithFlags;
        })).subscribe((resourcesWithGrantFlags)=> {
            this._resourcesWithStatusFlags$.next(resourcesWithGrantFlags);
        })
    }
    private _userResourceGrants$: Subject<UmrsResourceGrant[]> = new BehaviorSubject<UmrsResourceGrant[]>([]);
    private _resourcesWithStatusFlags$: Subject<ResourceWithStatusFlagsTreeNode[]> = new BehaviorSubject<ResourceWithStatusFlagsTreeNode[]>([]);
    private _resources$: Subject<ResourceTreeNode[]> = new BehaviorSubject<ResourceTreeNode[]>([]);
    private fetchResources() {
        this.webReqService.get<ResourceTreeNode[]>('resources/resourceFolders').subscribe(resources => {
            this._resources$.next(resources)
        })
    }
    
    get resourcesWithStatusFlags$() {
        return this._resourcesWithStatusFlags$ as Observable<ResourceWithStatusFlagsTreeNode[]>;
    }
}
Components
- A 
demo.component.tsfile for functionality of your component 
import { Component, OnInit } from '@angular/core';
import { ConfirmationService, MessageService, PrimeNGConfig, TreeNode } from 'primeng/api';
import { DialogService } from 'primeng/dynamicdialog';
import { Resource } from 'src/app/models';
import { DemoService } from 'src/app/Services/demo.service';
import { UmrsService } from 'src/app/Services/umrs.service';
import { UpsertDemoComponent } from '../upsert-demo/upsert-demo.component'; #This component is for viewing access to our demo resources and may be replaced with your desired action upon granting access
import { environment } from 'src/environments/environment';
@Component({
    selector: 'app-demo',
    templateUrl: './demo.component.html',
    styleUrls: ['./demo.component.scss'],
    providers: [DialogService, MessageService, ConfirmationService],
})
export class DemoComponent implements OnInit {
    readonly cols: any[];
    constructor(
        public demoService: DemoService,
        private confirmationService: ConfirmationService,
        private messageService: MessageService,
        private dialogService: DialogService,
        private primengConfig: PrimeNGConfig,
        private umrsService: UmrsService
    ) {
        this.cols = [ # Replace with your own data columns
            { field: 'title', header: 'Title' },
            { field: 'author', header: 'Author' },
            { field: 'text', header: 'Text' },
        ];
    }
    
    ngOnInit(): void {
        this.primengConfig.ripple = true;
    }
}
- A 
demo.component.htmlfile for the template of your component 
<p-toast></p-toast>
<p-messages [value]="(msgs | async) || []"></p-messages>
<div class="card">
<h2>Demo Resource Dashboard</h2>
<p-table #dt [value]="(demoService.resourcesWithGrantFlags$ | async) || []" [rows]="10" [paginator]="true"
[globalFilterFields]="[
'name',
'country.name',
'representative.name',
'status'
]" [rowHover]="true" dataKey="id" currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
[showCurrentPageReport]="true">
<ng-template pTemplate="header">
<tr>
<th pSortableColumn="title">
Title <p-sortIcon field="title"></p-sortIcon>
</th>
<th pSortableColumn="author">
Author <p-sortIcon field="author"></p-sortIcon>
</th>
<th pSortableColumn="text">
Journal<p-sortIcon field="journal"></p-sortIcon>
</th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-resource>
<tr>
<td></td>
<td></td>
<td></td>
<td>
[Insert Buttons here]
</td>
</tr>
</ng-template>
</p-table>
</div>
<p-confirmDialog [style]="{ width: '50vw' }" [baseZIndex]="10000"></p-confirmDialog>
Models
- a 
user.tsfile that models a user 
/* tslint:disable */
/* eslint-disable */
export interface User {
    '_customClaims'?: { };
    '_providerClaims'?: { };
    '_userProfileClaims'?: { };
    amr?: Array<'pwd' | 'mfa' | 'otp' | 'hwk'>;
    commonName?: null | string;
    company?: null | string;
    created?: string;
    department?: null | string;
    displayName?: null | string;
    email?: string;
    emailVerified?: null | boolean;
    externalGroups?: Array<string>;
    familyName?: null | string;
    givenName?: null | string;
    id?: number;
    identityIssuer?: null | string;
    isBlocked?: boolean;
    lastLogin?: string;
    lastStrike?: null | string;
    lockedOut?: null | boolean;
    middleName?: null | string;
    name?: null | string;
    nickname?: null | string;
    passwordHash?: null | string;
    picture?: null | string;
    provider?: string;
    role?: string;
    strikeCount?: null | number;
    totpSecret?: null | string;
    updatedAt?: string;
    upn?: null | string;
    username?: null | string;
}
- an 
umrs-resource-grant.tsfile that models an UMRS grant 
import { UmrsRole } from "./umrs-role";
import { User } from "./user";
export interface UmrsResourceGrant {
    id: number;
    roleName: string;
    resourceId: string;
    expiresAt?: Date;
    grantSource: string;
    resourceServer: string;
    umrsRole: UmrsRole;
    user: User;
}
- a 
treeNode.tsfile that models the data of a particular node 
export interface NodeData {
    _id:number;
}
export interface TreeNode<T extends NodeData> {
    data:T,
}
- a 
resource.tsfile that models the variations of the data with and without access data 
import {NodeData, TreeNode} from './treeNode';
export interface Resource extends NodeData { // replace with your data
    title: string,
    author: string,
    text: string
}
export interface ResourceWithStatusFlags extends Resource{
    isGrantedAccess:boolean;
    isPendingApproval:boolean;
}
export type ResourceTreeNode = TreeNode<Resource>
export type ResourceWithStatusFlagsTreeNode = TreeNode<ResourceWithStatusFlags>
Environment
- An 
environment.tsfile for your environment variables 
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
    production: false,
    clientId: 'umrs-client', # replace with your clientId
    authority: 'https://a-ci.labshare.org/_api/auth/umrs-dev',
    clientBaseUrl: 'https://local.mylocal.org:4203', # replace with your client URL
    serverBaseUrl: 'http://localhost:3000', # replace with your server URL
    resource: 'https://a-ci.labshare.org/_api/auth/umrs-dev',
    // callback url https://local.mylocal.org:4203/auth-callback
    // post logout redirect url https://local.mylocal.org:4203/auth-logout
    // umrs role ids from the UmrsRole table
    roles: {
        umrsViewResourceData: {
            name:'Umrs_View_Resource_Data'
        }
    }
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
Backend
Routes
- A 
demo.tsfile for receiving CRUD calls from the frontend 
import express from "express";
import checkJwt from '../checkJwt';
import jwt_decode from 'jwt-decode';
import { config } from '../default';
import { AccessRequestService} from '../access-request-service';
import { AccessRequest } from "../models/access-request";
import { data as resources } from "../seed/sresource-data.json"; # replace with your data source
import { deleteAllKeys } from "./umrs";
const router = express.Router({ mergeParams: true });
router.get(
    "/resources",
    checkJwt,
    async (req, res) => {
        try {
            console.log(req.headers)
            checkJwt(req,res,(v)=>{
                console.log(v);
            })
        }
        catch(ex) {
            console.log(ex);
        }
        try {
            res.json(resources);
        } catch (err) {
            res.status(500).json({ message: err });
        }
    }
);
export default router;
- An 
umrs.tsfile for receiving CRUD calls from the frontend 
import express from 'express';
import checkJwt from '../checkJwt';
import { UmrsService } from '../umrs-service';
import { AccessRequestService } from '../access-request-service';
import jwt_decode from 'jwt-decode';
const router = express.Router({ mergeParams: true });
const umrsService = new UmrsService();
const NodeCache = require("node-cache");
const nodeCache = new NodeCache({ stdTTL: 100, checkperiod: 120 });
// node-cache event functions for logging & automation
nodeCache.on("set", function (key, value) {
    console.log(`A value for ${key} has been set.`)
});
nodeCache.on("del", function (key, value) {
    console.log(`A value for ${key} has been deleted.`)
});
router.post(
    '/umrs-grants/check', checkJwt, async (req, res) => {
        try {
            const body = req.body;
            const permissions = await umrsService.checkUmrsPermission(body);
            res.status(permissions.status).json({message: permissions.data });
        } catch (err) {
            res.status(500).json({ message: err });
        }
    }
);
router.get(
    '/check/user/:userId/role/:roleName/resource/:resourceId', checkJwt, async (req, res) => {
        try {
            const body = req.body;
            const permissions = await umrsService.checkUmrsPermission(body);
            res.status(permissions.status).json({message: permissions.data });
        } catch (err) {
            res.status(500).json({ message: err });
        }
    }
);
router.get('/requestableAccess/:clientId', checkJwt, async (req, res) => {
    try {
        if (nodeCache.has(`requestableAccess-clientId-${req.params.clientId}`)) {
            res.setHeader('n-cache-status', "HIT");
            res.json(nodeCache.get(`requestableAccess-clientId-${req.params.clientId}`));
        } else {
            const requestableAccesses = await umrsService.getRequestableAccesses(req.params.clientId);
            nodeCache.set(`requestableAccess-clientId-${req.params.clientId}`, requestableAccesses);
            res.setHeader('n-cache-status', "MISS");
            res.json(requestableAccesses);
        }
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.get('/grants/user/:userId', checkJwt, async (req, res) => {
    try {
        if (nodeCache.has(`grants-userId-${req.params.userId}`)) {
            res.setHeader('n-cache-status', "HIT");
            res.json(nodeCache.get(`grants-userId-${req.params.userId}`));
        } else {
            const userGrants = await umrsService.getUserGrants(req.params.userId);
            nodeCache.set(`grants-userId-${req.params.userId}`, userGrants);
            res.setHeader('n-cache-status', "MISS");
            res.json(userGrants);
        }
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.get('/grants', checkJwt, async (req, res) => {
    try {
        if (nodeCache.has(`grants-all`)) {
            res.setHeader('n-cache-status', "HIT");
            res.json(nodeCache.get(`grants-all`));
        } else {
            const grants = await umrsService.getAllGrantsForTenant();
            nodeCache.set(`grants-all`, grants);
            res.setHeader('n-cache-status', "MISS");
            res.json(grants);
        }
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.get('/requests/user/:userId', checkJwt, async (req, res) => {
    try {
        if (nodeCache.has(`requests-userId-${req.params.userId}`)) {
            res.setHeader('n-cache-status', "HIT");
            res.json(nodeCache.get(`requests-userId-${req.params.userId}`));
        } else {
            const requests = await umrsService.getUserAccessRequests(req.params.userId);
            nodeCache.set(`requests-userId-${req.params.userId}`, requests);
            res.setHeader('n-cache-status', "MISS");
            res.json(requests);
        }
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.get('/requests/status/pending', checkJwt, async (req, res) => {
    try {
        if (nodeCache.has('requests-status-pending')) {
            res.setHeader('n-cache-status', "HIT");
            res.json(nodeCache.get('requests-status-pending'));
        } else {
            const requests = await umrsService.getPendingAccessRequests();
            nodeCache.set('requests-status-pending', requests);
            res.setHeader('n-cache-status', "MISS");
            res.json(requests);
        }
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.delete("/revokeUserGrant/:grantId", checkJwt, async (req, res) => {
    try {
        deleteAllKeys();
        const requests = await accessRequestService.rejectAccessRequest(body)
        res.status(requests.status).json({ message: requests.data });
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.get('/findUserInTenant/email/:email', checkJwt, async (req, res) => {
    try {
        const user = await umrsService.findUserInTenant(req.params.email);
        res.status(200).json(user[0]);
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
router.post('/addUserToTenant', checkJwt, async (req, res) => {
    try {
        const newUser = await umrsService.addUserToTenant(req.body);
        res.status(200).json(newUser);
    } catch (err) {
        res.status(500).json({ message: err });
    }
});
export default router;
export function deleteAllKeys() {
const keys = nodeCache.keys();
nodeCache.del(keys);
console.log(nodeCache.getStats());
}
Services
- An 
access-request-service.tsfile for configuring access requests 
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
import debug from 'debug';
import { config } from './default';
import { AccessRequest } from './models/access-request';
export class AccessRequestService {
    userToken:string;
    private authHttp = axios.create();
    private defaultHttp = axios.create();
    
    constructor(token,tenantId){
        this.authHttp.defaults.baseURL = config.servicesAuthUrl;
        this.defaultHttp.defaults.baseURL = config.servicesAuthUrl;
        this.authHttp.interceptors.request.use(
            async (requestConfig: AxiosRequestConfig) => {
                requestConfig.url = requestConfig.url.replace(':tenantId', tenantId);
                requestConfig.headers = {
                    ...requestConfig.headers,
                    Authorization: `Bearer ${token}`
                };
                return requestConfig;
            }
        );
        this.authHttp.interceptors.response.use((d) => d, this.handleError);
    }
    
    handleError(e: AxiosError) {
        debug('access-request-service-error')(e.response?.data);
        throw e;
    }
    
    private tenantUrl = (url)=> `/auth/admin/tenants/:tenantId/${url}`;
    async createInvitation(accessRequest: AccessRequest) {
        const url = '/auth/user/selfService/access-invitation';
        const createdRequest = await this.authHttp.post(url, accessRequest)
        return createdRequest;
    }
    
    async createRequest(accessRequest: AccessRequest) {
        const url = this.tenantUrl('access-requests');
        const createdRequest = await this.authHttp.post(url, accessRequest)
        return createdRequest;
    }
}
Models
- An 
access-request.tsfile for modeling an access request 
export interface AccessRequest {
    approvedAt?: string;
    createdAt?: string;
    expiresAt?: null | string;
    id?: number;
    requestableAccessId: number;
    requestedByUserId?: number;
    requestedForUserId?: null | number;
    requestorNotes?: null | string;
    resourceId?: string;
    status?: 'pending' | 'approved' | 'accepted' | 'rejected';
    tenantId?: number;
    resourceUrl?: string;
    requestDescription?: string;
}
UMRS Developers Guide
Add UMRS Role
UMRS Roles are used to determine whether a user has permissions for a particular resource. An UMRS Grant is created from a combination of an UMRS Role and a resource ID; if the user has the appropriate role for the resource ID, then the user can access the resource.
There are two ways to create UMRS Roles: directly in auth, and through a POST call.
Auth:
-Access Requests
-User Roles
-Add User Role
–name it something recognizeable (“Umrs_View_Resource_Data”)
–add a description (“UMRS role for viewing resource data”)
–choose the resource server the role should be used with (“UMRS Demo”)
POST:
To create a role through a POST call, you will need to send your call to the following route, with tenantId as a required parameter:
https://a-ci.labshare.org/_api/auth/admin/tenants/{tenantId}/umrs-roles
In the body of the call, send the following information, with name as a required parameter:
{
    "id": 0,
    "resourceServerId": 0,
    "tenantId": 0,
    "name": "string",
    "description": "string"
}
Requesting and Sharing Access
Before a user can be given an UMRS Grant for a resource, an access invitation or an access request needs to be submitted for the user. This can be done in two different ways: either a user with the appropriate permissions can invite another user to have access to a resource, or a user can self-request access to a resource.
Submit an Invitation for a Grant for a Resource:
An invitation for a resource grant comes from a user who has permissions to invite other users to access a resource. Our demo app’s implementation of this includes submitting an HTML form that sends an email to the invited user asking them to confirm, and then the user receiving permissions upon accepting the invitation.
demo.component.html
// The "Share Access" button below passes the selected resource to the accessRequest function in the demo.component.ts file.
<button
    pTooltip="Share Access"
    tooltipPosition="bottom"
    pButton
    pRipple
    icon="pi pi-send"
    class="p-button-rounded p-button-success p-mr-2"
    (click)="accessRequest(resource)"
></button>
demo.component.ts
// The accessRequest function takes the selected resource and opens a dialog with the ShareButtonFormComponent nested inside.
accessRequest(resource) {
    const ref = this.dialogService.open(ShareButtonFormComponent, {
        header: `Invite a user to access ${resource.name}`,
        width: 'auto',
        contentStyle: { "max-height": "400px", "overflow": "auto" }
    });
    
    ref.onClose.subscribe(async (invData) => {
        await this.inviteService.inviteResourceAccess(
            invData,
            (`Resource: ${resource.name}`),
        );
    })
}
share-button-form.component.ts
// the share-button-form.component.html has a simple form asking for the user's name, the identity provider they will be using via a dropdown selection, and their email address. Submitting the form creates an AccessRequest and sends the user an invitation to the resource via the demoService upon closing the dialog with a submission.
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DynamicDialogRef } from 'primeng/dynamicdialog';
type AccessRequest = {
    name: string;
    identityIssuer: string;
    email: string;
};
interface idProvider {
    name: string;
    identityIssuer: string;
}
@Component({
    selector: 'app-share-button-form',
    templateUrl: './share-button-form.component.html',
    styleUrls: ['./share-button-form.component.scss'],
})
export class ShareButtonFormComponent implements OnInit {
    idProviders: idProvider[];
    formGroup = new FormGroup({
        name: new FormControl(null, [Validators.required]),
        identityIssuer: new FormControl('https://labshare.local',
        Validators.required,
        ), // local set to default
        email: new FormControl(null, [Validators.required,
        Validators.pattern("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$")]),
    });
    
    constructor(public dialogRef: DynamicDialogRef) {
        this.idProviders = [
            { name: 'NIH', identityIssuer:'https://auth.nih.gov/affwebservices/public/wsfeddispatcher' },
            { name: 'Azure OIDC', identityIssuer:`https://sts.windows.net/{tenantid}/` },
            { name: 'Local', identityIssuer:'https://labshare.local' },
            { name: 'Google', identityIssuer:'https://accounts.google.com' },
        ];
    }
    
    ngOnInit(): void {}
    
    async onSubmit() {
        let accessRequest: AccessRequest = {
            name: this.formGroup.controls['name'].value,
            identityIssuer: this.formGroup.controls['identityIssuer'].value,
            email: this.formGroup.controls['email'].value,
        };
        
        this.dialogRef.close(accessRequest);
    }
    
    get name() {
        return this.formGroup.get('name');
    }
    
    get email() {
        return this.formGroup.get('email');
    }
}
demo.service.ts
// The service creates a new user in the tenant if user is not found, then sends invitation to the user. The webReqService sends a POST call to the server.
async inviteResourceAccess(
    user: User,
    requestDescription: string,
    resourceId: string,
    resourceUrl: string
) {
    const userData: User = {
        name: user.name,
        identityIssuer: user.identityIssuer,
        email: user.email,
    };
    
    const userFound = await this.umrsService.findUserInTenant(userData).toPromise();
    const requestedForUser = userFound || await this.umrsService.addUserToTenant(userData).toPromise();
    let defaultExpirationDate = new Date();
    defaultExpirationDate.setDate(defaultExpirationDate.getDate() + 30);
    this.webReqService
    .post(`demo/accessInvitation`, {
        resourceId: resourceId,
        requestedForUserId: requestedForUser.id,
        requestDescription: requestDescription,
        expiresAt: defaultExpirationDate,
        resourceUrl: `${environment.clientBaseUrl}/demo/${resourceUrl}`,
    })
    .subscribe(() => {
        // this.umrsService.refreshUserGrants();
        this.umrsService.refreshRequests();
    });
}
demo.ts (UMRS server)
//The POST is then routed to the server to create the request.
router.post(
    "/accessInvitation",
    checkJwt,
    
    async (req, res) => {
        const {resourceId,requestedForUserId,requestDescription, expiresAt, resourceUrl} = req.body;
        try {
            const token = req.headers.authorization.substring(7);
            const decodedToken = jwt_decode(token);
            const tenantId = decodedToken['org.labshare.tenant.id'];
            const requestableAccessId = config.requestableAccess.readRequestableAccessId;
            const accessRequest: AccessRequest = {
                tenantId,
                requestableAccessId: parseInt(requestableAccessId),
                requestedForUserId:parseInt(requestedForUserId),
                resourceId,
                requestDescription,
                resourceUrl,
                expiresAt
            }
            deleteAllKeys();
            const service = new AccessRequestService(token, tenantId);
            const request = await service.createInvitation(accessRequest);
            res.status(request.status).json(request.status);
        }
        catch(err) {
            res.status(500).json({ message: err });
        }
    }
);
Submit a Request for a Grant for a Resource:
A request for a resource grant comes directly from the user who wants to access the resource. This request is then sent to an approver or administrator to approve or deny access to the resource.
demo.component.html
// The button passes the resource in the table.
<button
    pTooltip="Request Access"
    tooltipPosition="bottom"
    pButton
    pRipple
    icon="pi pi-lock"
    class="p-button-rounded p-button-info p-mr-2"
    (click)="accessRequest(resource)"
></button>
demo.component.ts
// The accessRequest function sends the data to the demoService to send the request to the server.
accessRequest(resource: any) {
// send access request
    await this.demoService.requestAccess(
        (`Resource: ${resource.title}`);
    )
}
demo.service.ts
// The demoService creates a new access request, and the webReqService sends a POST call to the server.
async requestAccess(requestDescription: string, resourceId: string, resourceUrl: string) {
    const userId = this.authService.getLoggedInUserId();
    let defaultExpirationDate = new Date();
    defaultExpirationDate.setDate(defaultExpirationDate.getDate() + 30);
    return this.webReqService.post(`demo/accessRequest`,{
        resourceId: resourceId,
        requestedForUserId: userId,
        requestDescription: requestDescription,
        expiresAt: defaultExpirationDate,
        resourceUrl: `${environment.clientBaseUrl}/demo/${resourceUrl}`
    }).subscribe(() => {
        this.umrsService.refreshUserGrants();
        this.umrsService.refreshRequests();
    });
}
demo.ts (UMRS server)
//The POST is then routed to the server to create the request.
router.post(
    "/accessRequest",
    checkJwt,
    async (req, res) => {
        const {resourceId,requestedForUserId,requestDescription,resourceUrl, expiresAt} = req.body;
        try {
            const token = req.headers.authorization.substring(7);
            const decodedToken = jwt_decode(token);
            const tenantId = decodedToken['org.labshare.tenant.id'];
            const requestableAccessId = config.requestableAccess.readRequestableAccessId;
            const accessRequest: AccessRequest = {
                tenantId,
                requestableAccessId: parseInt(requestableAccessId),
                requestedForUserId:parseInt(requestedForUserId),
                resourceId,
                requestDescription,
                resourceUrl,
                expiresAt,
            }
            deleteAllKeys();
            const service = new AccessRequestService(token,tenantId);
            await service.createRequest(accessRequest)
            res.status(201).json({message:'Access Request Created'});
        }
        catch(err) {
            res.status(500).json({ message: err });
        }
    }
)
Add Grant to User
An UMRS Grant is created from a combination of an UMRS Role and a resource ID; if the user has the appropriate role for the resource ID, then the user can access the resource.
To create a grant through a POST call, you will need to send your call to the following route, with tenantId as a required parameter:
https://a-ci.labshare.org/_api/auth/admin/tenants/{tenantId}/umrs-grants/user-assignment
In the body of the call, send the following information (the umrsRoleId will be the ID of the UMRS role you created previously, and the resourceID will be the ID of the particular resource you are granting the user access to):
{
    "id": 0,
    "userId": 0,
    "umrsRoleId": 0,
    "expiresAt": "2019-08-24T14:15:22Z",
    "resourceId": "string"
}
Check All Grants for Access
Checking all grants returns all the grants found for a particular user; this is useful for displaying a list of grants that the user has or for visually-identifying unlocked resources.
demo.service.ts
// The constructor of the demo service marks whether each resource is accessible or pending.
demo.component.html
// The template then shows buttons based on whether a resource is pending or granted.
<i
    pTooltip="Pending Access"
    tooltipPosition="bottom"
    *ngIf="rowData.isPendingApproval && !rowData.isGrantedAccess"
    pButton
    pRipple
    icon="pi pi-clock"
    class="p-button-rounded p-button-secondary p-mr-2"
></i>
<button
    pTooltip="Request Access"
    tooltipPosition="bottom"
    *ngIf="!rowData.isPendingApproval && !rowData.isGrantedAccess"
    pButton
    pRipple
    icon="pi pi-lock"
    class="p-button-rounded p-button-info p-mr-2"
    (click)="accessRequest(resource)"
></button>
<button
    *ngIf="rowData.isGrantedAccess"
    pTooltip="View Document"
    tooltipPosition="bottom"
    pButton
    pRipple
    icon="pi pi-book"
    class="p-button-rounded p-button-success p-mr-2"
    (click)="viewResource(resource)"
></button>
umrs.service.ts
// Add to constructor:
this.fetchUserRoleGrants(userId);
// The UMRS service uses the webReqService to fetch grants via a POST request.
private _userGrants$:Subject<UmrsResourceGrant[]> = new BehaviorSubject<UmrsResourceGrant[]>([]);
private fetchUserRoleGrants(userId:string) {
    this.webReqService.get<UmrsResourceGrant[]>(`umrs/grants/user/${userId}`).subscribe(grants=>{
        this._userGrants$.next(grants)
    });
}
get userGrants$() {
    return this._userGrants$ as Observable<UmrsResourceGrant[]>;
}
checkUserGrants(roleName: string, resourceId: number | string) {
    const userId: number = this.authService.getLoggedInUserId();
    const body = {
        "userId": userId,
        "roleName": roleName,
        "resourceIds": [resourceId]
    };
    return this.webReqService.post(`umrs/umrs-grants/check`, body ) as Observable<boolean>;
}
umrs.ts (UMRS server)
// The server then checks the call sent to the route via the POST call.
router.post(
    '/umrs-grants/check', checkJwt, async (req, res) => {
        try {
            const body = req.body;
            const permissions = await umrsService.checkUmrsPermission(body);
            res.status(permissions.status).json({message: permissions.data });
        } catch (err) {
            res.status(500).json({ message: err });
        }
    }
);
Check an Individual Grant for Access
Checking an individual grant returns whether or not a particular user has a grant for a specific resource; this is useful for checking permissions on a single resource at the time of access.
demo.component.ts
// The component sends the same POST call to the server as for checking all grants, but filters to check only the selected resource.
viewResource(resource: Resource) {
    const roleName = environment.roles.umrsViewResourceData.name;
    const resourceId = 'resource_' + resource._id;
    this.umrsService.checkUserGrants(roleName, resourceId).subscribe((permissions) => {
        if (!permissions) {
            // error message
        }
    );
}
const ref = this.dialogService.open(UpsertResourceComponent, {
    data: {
        resource,
        mode: 'view',
    },
    header: resource.title,
    width: '70%',
});
Retrieve all users with a specific grant
Retrieving all users with a specific grant returns users who have been granted permission to a access particular resource.
demo.component.ts
// add variables to class
  resourceGrants$!: Observable<UmrsResourceGrant[]>;
...
// add to component class
    this.resourceGrants$ = this.umrsService.tenantGrants$.pipe(map(grants => {
      return grants.filter(
          grant => grant.resourceId === this.dialogConfig.data.resourceId &&
          grant.umrsRole.name === this.dialogConfig.data.umrsRoleName
      );
    }));
Retrieve all invitations for a user
Retrieving all invitations for a user returns the invitations that have been sent to a particular user.
demo.component.ts
// add to class
const userId = Number(this.authService.getLoggedInUserId());
    this.filterGroup = this.fb.group({
        // returns only invitations sent to the user
      invitationType: InvitationTypeOption.ToMe,
    });
    this.invitations = combineLatest([
        // returns all invitations related to the user
      umrsService.userInvitations$,
      this.invitationType$,
    ]).pipe(
      map(([invitations, invitationType]) => {
        if (invitationType === InvitationTypeOption.ToMe) {
          return invitations.filter(
            (invite) => invite.requestedForUserId === userId
          );
        } else if (invitationType === InvitationTypeOption.ByMe) {
          return invitations.filter(
            (invite) => invite.requestedByUserId === userId
          );
        } else {
          return [];
        }
      })
    );
// add to ngOnInit()
    this.filterGroup.get('invitationType')?.valueChanges.subscribe((x) => {
        // filters invitations based on their type
      this.invitationType$.next(x);
    });    
// add to component body  
  get invitationType() {
    // returns whether an invitation was sent to the user or sent by the user
    return this.filterGroup.get('invitationType')
      ?.value as InvitationTypeOption;
  }
  invitationTypeOptions = [
    { name: 'sent to me', code: InvitationTypeOption.ToMe },
    { name: 'sent by me', code: InvitationTypeOption.ByMe },
  ];
Retrieve all requests for a user
Retrieving all requests and request statuses for a user returns the requests to access resources that a particular user has made.
demo.component.ts
// add variables to class
    statuses!: any[];
    myAccessRequests!: AccessRequest[];
...
// add to ngOnInit()
    this.statuses = [
      {label: 'Pending', value: 'pending'},
      {label: 'Approved', value: 'approved'},
      {label: 'Rejected', value: 'rejected'},
      {label: 'Accepted', value: 'accepted'}]
  
    this.umrsService.userRequests$.subscribe((reqs) => {
      this.myAccessRequests = reqs;
Retrieve all grants
Retrieving all grants returns all grants that have been made.
demo.component.ts
// add variables to component class
  grants$: Observable<UmrsResourceGrant[]>;
  filterGroup: FormGroup;
  resourceType$ = new BehaviorSubject<ResourceTypeOption>(ResourceTypeOption.All);
...
// add to component class
 this.grants$ = combineLatest([
      umrsService.tenantGrants$,
      this.resourceType$,
    ]).pipe(
      map(([grants, resourceType]) => {
        if (resourceType === ResourceTypeOption.All) {
          return grants;
        else {
          return [];
        }
      })
    )
You can also return all grants of a particular type:
demo.component.ts
    this.grants$ = combineLatest([
      umrsService.tenantGrants$,
      this.resourceType$,
    ]).pipe(
      map(([grants, resourceType]) => {
        if (resourceType === ResourceTypeOption.All) {
          return grants;
        } else if (resourceType === ResourceTypeOption.ExampleType1) {
          return grants.filter((grant) => grant.umrsRole?.name === 'Umrs_View_ExampleType1_Data');
        } else if (resourceType === ResourceTypeOption.ExampleType2) {
          return grants.filter((grant) => grant.umrsRole?.name === 'Umrs_View_ExampleType2_Data');
        } else {
          return [];
        }
      })
    )
Retrieve all pending requests
Retrieving all pending requests returns all requests that have not been accepted or rejected yet.
demo.component.ts
// add variables to class
  pendingRequests!: AccessRequest[];
  statuses!: any[];
  ...
// add to ngOnInit()
    this.statuses = [
      { label: 'Pending', value: 'pending' },
      { label: 'Approved', value: 'approved' },
      { label: 'Rejected', value: 'rejected' },
      { label: 'Accepted', value: 'accepted' },
    ];
    this.umrsService.pendingRequests$.subscribe((reqs) => {
        //retrieves all pending requests
      this.pendingRequests = reqs;
    });
Accept a pending request
Accepting a pending request grants the requester access to the resource.
demo.component.ts
// the acceptRequest method in the component opens a dialog box confirming whether you want to accept the request and gives you the option to send an email confirmation to the requester.
async acceptRequest(requestId: number, requestableAccessId: number, userEmail: string) {
    const ref = this.dialogService.open(ApproverMessageComponent, {
      header: `Approve access`,
      width: 'auto',
      contentStyle: { 'max-height': '400px', overflow: 'auto' },
    });
    ref.onClose.subscribe(async (msgData) => {
      if (!msgData) {
        return;
      }else{
        if(msgData.checked){
          console.log(`Send Email confirmation with Approver Note`);
        }else {
          console.log(`Don't Send Email confirmation`);
        }
      }
    this.umrsService
      .acceptAccessRequest(requestId, msgData.message)
      .subscribe(() => {
        this.umrsService.refreshPendingRequests();
        this.umrsService.refreshUserGrants();
        this.umrsService.refreshGrants();
        // create toast when request is accepted
        this.messageService.add({
          severity: 'success',
          summary: 'Accepted',
          detail: `You have accepted this request.`,
        });
      });
    })
  }
  
demo.component.html
// the Accept Access Request button in the template opens the dialog box.
<button pTooltip="Accept Access Request" tooltipPosition="bottom" tooltipStyleClass="tooltip" pButton pRipple
    icon="pi pi-check" class="p-button-rounded p-button-success p-mr-2" (click)="acceptRequest(pendingRequests.id, pendingRequests.requestableAccessId, pendingRequests.requestedForUser.email)"></button>  
Reject a pending request
Rejecting a pending request denies the requester access to the resource.
demo.component.ts
// the rejectRequest method in the component opens a dialog box confirming whether you want to reject the request and gives you the option to send an email confirmation to the requester.
async rejectRequest(requestId: number, requestableAccessId: number, userEmail: string) {
const ref = this.dialogService.open(ApproverMessageComponent, {
    header: `Reject access`,
    width: 'auto',
    contentStyle: { 'max-height': '400px', overflow: 'auto' },
});
ref.onClose.subscribe(async (msgData) => {
    if (!msgData) {
    return;
    }else{
    if(msgData.checked){
        console.log("Send Email confirmation with Approver Note");
    }
    }
    await this.umrsService
    .rejectAccessRequest(requestId, msgData.message)
    .subscribe(() => {
        this.umrsService.refreshPendingRequests();
        this.umrsService.refreshUserGrants();
        this.umrsService.refreshGrants();
        // create toast when request is rejected
        this.messageService.add({
        severity: 'error',
        summary: 'Rejected',
        detail: `You have rejected this request.`,
        });
    });
});
}
demo.component.html
// the Reject Access Request button in the template opens the dialog box.
<button pTooltip="Reject Access Request" tooltipPosition="bottom" tooltipStyleClass="tooltip" pButton pRipple icon="pi pi-times"
    class="p-button-rounded p-button-help" (click)="rejectRequest(pendingRequests.id, pendingRequests.requestableAccessId, pendingRequests.requestedForUser.email)"></button>  
Check a user for manager role
Returns whether or not a particular user has a manager role; this can be used to ensure that the person accepting and rejecting requests has permimssions to do so.
demo.service.ts
  get isApprover$() {
    return this._isApprover$ as Observable<boolean>
  }
demo.component.ts
// add to constructor
    public demoService: DemoService,
    private messageService: MessageService,
    private dialogService: DialogService,
    ...
// add to class
    this.demoService.isApprover$.pipe(filter(x => !x)).subscribe(() => {
      this.messageService.add({
        severity: 'error',
        summary: 'Access Denied',
        detail: 'User lacks permissions.',
      });
    })
    this.demoService.isApprover$.pipe(filter(Boolean)).subscribe(() => {
      const ref = this.dialogService.open(ShareButtonFormComponent, {
        header: `Invite a user to access ${rowData.name}`,
        width: 'auto',
        contentStyle: { "max-height": "400px", "overflow": "auto" }
      });
      ref.onClose.subscribe(async (invData) => {
        if (!invData) {
          // create toast if dialog is closed
          this.messageService.add({
            severity: 'info',
            summary: 'Cancelled',
            detail: `Invitation cancelled.`,
          });
          return;
        }