diff --git a/packages/server/.env-example b/packages/server/.env-example index 16525aad17..b1a9b83f6c 100644 --- a/packages/server/.env-example +++ b/packages/server/.env-example @@ -126,4 +126,9 @@ STRATEGY_LOCAL=true # FRONTEND_HOST=127.0.0.1 # FRONTEND_PORT=8081 -SPECKLE_AUTOMATE_URL="http://127.0.0.1:3030" +############################################################ +# Speckle automate related variables +# the env var is only needed if you are running the server and +# the execution engine locally +# SPECKLE_AUTOMATE_URL="http://127.0.0.1:3030" +############################################################ diff --git a/packages/server/modules/auth/defaultApps.js b/packages/server/modules/auth/defaultApps.js deleted file mode 100644 index 9b26563f14..0000000000 --- a/packages/server/modules/auth/defaultApps.js +++ /dev/null @@ -1,220 +0,0 @@ -'use strict' -const knex = require('@/db/knex') -const Scopes = () => knex('scopes') -const Apps = () => knex('server_apps') -const AppScopes = () => knex('server_apps_scopes') - -const { getApp } = require('@/modules/auth/services/apps') -const { Scopes: ScopesConst } = require('@/modules/core/helpers/mainConstants') -const { difference } = require('lodash') -const { moduleLogger } = require('@/logging/logging') -const { speckleAutomateUrl } = require('@/modules/shared/helpers/envHelper') - -let allScopes = [] - -module.exports = async () => { - allScopes = await Scopes().select('*') - - // Note: shallow cloning of app objs so as to not interfere with the original objects. - await registerOrUpdateApp({ ...SpeckleWebApp }) - await registerOrUpdateApp({ ...SpeckleApiExplorer }) - await registerOrUpdateApp({ ...SpeckleDesktopApp }) - await registerOrUpdateApp({ ...SpeckleConnectorApp }) - await registerOrUpdateApp({ ...SpeckleExcel }) - await registerOrUpdateApp({ ...SpecklePowerBi }) - await registerOrUpdateApp({ ...SpeckleAutomate }) -} - -async function registerOrUpdateApp(app) { - if (app.scopes && app.scopes === 'all') { - // let scopes = await Scopes( ).select( '*' ) - // logger.debug( allScopes.length ) - app.scopes = allScopes.map((s) => s.name) - } - - const existingApp = await getApp({ id: app.id }) - if (existingApp) { - updateDefaultApp(app, existingApp) - } else { - await registerDefaultApp(app) - } -} - -async function registerDefaultApp(app) { - const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s })) - delete app.scopes - await Apps().insert(app) - await AppScopes().insert(scopes) -} - -async function updateDefaultApp(app, existingApp) { - const existingAppScopes = existingApp.scopes.map((s) => s.name) - - const newScopes = difference(app.scopes, existingAppScopes) - const removedScopes = difference(existingAppScopes, app.scopes) - - let affectedTokenIds = [] - - if (newScopes.length || removedScopes.length) { - moduleLogger.info(`🔑 Updating default app ${app.name}`) - affectedTokenIds = await knex('user_server_app_tokens') - .where({ appId: app.id }) - .pluck('tokenId') - } - - // the internal code block makes sure if an error occurred, the trx gets rolled back - await knex.transaction(async (trx) => { - // add new scopes to the app - if (newScopes.length) - await AppScopes() - .insert(newScopes.map((s) => ({ appId: app.id, scopeName: s }))) - .transacting(trx) - - // remove scopes from the app - if (removedScopes.length) - await AppScopes() - .where({ appId: app.id }) - .whereIn('scopeName', removedScopes) - .delete() - .transacting(trx) - - //update user tokens with scope changes - if (affectedTokenIds.length) - await Promise.all( - affectedTokenIds.map(async (tokenId) => { - if (newScopes.length) - await knex('token_scopes') - .insert(newScopes.map((s) => ({ tokenId, scopeName: s }))) - .transacting(trx) - - if (removedScopes.length) - await knex('token_scopes') - .where({ tokenId }) - .whereIn('scopeName', removedScopes) - .delete() - .transacting(trx) - }) - ) - delete app.scopes - await Apps().where({ id: app.id }).update(app).transacting(trx) - }) -} - -// this is exported to be able to test the retention of permissions -module.exports.updateDefaultApp = updateDefaultApp - -const SpeckleWebApp = { - id: 'spklwebapp', - secret: 'spklwebapp', - name: 'Speckle Web Manager', - description: - 'The Speckle Web Manager is your one-stop place to manage and coordinate your data.', - trustByDefault: true, - public: true, - redirectUrl: process.env.CANONICAL_URL, - scopes: 'all' -} - -const SpeckleApiExplorer = { - id: 'explorer', - secret: 'explorer', - name: 'Speckle Explorer', - description: 'GraphiQL Playground with authentication.', - trustByDefault: true, - public: true, - redirectUrl: new URL('/explorer', process.env.CANONICAL_URL).toString(), - scopes: 'all' -} - -const SpeckleDesktopApp = { - id: 'sdm', - secret: 'sdm', - name: 'Speckle Desktop Manager', - description: - 'Manages local installations of Speckle connectors, kits and everything else.', - trustByDefault: true, - public: true, - redirectUrl: 'speckle://account', - scopes: [ - ScopesConst.Streams.Read, - ScopesConst.Streams.Write, - ScopesConst.Profile.Read, - ScopesConst.Profile.Email, - ScopesConst.Users.Read, - ScopesConst.Users.Invite - ] -} - -const SpeckleConnectorApp = { - id: 'sca', - secret: 'sca', - name: 'Speckle Connector', - description: 'A Speckle Desktop Connectors.', - trustByDefault: true, - public: true, - redirectUrl: 'http://localhost:29363', - scopes: [ - ScopesConst.Streams.Read, - ScopesConst.Streams.Write, - ScopesConst.Profile.Read, - ScopesConst.Profile.Email, - ScopesConst.Users.Read, - ScopesConst.Users.Invite - ] -} - -const SpeckleExcel = { - id: 'spklexcel', - secret: 'spklexcel', - name: 'Speckle Connector For Excel', - description: - 'The Speckle Connector For Excel. For more info, check the docs here: https://speckle.guide/user/excel.', - trustByDefault: true, - public: true, - redirectUrl: 'https://speckle-excel.netlify.app', - scopes: [ - ScopesConst.Streams.Read, - ScopesConst.Streams.Write, - ScopesConst.Profile.Read, - ScopesConst.Profile.Email, - ScopesConst.Users.Read, - ScopesConst.Users.Invite - ] -} - -const SpecklePowerBi = { - id: 'spklpwerbi', - secret: 'spklpwerbi', - name: 'Speckle Connector For PowerBI', - description: - 'The Speckle Connector For Excel. For more info check the docs here: https://speckle.guide/user/powerbi.html.', - trustByDefault: true, - public: true, - redirectUrl: 'https://oauth.powerbi.com/views/oauthredirect.html', - scopes: [ - ScopesConst.Streams.Read, - ScopesConst.Profile.Read, - ScopesConst.Profile.Email, - ScopesConst.Users.Read, - ScopesConst.Users.Invite - ] -} - -const SpeckleAutomate = { - id: 'spklautoma', - secret: 'spklautoma', - name: 'Speckle Automate', - description: 'Our automation platform', - trustByDefault: true, - public: true, - redirectUrl: `${speckleAutomateUrl()}/authn/callback`, - scopes: [ - ScopesConst.Profile.Email, - ScopesConst.Profile.Read, - ScopesConst.Users.Read, - ScopesConst.Tokens.Write, - ScopesConst.Streams.Read, - ScopesConst.Streams.Write, - ScopesConst.Automate.ReportResults - ] -} diff --git a/packages/server/modules/auth/defaultApps.ts b/packages/server/modules/auth/defaultApps.ts new file mode 100644 index 0000000000..a79161ce00 --- /dev/null +++ b/packages/server/modules/auth/defaultApps.ts @@ -0,0 +1,136 @@ +import { Scopes } from '@/modules/core/helpers/mainConstants' +import { speckleAutomateUrl, getServerOrigin } from '@/modules/shared/helpers/envHelper' + +const SpeckleWebApp = { + id: 'spklwebapp', + secret: 'spklwebapp', + name: 'Speckle Web Manager', + description: + 'The Speckle Web Manager is your one-stop place to manage and coordinate your data.', + trustByDefault: true, + public: true, + redirectUrl: getServerOrigin(), + scopes: 'all' +} + +const SpeckleApiExplorer = { + id: 'explorer', + secret: 'explorer', + name: 'Speckle Explorer', + description: 'GraphiQL Playground with authentication.', + trustByDefault: true, + public: true, + redirectUrl: new URL('/explorer', getServerOrigin()).toString(), + scopes: 'all' +} + +const SpeckleDesktopApp = { + id: 'sdm', + secret: 'sdm', + name: 'Speckle Desktop Manager', + description: + 'Manages local installations of Speckle connectors, kits and everything else.', + trustByDefault: true, + public: true, + redirectUrl: 'speckle://account', + scopes: [ + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Profile.Read, + Scopes.Profile.Email, + Scopes.Users.Read, + Scopes.Users.Invite + ] +} + +const SpeckleConnectorApp = { + id: 'sca', + secret: 'sca', + name: 'Speckle Connector', + description: 'A Speckle Desktop Connectors.', + trustByDefault: true, + public: true, + redirectUrl: 'http://localhost:29363', + scopes: [ + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Profile.Read, + Scopes.Profile.Email, + Scopes.Users.Read, + Scopes.Users.Invite + ] +} + +const SpeckleExcel = { + id: 'spklexcel', + secret: 'spklexcel', + name: 'Speckle Connector For Excel', + description: + 'The Speckle Connector For Excel. For more info, check the docs here: https://speckle.guide/user/excel.', + trustByDefault: true, + public: true, + redirectUrl: 'https://speckle-excel.netlify.app', + scopes: [ + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Profile.Read, + Scopes.Profile.Email, + Scopes.Users.Read, + Scopes.Users.Invite + ] +} + +const SpecklePowerBi = { + id: 'spklpwerbi', + secret: 'spklpwerbi', + name: 'Speckle Connector For PowerBI', + description: + 'The Speckle Connector For Excel. For more info check the docs here: https://speckle.guide/user/powerbi.html.', + trustByDefault: true, + public: true, + redirectUrl: 'https://oauth.powerbi.com/views/oauthredirect.html', + scopes: [ + Scopes.Streams.Read, + Scopes.Profile.Read, + Scopes.Profile.Email, + Scopes.Users.Read, + Scopes.Users.Invite + ] +} + +const SpeckleAutomate = { + id: 'spklautoma', + secret: 'spklautoma', + name: 'Speckle Automate', + description: 'Our automation platform', + trustByDefault: true, + public: true, + redirectUrl: `${speckleAutomateUrl()}/authn/callback`, + scopes: [ + Scopes.Profile.Email, + Scopes.Profile.Read, + Scopes.Users.Read, + Scopes.Tokens.Write, + Scopes.Streams.Read, + Scopes.Streams.Write, + Scopes.Automate.ReportResults + ] +} + +const defaultApps = [ + SpeckleWebApp, + SpeckleApiExplorer, + SpeckleDesktopApp, + SpeckleConnectorApp, + SpeckleExcel, + SpecklePowerBi, + SpeckleAutomate +] + +export function getDefaultApps() { + return defaultApps +} + +export function getDefaultApp({ id }: { id: string }) { + return defaultApps.find((app) => app.id === id) || null +} diff --git a/packages/server/modules/auth/index.js b/packages/server/modules/auth/index.js index 25c84d448f..91823ec3e8 100644 --- a/packages/server/modules/auth/index.js +++ b/packages/server/modules/auth/index.js @@ -21,5 +21,5 @@ exports.init = async (app) => { exports.finalize = async () => { // Note: we're registering the default apps last as we want to ensure that all // scopes have been registered by any other modules. - await require('./defaultApps')() + await require('./manageDefaultApps')() } diff --git a/packages/server/modules/auth/manageDefaultApps.js b/packages/server/modules/auth/manageDefaultApps.js new file mode 100644 index 0000000000..fe7352425c --- /dev/null +++ b/packages/server/modules/auth/manageDefaultApps.js @@ -0,0 +1,100 @@ +'use strict' +const knex = require('@/db/knex') +const Scopes = () => knex('scopes') +const Apps = () => knex('server_apps') +const AppScopes = () => knex('server_apps_scopes') + +const { getApp } = require('@/modules/auth/services/apps') +const { difference } = require('lodash') +const { moduleLogger } = require('@/logging/logging') +const { getDefaultApps } = require('@/modules/auth/defaultApps') + +let allScopes = [] + +module.exports = async () => { + allScopes = await Scopes().select('*') + + // Note: shallow cloning of app objs so as to not interfere with the original objects. + await Promise.all(getDefaultApps().map((app) => registerOrUpdateApp({ ...app }))) +} + +async function registerOrUpdateApp(app) { + if (app.scopes && app.scopes === 'all') { + // let scopes = await Scopes( ).select( '*' ) + // logger.debug( allScopes.length ) + app.scopes = allScopes.map((s) => s.name) + } + + const existingApp = await getApp({ id: app.id }) + if (existingApp) { + updateDefaultApp(app, existingApp) + } else { + await registerDefaultApp(app) + } +} + +async function registerDefaultApp(app) { + const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s })) + delete app.scopes + await Apps().insert(app) + await AppScopes().insert(scopes) +} + +async function updateDefaultApp(app, existingApp) { + const existingAppScopes = existingApp.scopes.map((s) => s.name) + + const newScopes = difference(app.scopes, existingAppScopes) + const removedScopes = difference(existingAppScopes, app.scopes) + + let affectedTokenIds = [] + + if (newScopes.length || removedScopes.length) { + moduleLogger.info(`🔑 Updating default app ${app.name}`) + affectedTokenIds = await knex('user_server_app_tokens') + .where({ appId: app.id }) + .pluck('tokenId') + } + + // the internal code block makes sure if an error occurred, the trx gets rolled back + await knex.transaction(async (trx) => { + // add new scopes to the app + if (newScopes.length) + await AppScopes() + .insert(newScopes.map((s) => ({ appId: app.id, scopeName: s }))) + .transacting(trx) + + // remove scopes from the app + if (removedScopes.length) + await AppScopes() + .where({ appId: app.id }) + .whereIn('scopeName', removedScopes) + .delete() + .transacting(trx) + + //update user tokens with scope changes + if (affectedTokenIds.length) + await Promise.all( + affectedTokenIds.map(async (tokenId) => { + if (newScopes.length) + await knex('token_scopes') + .insert(newScopes.map((s) => ({ tokenId, scopeName: s }))) + .transacting(trx) + + if (removedScopes.length) + await knex('token_scopes') + .where({ tokenId }) + .whereIn('scopeName', removedScopes) + .delete() + .transacting(trx) + }) + ) + delete app.scopes + // not writing the redirect url to the DB anymore + // it will be patched on an application level from the default app definitions + delete app.redirectUrl + await Apps().where({ id: app.id }).update(app).transacting(trx) + }) +} + +// this is exported to be able to test the retention of permissions +module.exports.updateDefaultApp = updateDefaultApp diff --git a/packages/server/modules/auth/services/apps.js b/packages/server/modules/auth/services/apps.js index ca34909f17..b375f30973 100644 --- a/packages/server/modules/auth/services/apps.js +++ b/packages/server/modules/auth/services/apps.js @@ -5,6 +5,7 @@ const knex = require(`@/db/knex`) const { createBareToken, createAppToken } = require(`@/modules/core/services/tokens`) const { logger } = require('@/logging/logging') +const { getDefaultApp } = require('@/modules/auth/defaultApps') const Users = () => knex('users') const ApiTokens = () => knex('api_tokens') const ServerApps = () => knex('server_apps') @@ -14,6 +15,12 @@ const Scopes = () => knex('scopes') const AuthorizationCodes = () => knex('authorization_codes') const RefreshTokens = () => knex('refresh_tokens') +const addDefaultAppOverrides = (app) => { + const defaultApp = getDefaultApp({ id: app.id }) + if (defaultApp) app.redirectUrl = defaultApp.redirectUrl + return app +} + module.exports = { async getApp({ id }) { const allScopes = await Scopes().select('*') @@ -30,7 +37,8 @@ module.exports = { .select('id', 'name', 'avatar') .where({ id: app.authorId }) .first() - return app + + return addDefaultAppOverrides(app) }, async getAllPublicApps() { @@ -40,6 +48,7 @@ module.exports = { 'server_apps.name', 'server_apps.description', 'server_apps.trustByDefault', + 'server_apps.redirectUrl', 'server_apps.logo', 'server_apps.termsAndConditionsLink', 'users.name as authorName', @@ -50,6 +59,8 @@ module.exports = { .orderBy('server_apps.trustByDefault', 'DESC') apps.forEach((app) => { + app = addDefaultAppOverrides(app) + if (app.authorName && app.authorId) { app.author = { name: app.authorName, id: app.authorId } } @@ -100,10 +111,13 @@ module.exports = { ) const { rows } = await query - return rows.map((r) => ({ - ...r, - author: r.author?.id ? r.author : null - })) + return rows.map((r) => { + const app = { + ...r, + author: r.author?.id ? r.author : null + } + return addDefaultAppOverrides(app) + }) }, async createApp(app) { diff --git a/packages/server/modules/auth/tests/apps.spec.js b/packages/server/modules/auth/tests/apps.spec.js index 212ea5371f..c34fb552a8 100644 --- a/packages/server/modules/auth/tests/apps.spec.js +++ b/packages/server/modules/auth/tests/apps.spec.js @@ -17,7 +17,7 @@ const { } = require('../services/apps') const { Scopes } = require('@/modules/core/helpers/mainConstants') -const { updateDefaultApp } = require('@/modules/auth/defaultApps') +const { updateDefaultApp } = require('@/modules/auth/manageDefaultApps') const knex = require('@/db/knex') const cryptoRandomString = require('crypto-random-string') diff --git a/packages/server/modules/shared/helpers/envHelper.ts b/packages/server/modules/shared/helpers/envHelper.ts index f32c6b63c4..bcd7086444 100644 --- a/packages/server/modules/shared/helpers/envHelper.ts +++ b/packages/server/modules/shared/helpers/envHelper.ts @@ -197,8 +197,7 @@ export function enableMixpanel() { } export function speckleAutomateUrl() { - const automateUrl = - process.env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.systems' + const automateUrl = process.env.SPECKLE_AUTOMATE_URL return automateUrl }