From 6b15f56d27b72f86ae0c93be04302567403e4245 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 19 Jan 2024 01:20:22 +0100 Subject: [PATCH] Authentication with Google Account (experimental) --- .eslintrc.yml | 2 - README.md | 39 +++++++++- config.json | 6 +- src/api/users.js | 33 ++++++-- src/models/catalog.js | 3 +- src/models/userstore.js | 43 ++++++++++- src/processes/aggregate_temporal_frequency.js | 3 +- src/processes/anomaly.js | 4 +- src/processes/climatological_normal.js | 6 +- src/processes/create_raster_cube.js | 2 +- src/processes/filter_bbox.js | 2 +- src/processes/filter_spatial.js | 2 +- src/processes/first.js | 1 + src/processes/if.js | 2 +- src/processes/last.js | 1 + src/processes/load_collection.js | 9 ++- src/processes/log.js | 2 +- src/processgraph/commons.js | 29 +++---- src/processgraph/context.js | 32 +++++++- src/processgraph/datacube.js | 33 ++++---- src/processgraph/node.js | 6 +- src/processgraph/processgraph.js | 5 ++ src/processgraph/utils.js | 76 +++++++++++++++++++ src/server.js | 15 +--- src/utils/config.js | 3 +- src/utils/servercontext.js | 9 ++- src/utils/utils.js | 67 ---------------- 27 files changed, 292 insertions(+), 143 deletions(-) create mode 100644 src/processgraph/utils.js diff --git a/.eslintrc.yml b/.eslintrc.yml index e253dd0..2fc10b7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,8 +6,6 @@ extends: parserOptions: ecmaVersion: 2022 sourceType: module -globals: - ee: readonly rules: n/no-extraneous-import: - error diff --git a/README.md b/README.md index 1ef43ad..8a4bd2a 100644 --- a/README.md +++ b/README.md @@ -33,9 +33,46 @@ There are several important configuration options in the file [config.json](conf #### Setting up GEE authentication +Generally, information about authentication with Google Earth Engine can be found in the [Earth Engine documentation](https://developers.google.com/earth-engine/app_engine_intro). + +##### Service Account + +If you want to run all processing through a single account you can use service accounts. That's the most reliable way right now. The server needs to authenticate with a [service accounts](https://developers.google.com/earth-engine/service_account) using a private key. The account need to have access rights for earth engine. You need to drop your private key file into a secure place specify the file path of the private key in the property `serviceAccountCredentialsFile` in the file [config.json](config.json). -More information about authentication can be found in the [Earth Engine documentation](https://developers.google.com/earth-engine/app_engine_intro). +##### Google User Accounts + +**EXPERIMENTAL:** *This authentication method currently requires you to login every 60 minutes unless the +openEO clients refresh the tokens automatically. User workspaces also don't work reliably as of now.* + +Alternatively, you can configure the driver to let users authenticatie with their User Accounts via OAuth2 / OpenID Connect. +For this you need to configure the property `googleAuthClients` in the file [config.json](config.json). + +You want to have at least client IDs for (1) "Web Application" and (2) "TVs & limited-input devices" from the +[Google Cloud Console](https://console.cloud.google.com/apis/credentials). + +For example: + +```json +[ + { + "id": "1234567890-abcdefghijklmnop.apps.googleusercontent.com", + "grant_types": [ + "implicit" + ], + "redirect_urls": [ + "https://editor.openeo.org/", + "http://localhost/" + ] + }, + { + "id": "0123456789-abcdefghijklmnop.apps.googleusercontent.com", + "grant_types": [ + "urn:ietf:params:oauth:grant-type:device_code+pkce" + ] + } +] +``` ### Starting up the server diff --git a/config.json b/config.json index 9d14d30..ad9f596 100644 --- a/config.json +++ b/config.json @@ -10,8 +10,7 @@ "key": null, "certificate": null }, - "serviceAccountCredentialsFile": "privatekey.json", - "googleProjectId": "", + "serviceAccountCredentialsFile": "", "id": "openeo-earthengine-driver", "title": "Google Earth Engine Proxy for openEO", "description": "This is the Google Earth Engine Driver for openEO.\n\nGoogle Earth Engine is a planetary-scale platform for Earth science data & analysis. It is powered by Google's cloud infrastructure and combines a multi-petabyte catalog of satellite imagery and geospatial datasets with planetary-scale analysis capabilities. Google makes it available for scientists, researchers, and developers to detect changes, map trends, and quantify differences on the Earth's surface. Google Earth Engine is free for research, education, and nonprofit use.", @@ -26,5 +25,6 @@ } ] }, - "otherVersions": [] + "otherVersions": [], + "googleAuthClients": [] } diff --git a/src/api/users.js b/src/api/users.js index 2888492..ddfd1ac 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -10,7 +10,9 @@ export default class UsersAPI { beforeServerStart(server) { server.addEndpoint('get', '/credentials/basic', this.getCredentialsBasic.bind(this)); -// server.addEndpoint('get', '/credentials/oidc', this.getCredentialsOidc.bind(this)); + if (this.context.googleAuthClients) { + server.addEndpoint('get', '/credentials/oidc', this.getCredentialsOidc.bind(this)); + } server.addEndpoint('get', '/me', this.getUserInfo.bind(this)); return Promise.resolve(); @@ -28,13 +30,34 @@ export default class UsersAPI { try { req.user = await this.storage.checkAuthToken(token); } catch(err) { - res.send(Error.wrap(err)); + res.send(Errors.wrap(err)); } } -// getCredentialsOidc(req, res, next) { -// res.redirect('https://accounts.google.com/.well-known/openid-configuration', next); -// } + async getCredentialsOidc(req, res) { + if (!this.context.googleAuthClients) { + throw new Errors.FeatureUnsupported(); + } + + res.send({ + "providers": [ + { + id: "google", + issuer: "https://accounts.google.com", + title: "Google", + description: "Login with your Google Earth Engine account.", + scopes: [ + "openid", + "email", + "https://www.googleapis.com/auth/earthengine", + // "https://www.googleapis.com/auth/cloud-platform", + // "https://www.googleapis.com/auth/devstorage.full_control" + ], + default_clients: this.context.googleAuthClients + } + ] + }); + } async getCredentialsBasic(req, res) { if (!req.authorization.scheme) { diff --git a/src/models/catalog.js b/src/models/catalog.js index e57e384..2632adb 100644 --- a/src/models/catalog.js +++ b/src/models/catalog.js @@ -68,8 +68,7 @@ export default class DataCatalog { } const storage = new Storage({ - keyFile: './privatekey.json', - projectId: this.serverContext.googleProjectId + keyFile: this.serverContext.serviceAccountCredentialsFile || null }); const bucket = storage.bucket('earthengine-stac'); const prefix = 'catalog/'; diff --git a/src/models/userstore.js b/src/models/userstore.js index dfa861e..c2a3b81 100644 --- a/src/models/userstore.js +++ b/src/models/userstore.js @@ -101,9 +101,9 @@ export default class UserStore { return await this.db.insertAsync(userData); } - async checkAuthToken(token) { + async authenticateBasic(token) { const query = { - token: token.replace(/^basic\/\//, ''), // remove token prefix for basic + token, validity: { $gt: Utils.getTimestamp() } }; @@ -120,8 +120,45 @@ export default class UserStore { reason: 'User account has been removed.' }); } - return user; } + async authenticateGoogle(token) { + const userData = this.emptyUser(false); + userData._id = "google-" + Utils.generateHash(8); + userData.token = token; + // Googles tokens are valid for roughly an hour, so we set it slightly lower + userData.token_valid_until = Utils.getTimestamp() + 59 * 60; + return userData; + } + + async checkAuthToken(apiToken) { + const parts = apiToken.split('/', 3); + if (parts.length !== 3) { + throw new Errors.AuthenticationRequired({ + reason: 'Token format invalid.' + }); + } + const [type, provider, token] = parts; + + if (type === 'basic') { + return this.authenticateBasic(token); + } + else if (type === 'oidc') { + if (provider === 'google') { + return this.authenticateGoogle(token); + } + else { + throw new Errors.AuthenticationRequired({ + reason: 'Identity provider not supported.' + }); + } + } + else { + throw new Errors.AuthenticationRequired({ + reason: 'Authentication method not supported.' + }); + } + } + } diff --git a/src/processes/aggregate_temporal_frequency.js b/src/processes/aggregate_temporal_frequency.js index 776a31e..4cc6246 100644 --- a/src/processes/aggregate_temporal_frequency.js +++ b/src/processes/aggregate_temporal_frequency.js @@ -41,12 +41,13 @@ export default class aggregate_temporal_frequency extends BaseProcess { } async execute(node) { + const ee = node.ee; // STEP 1: Get parameters and set some variables const dc = node.getDataCube('data'); const frequency = node.getArgument('frequency'); // STEP 2: prepare image collection with aggregation label - const images = Commons.setAggregationLabels(dc.imageCollection(), frequency); + const images = Commons.setAggregationLabels(node, dc.imageCollection(), frequency); // STEP 3: aggregate based on aggregation label diff --git a/src/processes/anomaly.js b/src/processes/anomaly.js index e797b37..4acfc87 100644 --- a/src/processes/anomaly.js +++ b/src/processes/anomaly.js @@ -6,12 +6,12 @@ export default class anomaly extends BaseProcess { async execute(node) { const dc = node.getDataCube('data'); const normalsDataCube = node.getDataCube('normals'); - const normalsLabels = ee.List(normalsDataCube.dimT().getValues()); + const normalsLabels = node.ee.List(normalsDataCube.dimT().getValues()); const normalsCollection = normalsDataCube.imageCollection(); const normals = normalsCollection.toList(normalsCollection.size()); const frequency = node.getArgument('frequency'); - let images = Commons.setAggregationLabels(dc.imageCollection(), frequency); + let images = Commons.setAggregationLabels(node, dc.imageCollection(), frequency); images = images.map(image => { const label = image.get('label'); const normal = normals.get(normalsLabels.indexOf(label)); diff --git a/src/processes/climatological_normal.js b/src/processes/climatological_normal.js index e6c3686..e7a357a 100644 --- a/src/processes/climatological_normal.js +++ b/src/processes/climatological_normal.js @@ -1,10 +1,12 @@ import { BaseProcess } from '@openeo/js-processgraphs'; import Commons from '../processgraph/commons.js'; import Utils from '../utils/utils.js'; +import GeeUtils from '../processgraph/utils.js'; export default class climatological_normal extends BaseProcess { async execute(node) { + const ee = node.ee; const dc = node.getDataCube('data'); const frequency = node.getArgument('frequency'); @@ -25,7 +27,7 @@ export default class climatological_normal extends BaseProcess { break; case 'seasons': // Define seasons + labels - seasons = Utils.seasons(); + seasons = GeeUtils.seasons(node); geeSeasons = ee.Dictionary(seasons); labels = Object.keys(seasons); range = geeSeasons.values(); @@ -41,7 +43,7 @@ export default class climatological_normal extends BaseProcess { break; case 'tropical_seasons': // Define seasons + labels - seasons = Utils.tropicalSeasons(); + seasons = GeeUtils.tropicalSeasons(node); geeSeasons = ee.Dictionary(seasons); labels = Object.keys(seasons); range = geeSeasons.values(); diff --git a/src/processes/create_raster_cube.js b/src/processes/create_raster_cube.js index a3df72d..6236b71 100644 --- a/src/processes/create_raster_cube.js +++ b/src/processes/create_raster_cube.js @@ -4,7 +4,7 @@ import DataCube from '../processgraph/datacube.js'; export default class create_raster_cube extends BaseProcess { async execute(node) { - const dc = new DataCube(); + const dc = new DataCube(node.ee); dc.setLogger(node.getLogger()); return dc; } diff --git a/src/processes/filter_bbox.js b/src/processes/filter_bbox.js index bb627e6..dcd7249 100644 --- a/src/processes/filter_bbox.js +++ b/src/processes/filter_bbox.js @@ -4,7 +4,7 @@ import Commons from '../processgraph/commons.js'; export default class filter_bbox extends BaseProcess { async execute(node) { - return Commons.filterBbox(node.getDataCube("data"), node.getArgument("extent"), this.id, 'extent'); + return Commons.filterBbox(node, node.getDataCube("data"), node.getArgument("extent"), this.id, 'extent'); } } diff --git a/src/processes/filter_spatial.js b/src/processes/filter_spatial.js index 86f73b4..7c305f3 100644 --- a/src/processes/filter_spatial.js +++ b/src/processes/filter_spatial.js @@ -4,7 +4,7 @@ import Commons from '../processgraph/commons.js'; export default class filter_spatial extends BaseProcess { async execute(node) { - return Commons.filterGeoJSON(node.getData("data"), node.getArgument("geometries"), this.id, 'geometries'); + return Commons.filterGeoJSON(node, node.getData("data"), node.getArgument("geometries"), this.id, 'geometries'); } } diff --git a/src/processes/first.js b/src/processes/first.js index 3965ae7..dc9176b 100644 --- a/src/processes/first.js +++ b/src/processes/first.js @@ -8,6 +8,7 @@ export default class first extends BaseProcess { } async execute(node) { + const ee = node.ee; const data = node.getArgument('data'); if (Array.isArray(data)) { diff --git a/src/processes/if.js b/src/processes/if.js index f7185c7..bf08321 100644 --- a/src/processes/if.js +++ b/src/processes/if.js @@ -7,7 +7,7 @@ export default class If extends BaseProcess { const accept = node.getArgument('accept'); const reject = node.getArgument('reject'); - return ee.Algorithms.If(value, accept, reject); + return node.ee.Algorithms.If(value, accept, reject); //if (value === true) { // return accept; //} diff --git a/src/processes/last.js b/src/processes/last.js index ded2c23..4a0fd22 100644 --- a/src/processes/last.js +++ b/src/processes/last.js @@ -8,6 +8,7 @@ export default class last extends BaseProcess { } async execute(node) { + const ee = node.ee; const data = node.getArgument('data'); if (Array.isArray(data)) { diff --git a/src/processes/load_collection.js b/src/processes/load_collection.js index ec9b644..5ab9bd1 100644 --- a/src/processes/load_collection.js +++ b/src/processes/load_collection.js @@ -5,14 +5,15 @@ import Commons from '../processgraph/commons.js'; export default class load_collection extends BaseProcess { async execute(node) { + const ee = node.ee; // Load data const id = node.getArgument('id'); const collection = node.getContext().getCollection(id); - let dc = new DataCube(); + let dc = new DataCube(ee); dc.setLogger(node.getLogger()); let eeData; if (collection['gee:type'] === 'image') { - eeData = ee.ImageCollection(ee.Image(id)); + eeData = ee.Image(id); } else { eeData = ee.ImageCollection(id); @@ -32,10 +33,10 @@ export default class load_collection extends BaseProcess { const spatial_extent = node.getArgument("spatial_extent"); if (spatial_extent !== null) { if (spatial_extent.type) { // GeoJSON - has been validated before so `type` should be a safe indicator for GeoJSON - dc = Commons.filterGeoJSON(dc, spatial_extent, this.id, 'spatial_extent'); + dc = Commons.filterGeoJSON(node, dc, spatial_extent, this.id, 'spatial_extent'); } else { // Bounding box - dc = Commons.filterBbox(dc, spatial_extent, this.id, 'spatial_extent'); + dc = Commons.filterBbox(node, dc, spatial_extent, this.id, 'spatial_extent'); } } diff --git a/src/processes/log.js b/src/processes/log.js index aff0d44..44f3a8f 100644 --- a/src/processes/log.js +++ b/src/processes/log.js @@ -12,7 +12,7 @@ export default class log extends BaseProcess { case 10: return image.log10(); default: - return image.log().divide(ee.Image(base).log()); + return image.log().divide(node.ee.Image(base).log()); } }, x => { diff --git a/src/processgraph/commons.js b/src/processgraph/commons.js index 68a3565..a08a3b1 100644 --- a/src/processgraph/commons.js +++ b/src/processgraph/commons.js @@ -2,6 +2,7 @@ import Errors from '../utils/errors.js'; import Utils from '../utils/utils.js'; import DataCube from './datacube.js'; import ProcessGraph from './processgraph.js'; +import GeeUtils from '../processgraph/utils.js'; export default class Commons { @@ -149,12 +150,13 @@ export default class Commons { } static _reduceBinary(node, eeImgReducer, jsReducer, valA, valB, dataArg = "data") { + const ee = node.ee; let result; - const dataCubeA = new DataCube(null, valA); + const dataCubeA = new DataCube(node.ee, null, valA); dataCubeA.setLogger(node.getLogger()); - const dataCubeB = new DataCube(null, valB); + const dataCubeB = new DataCube(node.ee, null, valB); dataCubeA.setLogger(node.getLogger()); const imgReducer = (a,b) => eeImgReducer(a,b).copyProperties({source: a, properties: a.propertyNames()}); @@ -242,7 +244,7 @@ export default class Commons { static applyInCallback(node, eeImgProcess, jsProcess = null, dataArg = "x") { const data = node.getArgument(dataArg); - const dc = new DataCube(null, data); + const dc = new DataCube(node.ee, null, data); dc.setLogger(node.getLogger()); const imgProcess = a => eeImgProcess(a).copyProperties({source: a, properties: a.propertyNames()}); if (dc.isNull()) { @@ -262,17 +264,17 @@ export default class Commons { } } - static restrictToSpatialExtent(dc) { + static restrictToSpatialExtent(node, dc) { const bbox = dc.getSpatialExtent(); - const geom = ee.Geometry.Rectangle([bbox.west, bbox.south, bbox.east, bbox.north], Utils.crsToString(bbox.crs, 4326)); + const geom = node.ee.Geometry.Rectangle([bbox.west, bbox.south, bbox.east, bbox.north], Utils.crsToString(bbox.crs, 4326)); dc.imageCollection(ic => ic.filterBounds(geom)); return dc; } - static filterBbox(dc, bbox, process_id, paramName) { + static filterBbox(node, dc, bbox, process_id, paramName) { try { dc.setSpatialExtent(bbox); - return Commons.restrictToSpatialExtent(dc); + return Commons.restrictToSpatialExtent(node, dc); } catch (e) { throw new Errors.ProcessArgumentInvalid({ process: process_id, @@ -305,11 +307,11 @@ export default class Commons { return dc; } - static filterGeoJSON(dc, geometries, process_id, paramName) { + static filterGeoJSON(node, dc, geometries, process_id, paramName) { try { - const geom = Utils.geoJsonToGeometry(geometries); + const geom = GeeUtils.geoJsonToGeometry(node, geometries); dc.setSpatialExtentFromGeometry(geometries); - dc = Commons.restrictToSpatialExtent(dc); + dc = Commons.restrictToSpatialExtent(node, dc); dc.imageCollection(ic => ic.map(img => img.clip(geom))); return dc; } catch (e) { @@ -347,7 +349,8 @@ export default class Commons { return dc; } - static setAggregationLabels(images, frequency) { + static setAggregationLabels(node, images, frequency) { + const ee = node.ee; let aggregationFormat = null; let temporalFormat = null; let seasons = {}; @@ -373,10 +376,10 @@ export default class Commons { temporalFormat = "yyyy"; break; case 'seasons': - seasons = Utils.seasons(); + seasons = GeeUtils.seasons(node); break; case 'tropical_seasons': - seasons = Utils.tropicalSeasons(); + seasons = GeeUtils.tropicalSeasons(node); break; } diff --git a/src/processgraph/context.js b/src/processgraph/context.js index c93384d..e913d25 100644 --- a/src/processgraph/context.js +++ b/src/processgraph/context.js @@ -6,9 +6,33 @@ import fse from 'fs-extra'; export default class ProcessingContext { - constructor(serverContext, userId = null) { + constructor(serverContext, user) { this.serverContext = serverContext; - this.userId = userId; + this.user = user; + this.userId = user._id; + this.ee = Utils.require('@google/earthengine'); + } + + async connectGee() { + const user = this.getUser(); + const ee = this.ee; + if (user._id.startsWith("google-")) { + console.log("Authenticate via user token"); + const expires = 59 * 60; + // todo: get expiration from token and set more parameters + ee.apiclient.setAuthToken(null, 'Bearer', user.token, expires, [], null, false, false); + } + else { + console.log("Authenticate via private key"); + await new Promise((resolve, reject) => { + ee.data.authenticateViaPrivateKey( + this.serverContext.geePrivateKey, + () => resolve(), + error => reject("ERROR: GEE Authentication failed: " + error.message) + ); + }); + } + await ee.initialize(); } server() { @@ -51,6 +75,10 @@ export default class ProcessingContext { return this.userId; } + getUser() { + return this.user; + } + // ToDo processes: the selection of formats and bands is really strict at the moment, maybe some of them are too strict async retrieveResults(dataCube) { const logger = dataCube.getLogger(); diff --git a/src/processgraph/datacube.js b/src/processgraph/datacube.js index a47b1d7..a812fc6 100644 --- a/src/processgraph/datacube.js +++ b/src/processgraph/datacube.js @@ -4,7 +4,8 @@ import Errors from '../utils/errors.js'; export default class DataCube { - constructor(sourceDataCube = null, data = undefined) { + constructor(ee, sourceDataCube = null, data = undefined) { + this.ee = ee; // Don't set this data directly, always use setData() to reset the type cache! this.data = data; // Cache the data type for less overhead, especially for ee.ComputedObject @@ -48,23 +49,24 @@ export default class DataCube { this.type = null; } - static getDataType(data, logger = null) { - if (data instanceof ee.Image) { + getDataType() { + const ee = this.ee; + if (this.data instanceof ee.Image) { return "eeImage"; } - else if (data instanceof ee.ImageCollection) { + else if (this.data instanceof ee.ImageCollection) { return "eeImageCollection"; } - else if(data instanceof ee.Array) { + else if(this.data instanceof ee.Array) { return "eeArray"; } // Check for ComputedObject only after checking all the other EE types above - else if (data instanceof ee.ComputedObject) { - if (logger) { - logger.warn("Calling slow function getInfo(); Try to avoid this."); + else if (this.data instanceof ee.ComputedObject) { + if (this.logger) { + this.logger.warn("Calling slow function getInfo(); Try to avoid this."); } // ToDo perf: This is slow and needs to be replaced so that it uses a callback as parameter for getInfo() and the method will be async. - const info = data.getInfo(); + const info = this.data.getInfo(); // Only works for Image and ImageCollection and maybe some other types, but not for Array for example. // Arrays, numbers and all other scalars should be handled with the native JS code below. if (Utils.isObject(info) && typeof info.type === 'string') { @@ -73,14 +75,14 @@ export default class DataCube { } // Check for native JS types - if (Array.isArray(data)) { + if (Array.isArray(this.data)) { return "array"; } - else if (data === null) { + else if (this.data === null) { return "null"; } - else if (typeof data === 'object' && data.constructor && typeof data.constructor.name === 'string') { - return data.constructor.name; // ToDo processes: This may conflict with other types, e.g. Image or ImageCollection + else if (typeof data === 'object' && this.data.constructor && typeof this.data.constructor.name === 'string') { + return this.data.constructor.name; // ToDo processes: This may conflict with other types, e.g. Image or ImageCollection } else { return typeof data; @@ -89,7 +91,7 @@ export default class DataCube { objectType() { if (this.type === null) { - this.type = DataCube.getDataType(this.data, this.logger); + this.type = this.getDataType(); } return this.type; } @@ -115,6 +117,7 @@ export default class DataCube { } image(callback = null, ...args) { + const ee = this.ee; if (this.isImage()){ // no operation } @@ -137,6 +140,7 @@ export default class DataCube { } imageCollection(callback = null, ...args) { + const ee = this.ee; if (this.isImageCollection()){ // no operation } @@ -478,6 +482,7 @@ export default class DataCube { // ToDo processes: revise this functions for other/more complex use cases #64 stackCollection(collection) { + const ee = this.ee; // create an initial image. const first = ee.Image(collection.first()).select([]); // write a function that appends a band to an image. diff --git a/src/processgraph/node.js b/src/processgraph/node.js index e215298..049e201 100644 --- a/src/processgraph/node.js +++ b/src/processgraph/node.js @@ -7,6 +7,10 @@ export default class GeeProcessGraphNode extends ProcessGraphNode { super(json, id, parent); } + get ee() { + return this.processGraph.getContext().ee; + } + getLogger() { return this.processGraph.getLogger() || console; // If no logger is set, use console.xxx } @@ -50,7 +54,7 @@ export default class GeeProcessGraphNode extends ProcessGraphNode { } getDataCube(name) { - return new DataCube(this.getArgument(name)); + return new DataCube(this.ee, this.getArgument(name)); } } diff --git a/src/processgraph/processgraph.js b/src/processgraph/processgraph.js index 8a3c7e9..f85cd28 100644 --- a/src/processgraph/processgraph.js +++ b/src/processgraph/processgraph.js @@ -63,6 +63,11 @@ export default class GeeProcessGraph extends ProcessGraph { return await process.execute(node, this.context); } + async execute(args = null) { + await this.context.connectGee(); + return await super.execute(args); + } + optimizeLoadCollectionRect(rect) { this.loadCollectionRect = rect; diff --git a/src/processgraph/utils.js b/src/processgraph/utils.js new file mode 100644 index 0000000..765b3cd --- /dev/null +++ b/src/processgraph/utils.js @@ -0,0 +1,76 @@ +const GeeUtils = { + + tropicalSeasons(node) { + const ee = node.ee; + return { + ndjfma: ee.List([-1, 0, 1, 2, 3, 4]), + mjjaso: ee.List([5, 6, 7, 8, 9, 10]) + }; + }, + + seasons(node) { + const ee = node.ee; + return { + djf: ee.List([0, 1, 2]), + mam: ee.List([3, 4, 5]), + jja: ee.List([6, 7, 8]), + son: ee.List([9, 10, 11]) + }; + }, + + geoJsonToGeometry(node, geojson) { + const ee = node.ee; + switch(geojson.type) { + case 'Feature': + return ee.Geometry(geojson.geometry); + case 'FeatureCollection': { + const geometries = { + type: "GeometryCollection", + geometries: [] + }; + for(const i in geojson.features) { + geometries.geometries.push(geojson.features[i].geometry); + } + return ee.Geometry(geometries); + } + case 'Point': + case 'MultiPoint': + case 'LineString': + case 'MultiLineString': + case 'Polygon': + case 'MultiPolygon': + case 'GeometryCollection': + return ee.Geometry(geojson); + default: + return null; + } + + }, + + geoJsonToFeatureCollection(node, geojson) { + const ee = node.ee; + switch(geojson.type) { + case 'Point': + case 'MultiPoint': + case 'LineString': + case 'MultiLineString': + case 'Polygon': + case 'MultiPolygon': + case 'GeometryCollection': + case 'Feature': + return ee.FeatureCollection(ee.Feature(geojson)); + case 'FeatureCollection': { + const features = []; + for(const i in geojson.features) { + features.push(ee.Feature(geojson.features[i])); + } + return ee.FeatureCollection(features); + } + default: + return null; + } + }, + +}; + +export default GeeUtils; diff --git a/src/server.js b/src/server.js index d0c637d..7f8b808 100644 --- a/src/server.js +++ b/src/server.js @@ -14,8 +14,6 @@ import ServerContext from './utils/servercontext.js'; import fse from 'fs-extra'; import restify from 'restify'; -global.ee = Utils.require('@google/earthengine'); - class Server { constructor() { @@ -42,18 +40,7 @@ class Server { this.api.users = new UsersAPI(this.serverContext); this.api.processGraphs = new ProcessGraphsAPI(this.serverContext); - const privateKey = fse.readJsonSync(this.serverContext.serviceAccountCredentialsFile); - ee.data.authenticateViaPrivateKey(privateKey, - () => { - console.info("GEE Authentication succeeded."); - ee.initialize(); - this.startServer(); - }, - (error) => { - console.error("ERROR: GEE Authentication failed: " + error); - process.exit(1); - } - ); + this.startServer(); } addEndpoint(method, path, callback, expose = true, root = false) { diff --git a/src/utils/config.js b/src/utils/config.js index 8ff02fb..fbe370f 100644 --- a/src/utils/config.js +++ b/src/utils/config.js @@ -24,7 +24,8 @@ export default class Config { certificate: null }; - this.serviceAccountCredentialsFile = "privatekey.json"; + this.serviceAccountCredentialsFile = null; + this.googleAuthClients = []; this.currency = null; this.plans = { diff --git a/src/utils/servercontext.js b/src/utils/servercontext.js index a3ead1f..ac64027 100644 --- a/src/utils/servercontext.js +++ b/src/utils/servercontext.js @@ -9,6 +9,7 @@ import FileWorkspace from '../models/workspace.js'; import JobStore from '../models/jobstore.js'; import UserStore from '../models/userstore.js'; import ServiceStore from '../models/servicestore.js'; +import fse from 'fs-extra'; export default class ServerContext extends Config { @@ -22,6 +23,12 @@ export default class ServerContext extends Config { this.userStore = new UserStore(); this.serviceStore = new ServiceStore(); this.tempFolder = './storage/temp_files'; + if (this.serviceAccountCredentialsFile) { + this.geePrivateKey = fse.readJsonSync(this.serviceAccountCredentialsFile); + } + else { + this.geePrivateKey = null; + } } jobs() { @@ -68,7 +75,7 @@ export default class ServerContext extends Config { } processingContext(req) { - return new ProcessingContext(this, req.user._id); + return new ProcessingContext(this, req.user); } } diff --git a/src/utils/utils.js b/src/utils/utils.js index 8828fd6..efce86e 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -20,22 +20,6 @@ const Utils = { return x; }, - tropicalSeasons() { - return { - ndjfma: ee.List([-1, 0, 1, 2, 3, 4]), - mjjaso: ee.List([5, 6, 7, 8, 9, 10]) - }; - }, - - seasons() { - return { - djf: ee.List([0, 1, 2]), - mam: ee.List([3, 4, 5]), - jja: ee.List([6, 7, 8]), - son: ee.List([9, 10, 11]) - }; - }, - sequence(min, max) { const list = []; for(let i = min; i <= max; i++) { @@ -204,57 +188,6 @@ const Utils = { } }, - geoJsonToGeometry(geojson) { - switch(geojson.type) { - case 'Feature': - return ee.Geometry(geojson.geometry); - case 'FeatureCollection': { - const geometries = { - type: "GeometryCollection", - geometries: [] - }; - for(const i in geojson.features) { - geometries.geometries.push(geojson.features[i].geometry); - } - return ee.Geometry(geometries); - } - case 'Point': - case 'MultiPoint': - case 'LineString': - case 'MultiLineString': - case 'Polygon': - case 'MultiPolygon': - case 'GeometryCollection': - return ee.Geometry(geojson); - default: - return null; - } - - }, - - geoJsonToFeatureCollection(geojson) { - switch(geojson.type) { - case 'Point': - case 'MultiPoint': - case 'LineString': - case 'MultiLineString': - case 'Polygon': - case 'MultiPolygon': - case 'GeometryCollection': - case 'Feature': - return ee.FeatureCollection(ee.Feature(geojson)); - case 'FeatureCollection': { - const features = []; - for(const i in geojson.features) { - features.push(ee.Feature(geojson.features[i])); - } - return ee.FeatureCollection(features); - } - default: - return null; - } - }, - getFileExtension(file) { return file.split('.').pop(); },