diff --git a/plugins/arcgis/service/src/ArcGISConfig.ts b/plugins/arcgis/service/src/ArcGISConfig.ts index af115776a..91ff97475 100644 --- a/plugins/arcgis/service/src/ArcGISConfig.ts +++ b/plugins/arcgis/service/src/ArcGISConfig.ts @@ -8,11 +8,6 @@ export interface FeatureServiceConfig { */ url: string - /** - * Access token - */ - token?: string - /** * Username and password for ArcGIS authentication */ @@ -58,8 +53,7 @@ export interface FeatureLayerConfig { /** * Access token */ - token?: string - + 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. */ @@ -77,26 +71,47 @@ export interface FeatureLayerConfig { } +export enum AuthType { + Token = 'token', + UsernamePassword = 'usernamePassword', + OAuth = 'oauth' + } + + /** - * Contains username and password for ArcGIS server authentication. + * Contains token-based authentication configuration. */ -export interface ArcGISAuthConfig { +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 + username: string /** * The password for authentication. */ - password?: string + password: string +} +/** + * Contains OAuth authentication configuration. + */ +export interface OAuthAuthConfig { + type: AuthType.OAuth /** * The Client Id for OAuth */ - clientId?: string - + clientId: string /** * The redirectUri for OAuth */ @@ -123,6 +138,14 @@ export interface ArcGISAuthConfig { refreshTokenExpires?: string } +/** + * Union type for authentication configurations. + */ +export type ArcGISAuthConfig = + | TokenAuthConfig + | UsernamePasswordAuthConfig + | OAuthAuthConfig + /** * Attribute configurations */ diff --git a/plugins/arcgis/service/src/FeatureServiceAdmin.ts b/plugins/arcgis/service/src/FeatureServiceAdmin.ts index aa497856c..4539aa36a 100644 --- a/plugins/arcgis/service/src/FeatureServiceAdmin.ts +++ b/plugins/arcgis/service/src/FeatureServiceAdmin.ts @@ -523,7 +523,7 @@ export class FeatureServiceAdmin { private httpClient(service: FeatureServiceConfig): HttpClient { let token = service.adminToken if (token == null) { - token = service.token + token = service.auth?.type == 'token' ? service.auth.token : "" } return new HttpClient(console, token) } diff --git a/plugins/arcgis/service/src/ObservationProcessor.ts b/plugins/arcgis/service/src/ObservationProcessor.ts index 9056d8b7a..7bf5ab172 100644 --- a/plugins/arcgis/service/src/ObservationProcessor.ts +++ b/plugins/arcgis/service/src/ObservationProcessor.ts @@ -14,7 +14,7 @@ import { EventTransform } from './EventTransform'; import { GeometryChangedHandler } from './GeometryChangedHandler'; import { EventDeletionHandler } from './EventDeletionHandler'; import { EventLayerProcessorOrganizer } from './EventLayerProcessorOrganizer'; -import { FeatureServiceConfig, FeatureLayerConfig } from "./ArcGISConfig" +import { FeatureServiceConfig, FeatureLayerConfig, AuthType } from "./ArcGISConfig" import { PluginStateRepository } from '@ngageoint/mage.service/lib/plugins.api' import { FeatureServiceAdmin } from './FeatureServiceAdmin'; @@ -173,12 +173,12 @@ export class ObservationProcessor { * @param config The plugins configuration. */ private getFeatureServiceLayers(config: ArcGISPluginConfig) { - + // TODO: What is the impact of what this is doing? Do we need to account for usernamePassword auth type services? for (const service of config.featureServices) { const services: FeatureServiceConfig[] = [] - if (service.token == null) { + if (service.auth?.type !== AuthType.Token || service.auth?.token == null) { const tokenServices = new Map() const nonTokenLayers = [] for (const layer of service.layers) { @@ -204,7 +204,7 @@ export class ObservationProcessor { } for (const serv of services) { - const featureService = new FeatureService(console, serv.token) + const featureService = new FeatureService(console, (serv.auth?.type === AuthType.Token && serv.auth?.token != null) ? serv.auth.token : '') featureService.queryFeatureService(serv.url, (featureServiceResult: FeatureServiceResult) => this.handleFeatureService(featureServiceResult, serv, config)) } } @@ -233,7 +233,7 @@ export class ObservationProcessor { for (const featureLayer of featureServiceConfig.layers) { if (featureLayer.token == null) { - featureLayer.token = featureServiceConfig.token + featureLayer.token = featureServiceConfig.auth?.type == AuthType.Token ? featureServiceConfig.auth.token : "" } const eventNames: string[] = [] diff --git a/plugins/arcgis/service/src/index.ts b/plugins/arcgis/service/src/index.ts index ee54586ae..40e0c70e5 100644 --- a/plugins/arcgis/service/src/index.ts +++ b/plugins/arcgis/service/src/index.ts @@ -6,6 +6,7 @@ import { UserRepositoryToken } from '@ngageoint/mage.service/lib/plugins.api/plu import { SettingPermission } from '@ngageoint/mage.service/lib/entities/authorization/entities.permissions' import express from 'express' import { ArcGISPluginConfig } from './ArcGISPluginConfig' +import { OAuthAuthConfig, AuthType } from './ArcGISConfig' import { ObservationProcessor } from './ObservationProcessor' import { HttpClient } from './HttpClient' import { FeatureServiceResult } from './FeatureServiceResult' @@ -103,9 +104,9 @@ async function handleAuthentication(req: express.Request, httpClient: HttpClient // Check if feature service has refresh token and use that to generate token to use // Else complain const config = await processor.safeGetConfig(); - const featureService = config.featureServices.find((service) => service.auth?.clientId === featureClientId); - const authToken = featureService?.auth?.authToken; - const authTokenExpires = featureService?.auth?.authTokenExpires as string; + const featureService = config.featureServices.find((service) => service.auth?.type === AuthType.OAuth); + const authToken = (featureService?.auth as OAuthAuthConfig)?.authToken; + const authTokenExpires = (featureService?.auth as OAuthAuthConfig)?.authTokenExpires as string; if (authToken && new Date(authTokenExpires) > new Date()) { // TODO: error handling identityManager = await ArcGISIdentityManager.fromToken({ @@ -114,8 +115,8 @@ async function handleAuthentication(req: express.Request, httpClient: HttpClient portal: portalUrl }); } else { - const refreshToken = featureService?.auth?.refreshToken; - const refreshTokenExpires = featureService?.auth?.refreshTokenExpires as string; + const refreshToken = (featureService?.auth as OAuthAuthConfig)?.refreshToken; + const refreshTokenExpires = (featureService?.auth as OAuthAuthConfig)?.refreshTokenExpires as string; if (refreshToken && new Date(refreshTokenExpires) > new Date()) { const url = `${portalUrl}/oauth2/token?client_id=${featureClientId}&refresh_token=${refreshToken}&grant_type=refresh_token` const response = await httpClient.sendGet(url) @@ -131,7 +132,7 @@ async function handleAuthentication(req: express.Request, httpClient: HttpClient } } } else { - throw new Error('Missing required query parameters to authenticate (token or username/password).'); + throw new Error('Missing required query parameters to authenticate (token or username/password or oauth parameters).'); } console.log('Identity Manager token', identityManager.token); @@ -181,6 +182,7 @@ const arcgisPluginHooks: InitPluginHook = { url: portal, layers: [], auth: { + type: AuthType.OAuth, clientId: clientId, redirectUri: redirectUri } @@ -199,8 +201,8 @@ const arcgisPluginHooks: InitPluginHook = { const config = await processor.safeGetConfig(); const featureService = config.featureServices[0]; const creds = { - clientId: featureService.auth?.clientId as string, - redirectUri: featureService.auth?.redirectUri as string, + clientId: (featureService.auth as OAuthAuthConfig)?.clientId as string, + redirectUri: (featureService.auth as OAuthAuthConfig)?.redirectUri as string, portal: featureService.url as string } ArcGISIdentityManager.exchangeAuthorizationCode(creds, code) @@ -210,7 +212,9 @@ const arcgisPluginHooks: InitPluginHook = { authToken: idManager.token, authTokenExpires: idManager.tokenExpires.toISOString(), refreshToken: idManager.refreshToken, - refreshTokenExpires: idManager.refreshTokenExpires.toISOString() + refreshTokenExpires: idManager.refreshTokenExpires.toISOString(), + type: AuthType.OAuth, + clientId: creds.clientId } await processor.putConfig(config); res.status(200).json({}) diff --git a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts index 15194f081..91ff97475 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/ArcGISConfig.ts @@ -8,11 +8,6 @@ export interface FeatureServiceConfig { */ url: string - /** - * Access token - */ - token?: string // TODO?: Perhaps move to the auth property? - /** * Username and password for ArcGIS authentication */ @@ -58,8 +53,7 @@ export interface FeatureLayerConfig { /** * Access token */ - token?: string - + 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. */ @@ -77,34 +71,81 @@ export interface FeatureLayerConfig { } +export enum AuthType { + Token = 'token', + UsernamePassword = 'usernamePassword', + OAuth = 'oauth' + } + + /** - * Contains username and password for ArcGIS server authentication. + * Contains token-based authentication configuration. */ -export interface ArcGISAuthConfig { - - // TODO?: May want to add authType property +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 + username: string /** * The password for authentication. */ - password?: string + password: string +} +/** + * Contains OAuth authentication configuration. + */ +export interface OAuthAuthConfig { + type: AuthType.OAuth /** * The Client Id for OAuth */ - clientId?: string + 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 + + /** + * The Refresh token for OAuth + */ + refreshToken?: string /** - * The Client secret for OAuth + * The expiration date for the Refresh token */ - clientSecret?: string + refreshTokenExpires?: string } +/** + * Union type for authentication configurations. + */ +export type ArcGISAuthConfig = + | TokenAuthConfig + | UsernamePasswordAuthConfig + | OAuthAuthConfig + /** * Attribute configurations */ diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html index c937d7e3e..068a060b9 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.html @@ -74,7 +74,7 @@

Layers

+ (click)="onAddLayerUrl({ layerUrl: layerUrl.value, selectableLayers: layers, authType: AuthType.Token, layerToken: layerToken.value })">SAVE diff --git a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts index f523458f9..b96b4051f 100644 --- a/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts +++ b/plugins/arcgis/web-app/projects/main/src/lib/arc-layer/arc-layer.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild import { FormControl, Validators } from '@angular/forms' import { ArcGISPluginConfig, defaultArcGISPluginConfig } from '../ArcGISPluginConfig' import { ArcService } from '../arc.service' -import { FeatureLayerConfig, FeatureServiceConfig } from '../ArcGISConfig'; +import { AuthType, OAuthAuthConfig, TokenAuthConfig, UsernamePasswordAuthConfig, FeatureServiceConfig } from '../ArcGISConfig'; import { MatDialog } from '@angular/material/dialog' import { ArcLayerSelectable } from './ArcLayerSelectable'; import { EventResult } from '../EventsResult'; @@ -50,17 +50,25 @@ export class ArcLayerComponent implements OnInit { } onEditLayer(arcService: FeatureServiceConfig) { - console.log('Editing layer ' + arcService.url + ', token: ' + arcService.token) - if (arcService.token) { - this.currentUrl = this.addToken(arcService.url, arcService.token); - } else if (arcService.auth?.username && arcService.auth?.password) { + console.log('Editing layer ' + arcService.url) + if (arcService.auth?.type === AuthType.Token && arcService.auth?.token) { + this.currentUrl = this.addToken(arcService.url, arcService.auth.token); + } else if (arcService.auth?.type === AuthType.UsernamePassword && arcService.auth?.username && arcService.auth?.password) { this.currentUrl = this.addCredentials(arcService.url, arcService.auth?.username, arcService.auth?.password); + } else if (arcService.auth?.type === AuthType.OAuth && arcService.auth?.clientId) { + // TODO: what needs to be sent in url for this to work? + this.currentUrl = this.addOAuth(arcService.url, arcService.auth?.clientId); } else { throw new Error('Invalid layer config, auth credentials: ' + JSON.stringify(arcService)) } this.arcLayerControl.setValue(arcService.url) - this.arcTokenControl.setValue(arcService.token != null ? arcService.token : '') + // Safely set the token control value based on the type + if (arcService.auth?.type === AuthType.Token) { + this.arcTokenControl.setValue(arcService.auth.token); + } else { + this.arcTokenControl.setValue(''); + } this.layers = [] let selectedLayers = new Array() for (const layer of arcService.layers) { @@ -166,25 +174,24 @@ export class ArcLayerComponent implements OnInit { } /** - * Adds a new layer to the configuration if it does not already exist. + * Adds or edits a layer configuration based on the provided parameters. * - * @param layerUrl - The URL of the layer to be added. - * @param arg2 - Either the username for authentication or the token for the layer. - * @param arg3 - Either the password for authentication or an array of selectable layers. - * @param arg4 - Optional array of selectable layers if `arg3` is a string (username). + * @param params - The parameters for adding or editing a layer. + * @param params.layerUrl - The URL of the layer to add or edit. + * @param params.selectableLayers - An array of selectable layers. + * @param params.authType - The type of authentication to use. + * @param params.layerToken - Optional. The token for token-based authentication. + * @param params.username - Optional. The username for username/password authentication. + * @param params.password - Optional. The password for username/password authentication. + * @param params.clientId - Optional. The client ID for OAuth authentication. + * @param params.clientSecret - Optional. The client secret for OAuth authentication. * - * This method performs the following steps: - * 1. Checks if the layer already exists in the configuration. - * 2. If the layer does not exist, it logs the addition of the layer. - * 3. Authenticates and retrieves a token if `arg3` is a string (password). - * 4. Creates a new feature layer configuration. - * 5. Adds selected layers to the feature layer configuration. - * 6. Updates the configuration and emits the change. - * 7. Persists the updated configuration using `arcService`. + * @throws Will throw an error if the provided authentication type is invalid. */ onAddLayerUrl(params: { layerUrl: string, selectableLayers: ArcLayerSelectable[], + authType: AuthType, layerToken?: string, username?: string, password?: string, @@ -192,7 +199,7 @@ export class ArcLayerComponent implements OnInit { clientSecret?: string }): void { let serviceConfigToEdit = null; - const { layerUrl, selectableLayers, layerToken, username, password, clientId, clientSecret } = params; + const { layerUrl, selectableLayers, authType, layerToken, username, password, clientId, clientSecret } = params; // Search if the layer in config to edit for (const service of this.config.featureServices) { @@ -205,23 +212,25 @@ export class ArcLayerComponent implements OnInit { if (serviceConfigToEdit == null) { console.log('Adding layer ' + layerUrl); + const authConfigMap = { + [AuthType.Token]: { type: AuthType.Token, token: layerToken } as TokenAuthConfig, + [AuthType.UsernamePassword]: { type: AuthType.UsernamePassword, username, password } as UsernamePasswordAuthConfig, + [AuthType.OAuth]: { type: AuthType.OAuth, clientId, clientSecret } as OAuthAuthConfig + }; + + const authConfig = authConfigMap[authType]; + if (!authConfig) { + throw new Error('Invalid authorization type: ' + authType); + } + + // Creates a new feature layer configuration. const featureLayer: FeatureServiceConfig = { url: layerUrl, - token: undefined, - auth: {}, + auth: authConfig, layers: [] } as FeatureServiceConfig; - if (username) { - // Handle username and password case - featureLayer.auth = { username, password }; - } else if (clientId) { - featureLayer.auth = { clientId, clientSecret }; - }else { - // Handle token case - featureLayer.token = layerToken; - } - + // Adds selected layers to the feature layer configuration. if (selectableLayers) { for (const aLayer of selectableLayers) { if (aLayer.isSelected) { @@ -233,9 +242,10 @@ export class ArcLayerComponent implements OnInit { } } - // Add the new featureLayer to the config + // Add the new featureLayer to the config and emits the change. this.config.featureServices.push(featureLayer); this.configChanged.emit(this.config); + // Persists the updated configuration using `arcService`. this.arcService.putArcConfig(this.config); } @@ -266,27 +276,35 @@ export class ArcLayerComponent implements OnInit { } serviceConfigToEdit.layers = editedLayers } - + // Emit and persist the updated configuration. this.configChanged.emit(this.config); this.arcService.putArcConfig(this.config); } + // Provide html access to auth types + public AuthType = AuthType; + // Helper method to add token to the URL + // append to layerURL query parameter, outer url already contains ? separator addToken(url: string, token?: string) { let newUrl = url if (token != null && token.length > 0) { - // TODO - appending to query param featureURL because there is an outer url using ? separater already - // const separator = url.includes('?') ? '&' : '?'; - // newUrl += separator + 'token=' + token - newUrl += '&' + 'token=' + token + newUrl += '&' + 'token=' + encodeURIComponent(token) } return newUrl } // Helper method to add credentials to the URL + // append to layerURL query parameter, outer url already contains ? separator private addCredentials(layerUrl: string, username: string, password: string): string { const encodedUsername = encodeURIComponent(username); const encodedPassword = encodeURIComponent(password); - // append to layerURL query parameter, outer url already contains ? separator return `${layerUrl}&username=${encodedUsername}&password=${encodedPassword}`; } + + // Helper method to add OAuth credentials to the URL + // append to layerURL query parameter, outer url already contains ? separator + private addOAuth(layerUrl: string, clientId: string): string { + const encodedClientId = encodeURIComponent(clientId); + return `${layerUrl}&client_id=${encodedClientId}`; + } } \ No newline at end of file