Skip to content

Commit

Permalink
[web/service] API update to include feature service authentication st…
Browse files Browse the repository at this point in the history
…atus
  • Loading branch information
newmanw committed Nov 13, 2024
1 parent 46cdaf4 commit f7cb430
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 282 deletions.
2 changes: 1 addition & 1 deletion plugins/arcgis/service/src/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface FeatureServiceConfig {
* Serialized ArcGISIdentityManager
*/
identityManager: string

/**
* The feature layers.
*/
Expand Down
4 changes: 2 additions & 2 deletions plugins/arcgis/service/src/ArcGISService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FeatureServiceConfig } from './ArcGISConfig'
import { PluginStateRepository } from '@ngageoint/mage.service/lib/plugins.api'

export interface ArcGISIdentityService {
getIdentityManager(featureService: FeatureServiceConfig): Promise<ArcGISIdentityManager>
signin(featureService: FeatureServiceConfig): Promise<ArcGISIdentityManager>
updateIndentityManagers(): Promise<void>
}

Expand All @@ -13,7 +13,7 @@ export function createArcGISIdentityService(
const identityManagerCache: Map<string, Promise<ArcGISIdentityManager>> = new Map()

return {
async getIdentityManager(featureService: FeatureServiceConfig): Promise<ArcGISIdentityManager> {
async signin(featureService: FeatureServiceConfig): Promise<ArcGISIdentityManager> {
let cached = await identityManagerCache.get(featureService.url)
if (!cached) {
const identityManager = ArcGISIdentityManager.deserialize(featureService.identityManager)
Expand Down
12 changes: 6 additions & 6 deletions plugins/arcgis/service/src/FeatureServiceAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ObservationsTransformer } from "./ObservationsTransformer"
import { LayerInfoResult, LayerField } from "./LayerInfoResult"
import FormData from 'form-data'
import { request } from '@esri/arcgis-rest-request'
import { ArcGISIdentityService, getFeatureServiceUrl } from "./ArcGISService"
import { ArcGISIdentityService } from "./ArcGISService"

/**
* Administers hosted feature services such as layer creation and updates.
Expand Down Expand Up @@ -405,7 +405,7 @@ export class FeatureServiceAdmin {
const form = new FormData()
form.append('addToDefinition', JSON.stringify(layer))

const identityManager = await this._identityService.getIdentityManager(service)
const identityManager = await this._identityService.signin(service)
const postResponse = await request(url, {
authentication: identityManager,
httpMethod: 'POST',
Expand All @@ -427,12 +427,12 @@ export class FeatureServiceAdmin {

this._console.info('ArcGIS feature layer addToDefinition (add fields) url ' + url)

const identityManager = await this._identityService.getIdentityManager(service)
const identityManager = await this._identityService.signin(service)
await request(url, {
authentication: identityManager,
params: {
addToDefinition: layer,
f: "json"
addToDefinition: layer,
f: "json"
}
}).then((postResponse) => {
console.log('Response: ' + postResponse)
Expand Down Expand Up @@ -462,7 +462,7 @@ export class FeatureServiceAdmin {

this._console.info('ArcGIS feature layer deleteFromDefinition (delete fields) url ' + url)

const identityManager = await this._identityService.getIdentityManager(service)
const identityManager = await this._identityService.signin(service)
const postResponse = request(url, {
authentication: identityManager,
httpMethod: 'POST',
Expand Down
6 changes: 3 additions & 3 deletions plugins/arcgis/service/src/ObservationProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export class ObservationProcessor {
private async getFeatureServiceLayers(config: ArcGISPluginConfig) {
for (const service of config.featureServices) {
try {
const identityManager = await this._identityService.getIdentityManager(service)
const identityManager = await this._identityService.signin(service)
const response = await request(service.url, { authentication: identityManager })
this.handleFeatureService(response, service, config)
} catch (err) {
Expand Down Expand Up @@ -261,7 +261,7 @@ export class ObservationProcessor {

if (layerId != null) {
featureLayer.layer = layerId
const identityManager = await this._identityService.getIdentityManager(featureServiceConfig)
const identityManager = await this._identityService.signin(featureServiceConfig)
const featureService = new FeatureService(console, featureServiceConfig, identityManager)
const layerInfo = await featureService.queryLayerInfo(layerId);
const url = `${featureServiceConfig.url}/${layerId}`;
Expand All @@ -285,7 +285,7 @@ export class ObservationProcessor {
const admin = new FeatureServiceAdmin(config, this._identityService, this._console)
await admin.updateLayer(featureServiceConfig, featureLayer, layerInfo, this._eventRepo)
const info = new LayerInfo(url, events, layerInfo)
const identityManager = await this._identityService.getIdentityManager(featureServiceConfig)
const identityManager = await this._identityService.signin(featureServiceConfig)
const layerProcessor = new FeatureLayerProcessor(info, config, identityManager, this._console);
this._layerProcessors.push(layerProcessor);
// clearTimeout(this._nextTimeout); // TODO why is this needed?
Expand Down
27 changes: 19 additions & 8 deletions plugins/arcgis/service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ArcGISIdentityManager, request } from "@esri/arcgis-rest-request"
import { FeatureServiceConfig } from './ArcGISConfig'
import { URL } from "node:url"
import express from 'express'
import { createArcGISIdentityService, getPortalUrl } from './ArcGISService'
import { ArcGISIdentityService, createArcGISIdentityService, getPortalUrl } from './ArcGISService'

const logPrefix = '[mage.arcgis]'
const logMethods = ['log', 'debug', 'info', 'warn', 'error'] as const
Expand All @@ -35,9 +35,15 @@ const InjectedServices = {

const pluginWebRoute = "plugins/@ngageoint/mage.arcgis.service"

const sanitizeFeatureService = (config: FeatureServiceConfig): Omit<FeatureServiceConfig, 'identityManager'> => {
const sanitizeFeatureService = async (config: FeatureServiceConfig, identityService: ArcGISIdentityService): Promise<Omit<FeatureServiceConfig & { authenticated: boolean }, 'identityManager'>> => {
let authenticated = false
try {
await identityService.signin(config)
authenticated = true
} catch(ignore) {}

const { identityManager, ...sanitized } = config;
return sanitized;
return { ...sanitized, authenticated }
}

/**
Expand Down Expand Up @@ -117,7 +123,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
config.featureServices.push(service)

await processor.putConfig(config)
const sanitizedService = sanitizeFeatureService(service)
const sanitizedService = await sanitizeFeatureService(service, identityService)
res.send(`
<html>
<head>
Expand Down Expand Up @@ -148,8 +154,13 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
.get(async (req, res, next) => {
console.info('Getting ArcGIS plugin config...')
const config = await processor.safeGetConfig()
const { featureServices, ...remaining } = config
res.json({ ...remaining, featureServices: featureServices.map((service) => sanitizeFeatureService(service)) })
const { featureServices, ...remaining } = config

const sanitizeFeatureServices = await Promise.all(
featureServices.map(async (service) => await sanitizeFeatureService(service, identityService))
)

res.json({ ...remaining, featureServices: sanitizeFeatureServices })
})
.put(async (req, res, next) => {
console.info('Applying ArcGIS plugin config...')
Expand Down Expand Up @@ -209,7 +220,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
}

await processor.patchConfig(config)
return res.send(sanitizeFeatureService(service))
return res.send(sanitizeFeatureService(service, identityService))
} catch (err) {
return res.send('Invalid credentials provided to communicate with feature service').status(400)
}
Expand All @@ -224,7 +235,7 @@ const arcgisPluginHooks: InitPluginHook<typeof InjectedServices> = {
}

try {
const identityManager = await identityService.getIdentityManager(featureService)
const identityManager = await identityService.signin(featureService)
const response = await request(url, {
authentication: identityManager
})
Expand Down
100 changes: 2 additions & 98 deletions plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,14 @@ export interface FeatureServiceConfig {
url: string

/**
* Username and password for ArcGIS authentication
* Flag indicating valid authentication
*/
auth?: ArcGISAuthConfig

/**
* Create layers that don't exist
*/
createLayers?: boolean

/**
* The administration url to the arc feature service.
*/
adminUrl?: string

/**
* Administration access token
*/
adminToken?: string
authenticated: boolean

/**
* The feature layers.
*/
layers: FeatureLayerConfig[]

}

/**
Expand All @@ -50,92 +34,12 @@ export interface FeatureLayerConfig {
*/
geometryType?: string

/**
* Access token
*/
token?: string // TODO - can this be removed? Will Layers have a token too?
/**
* The event ids or names that sync to this arc feature layer.
*/
events?: (number|string)[]

/**
* Add layer fields from form fields
*/
addFields?: boolean

/**
* Delete editable layer fields missing from form fields
*/
deleteFields?: boolean

}

export enum AuthType {
Token = 'token',
UsernamePassword = 'usernamePassword',
OAuth = 'oauth'
}


/**
* Contains token-based authentication configuration.
*/
export interface TokenAuthConfig {
type: AuthType.Token
token: string
authTokenExpires?: string
}

/**
* Contains username and password for ArcGIS server authentication.
*/
export interface UsernamePasswordAuthConfig {
type: AuthType.UsernamePassword
/**
* The username for authentication.
*/
username: string

/**
* The password for authentication.
*/
password: string
}

/**
* Contains OAuth authentication configuration.
*/
export interface OAuthAuthConfig {
type: AuthType.OAuth
/**
* The Client Id for OAuth
*/
clientId: string
/**
* The redirectUri for OAuth
*/
redirectUri?: string

/**
* The temporary auth token for OAuth
*/
authToken?: string

/**
* The expiration date for the temporary token
*/
authTokenExpires?: string
}

/**
* Union type for authentication configurations.
*/
export type ArcGISAuthConfig =
| TokenAuthConfig
| UsernamePasswordAuthConfig
| OAuthAuthConfig

/**
* Attribute configurations
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,47 @@
<div class="dialog">
<div mat-dialog-title>ArcGIS Feature Service</div>
<div class="cancel">
<button mat-icon-button (click)="onCancel()">
<mat-icon>close</mat-icon>
</button>
</div>

<div mat-dialog-title>ArcGIS Feature Service</div>
<mat-dialog-content>
<div class="dialog-content">
<mat-accordion>
<mat-expansion-panel [expanded]="state === State.Validate" (opened)="state = State.Validate">
<mat-expansion-panel [expanded]="state === State.Validate" (opened)="state = State.Validate" [disabled]="featureService?.authenticated">
<mat-expansion-panel-header>
<mat-panel-title>Feature Service</mat-panel-title>
<mat-panel-title *ngIf="!featureService?.authenticated">Feature Service</mat-panel-title>
<mat-panel-title *ngIf="featureService?.authenticated">{{featureService?.url}}</mat-panel-title>
<mat-panel-description *ngIf="featureService && !featureService?.authenticated">
<div class="invalid-credentials">
<mat-icon>error_outline</mat-icon> Credentials invalid or expired
</div>
</mat-panel-description>
</mat-expansion-panel-header>

<form class="validate" [formGroup]="layerForm" class="form">
<mat-form-field>
<mat-label>URL</mat-label>
<input matInput formControlName="url" required
placeholder="http{s}://{domain}/arcgis/rest/services/{service}/FeatureServer" />
<input matInput formControlName="url" required placeholder="http{s}://{domain}/arcgis/rest/services/{service}/FeatureServer"/>
<mat-error *ngIf="layerForm.hasError('required')">URL is required</mat-error>
</mat-form-field>

<mat-form-field appearance="fill">
<mat-label>Authentication</mat-label>
<mat-select formControlName="authenticationType">
<mat-option *ngFor="let authenticationType of authenticationTypes" [value]="authenticationType.value">
{{authenticationType.title}}
<mat-option *ngFor="let authenticationState of authenticationStates" [value]="authenticationState.value">
{{authenticationState.text}}
</mat-option>
</mat-select>
</mat-form-field>

<ng-container [ngSwitch]="layerForm.controls.authenticationType.value">
<div *ngSwitchCase="AuthenticationType.Token" formGroupName="token">
<mat-form-field appearance="fill">
<mat-label>Token</mat-label>
<mat-label>API Key</mat-label>
<input matInput formControlName="token" required />
<mat-error>Token is required</mat-error>
<mat-error>API Key is required</mat-error>
</mat-form-field>
</div>
<div *ngSwitchCase="AuthenticationType.OAuth" formGroupName="oauth">
Expand Down Expand Up @@ -61,7 +71,8 @@
<button mat-flat-button color="primary" [disabled]="loading" (click)="onValidate()">
<div class="actions__save">
<mat-spinner *ngIf="loading" diameter="16"></mat-spinner>
<span>Validate</span>
<span *ngIf="!featureService">Create Feature Server</span>
<span *ngIf="featureService">Update Feature Server</span>
</div>
</button>
</div>
Expand All @@ -85,7 +96,7 @@
<button mat-flat-button color="primary" [disabled]="loading" (click)="onSave()">
<div class="actions__save">
<mat-spinner *ngIf="loading" diameter="16"></mat-spinner>
<span>Save</span>
<span>Set Layers</span>
</div>
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ mat-dialog-content {
flex: 1
}

.cancel {
position: absolute;
top: -16px;
right: -16px;
}

.dialog {
min-width: 700px;
min-height: 450px;
Expand All @@ -24,6 +30,13 @@ mat-dialog-content {
margin-bottom: 16px;
}

.invalid-credentials {
color: #F44336;
display: flex;
align-items: center;
gap: 8px;
}

.actions {
width: 100%;
display: flex;
Expand Down
Loading

0 comments on commit f7cb430

Please sign in to comment.