auth IoC 1 - initializeDefaultAppsFactory
This commit is contained in:
Родитель
9992a9bd1d
Коммит
68376b91c1
|
@ -4,6 +4,8 @@ import {
|
|||
getServerOrigin,
|
||||
getFeatureFlags
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { ServerScope } from '@speckle/shared'
|
||||
import { Merge } from 'type-fest'
|
||||
|
||||
export enum DefaultAppIds {
|
||||
Web = 'spklwebapp',
|
||||
|
@ -150,3 +152,10 @@ export function getDefaultApps() {
|
|||
export function getDefaultApp({ id }: { id: string }) {
|
||||
return defaultApps.find((app) => app.id === id) || null
|
||||
}
|
||||
|
||||
export type DefaultApp = (typeof defaultApps)[number]
|
||||
|
||||
/**
|
||||
* Some workflows need 'all' unwrapped into the actual scopes
|
||||
*/
|
||||
export type DefaultAppWithUnwrappedScopes = Merge<DefaultApp, { scopes: ServerScope[] }>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { DefaultAppWithUnwrappedScopes } from '@/modules/auth/defaultApps'
|
||||
import { FullServerApp } from '@/modules/auth/domain/types'
|
||||
import { ScopeRecord } from '@/modules/auth/helpers/types'
|
||||
|
||||
export type GetApp = (params: { id: string }) => Promise<FullServerApp | null>
|
||||
|
||||
export type GetAllScopes = () => Promise<ScopeRecord[]>
|
||||
|
||||
export type RegisterDefaultApp = (app: DefaultAppWithUnwrappedScopes) => Promise<void>
|
||||
|
||||
export type UpdateDefaultApp = (
|
||||
app: DefaultAppWithUnwrappedScopes,
|
||||
existingApp: FullServerApp
|
||||
) => Promise<void>
|
||||
|
||||
export type InitializeDefaultApps = () => Promise<void>
|
|
@ -0,0 +1,7 @@
|
|||
import { ScopeRecord } from '@/modules/auth/helpers/types'
|
||||
import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types'
|
||||
|
||||
export type FullServerApp = ServerAppRecord & {
|
||||
scopes: ScopeRecord[]
|
||||
author: Pick<UserRecord, 'id' | 'name' | 'avatar'>
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const {
|
||||
getApp,
|
||||
getAllPublicApps,
|
||||
getAllAppsCreatedByUser,
|
||||
getAllAppsAuthorizedByUser,
|
||||
|
@ -10,6 +9,10 @@ const {
|
|||
revokeExistingAppCredentialsForUser
|
||||
} = require('../../services/apps')
|
||||
const { Roles } = require('@speckle/shared')
|
||||
const { getAppFactory } = require('@/modules/auth/repositories/apps')
|
||||
const { db } = require('@/db/knex')
|
||||
|
||||
const getApp = getAppFactory({ db })
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { MaybeAsync } from '@speckle/shared'
|
||||
import { MaybeAsync, ServerScope } from '@speckle/shared'
|
||||
import type { Express, RequestHandler } from 'express'
|
||||
import type { Session, SessionData } from 'express-session'
|
||||
import type { TokenSet, UserinfoResponse } from 'openid-client'
|
||||
|
@ -51,9 +51,26 @@ export type AuthRequestData = {
|
|||
authRedirectPath?: string
|
||||
}
|
||||
|
||||
export type ScopeRecord = {
|
||||
name: ServerScope
|
||||
description: string
|
||||
public: boolean
|
||||
}
|
||||
|
||||
export type ServerAppsScopesRecord = {
|
||||
appId: string
|
||||
scopeName: string
|
||||
scopeName: ServerScope
|
||||
}
|
||||
|
||||
export type UserServerAppTokenRecord = {
|
||||
userId: string
|
||||
appId: string
|
||||
tokenId: string
|
||||
}
|
||||
|
||||
export type TokenScopeRecord = {
|
||||
tokenId: string
|
||||
scopeName: ServerScope
|
||||
}
|
||||
|
||||
export type AuthStrategyMetadata = {
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
const { registerOrUpdateScopeFactory } = require('@/modules/shared/repositories/scopes')
|
||||
const { moduleLogger } = require('@/logging/logging')
|
||||
const db = require('@/db/knex')
|
||||
const { initializeDefaultAppsFactory } = require('@/modules/auth/services/serverApps')
|
||||
const {
|
||||
getAllScopesFactory,
|
||||
getAppFactory,
|
||||
updateDefaultAppFactory,
|
||||
registerDefaultAppFactory
|
||||
} = require('@/modules/auth/repositories/apps')
|
||||
|
||||
const initializeDefaultApps = initializeDefaultAppsFactory({
|
||||
getAllScopes: getAllScopesFactory({ db }),
|
||||
getApp: getAppFactory({ db }),
|
||||
updateDefaultApp: updateDefaultAppFactory({ db }),
|
||||
registerDefaultApp: registerDefaultAppFactory({ db })
|
||||
})
|
||||
|
||||
exports.init = async (app) => {
|
||||
moduleLogger.info('🔑 Init auth module')
|
||||
|
@ -23,5 +37,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('./manageDefaultApps')()
|
||||
await initializeDefaultApps()
|
||||
}
|
||||
|
|
|
@ -1,100 +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 { 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
|
|
@ -0,0 +1,154 @@
|
|||
import { moduleLogger } from '@/logging/logging'
|
||||
import { getDefaultApp } from '@/modules/auth/defaultApps'
|
||||
import {
|
||||
GetAllScopes,
|
||||
GetApp,
|
||||
RegisterDefaultApp,
|
||||
UpdateDefaultApp
|
||||
} from '@/modules/auth/domain/operations'
|
||||
import {
|
||||
ScopeRecord,
|
||||
ServerAppsScopesRecord,
|
||||
TokenScopeRecord,
|
||||
UserServerAppTokenRecord
|
||||
} from '@/modules/auth/helpers/types'
|
||||
import {
|
||||
Scopes,
|
||||
ServerApps,
|
||||
ServerAppsScopes,
|
||||
TokenScopes,
|
||||
Users,
|
||||
UserServerAppTokens
|
||||
} from '@/modules/core/dbSchema'
|
||||
import { ServerAppRecord, UserRecord } from '@/modules/core/helpers/types'
|
||||
import { Knex } from 'knex'
|
||||
import { difference, omit } from 'lodash'
|
||||
|
||||
const tables = {
|
||||
serverApps: (db: Knex) => db<ServerAppRecord>(ServerApps.name),
|
||||
scopes: (db: Knex) => db<ScopeRecord>(Scopes.name),
|
||||
serverAppsScopes: (db: Knex) => db<ServerAppsScopesRecord>(ServerAppsScopes.name),
|
||||
users: (db: Knex) => db<UserRecord>(Users.name),
|
||||
userServerAppTokens: (db: Knex) =>
|
||||
db<UserServerAppTokenRecord>(UserServerAppTokens.name),
|
||||
tokenScopes: (db: Knex) => db<TokenScopeRecord>(TokenScopes.name)
|
||||
}
|
||||
|
||||
const getAppRedirectUrl = (app: ServerAppRecord) => {
|
||||
const defaultApp = getDefaultApp({ id: app.id })
|
||||
return defaultApp ? defaultApp.redirectUrl : app.redirectUrl
|
||||
}
|
||||
|
||||
export const getAppFactory =
|
||||
(deps: { db: Knex }): GetApp =>
|
||||
async (params) => {
|
||||
const { id } = params
|
||||
|
||||
const allScopes = await getAllScopesFactory(deps)()
|
||||
|
||||
const app = await tables.serverApps(deps.db).select('*').where({ id }).first()
|
||||
if (!app) return null
|
||||
|
||||
const appScopeNames = (
|
||||
await tables.serverAppsScopes(deps.db).select('scopeName').where({ appId: id })
|
||||
).map((s) => s.scopeName)
|
||||
|
||||
const appScopes = allScopes.filter(
|
||||
(scope) => appScopeNames.indexOf(scope.name) !== -1
|
||||
)
|
||||
const appAuthor = await tables
|
||||
.users(deps.db)
|
||||
.select('id', 'name', 'avatar')
|
||||
.where({ id: app.authorId })
|
||||
.first()
|
||||
|
||||
return {
|
||||
...app,
|
||||
scopes: appScopes,
|
||||
author: appAuthor!,
|
||||
redirectUrl: getAppRedirectUrl(app)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAllScopesFactory =
|
||||
(deps: { db: Knex }): GetAllScopes =>
|
||||
async () => {
|
||||
return tables.scopes(deps.db).select('*')
|
||||
}
|
||||
|
||||
export const registerDefaultAppFactory =
|
||||
(deps: { db: Knex }): RegisterDefaultApp =>
|
||||
async (app) => {
|
||||
const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s }))
|
||||
|
||||
await tables.serverApps(deps.db).insert(omit(app, 'scopes'))
|
||||
await tables.serverAppsScopes(deps.db).insert(scopes)
|
||||
}
|
||||
|
||||
export const updateDefaultAppFactory =
|
||||
(deps: { db: Knex }): UpdateDefaultApp =>
|
||||
async (app, existingApp) => {
|
||||
const { db: knex } = deps
|
||||
|
||||
const existingAppScopes = existingApp.scopes.map((s) => s.name)
|
||||
|
||||
const newScopes = difference(app.scopes, existingAppScopes)
|
||||
const removedScopes = difference(existingAppScopes, app.scopes)
|
||||
|
||||
let affectedTokenIds: string[] = []
|
||||
|
||||
if (newScopes.length || removedScopes.length) {
|
||||
moduleLogger.info(`🔑 Updating default app ${app.name}`)
|
||||
affectedTokenIds = await tables
|
||||
.userServerAppTokens(knex)
|
||||
.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 tables
|
||||
.serverAppsScopes(knex)
|
||||
.insert(newScopes.map((s) => ({ appId: app.id, scopeName: s })))
|
||||
.transacting(trx)
|
||||
|
||||
// remove scopes from the app
|
||||
if (removedScopes.length)
|
||||
await tables
|
||||
.serverAppsScopes(knex)
|
||||
.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 tables
|
||||
.tokenScopes(knex)
|
||||
.insert(newScopes.map((s) => ({ tokenId, scopeName: s })))
|
||||
.transacting(trx)
|
||||
|
||||
if (removedScopes.length)
|
||||
await tables
|
||||
.tokenScopes(knex)
|
||||
.where({ tokenId })
|
||||
.whereIn('scopeName', removedScopes)
|
||||
.delete()
|
||||
.transacting(trx)
|
||||
})
|
||||
)
|
||||
|
||||
// not writing the redirect url to the DB anymore
|
||||
// it will be patched on an application level from the default app definitions
|
||||
await tables
|
||||
.serverApps(knex)
|
||||
.where({ id: app.id })
|
||||
.update(omit(app, ['scopes', 'redirectUrl']))
|
||||
.transacting(trx)
|
||||
})
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
'use strict'
|
||||
const cors = require('cors')
|
||||
const {
|
||||
getApp,
|
||||
createAuthorizationCode,
|
||||
createAppTokenFromAccessCode,
|
||||
refreshAppToken
|
||||
|
@ -12,6 +11,8 @@ const { validateScopes } = require(`@/modules/shared`)
|
|||
const { InvalidAccessCodeRequestError } = require('@/modules/auth/errors')
|
||||
const { Scopes } = require('@speckle/shared')
|
||||
const { ForbiddenError } = require('@/modules/shared/errors')
|
||||
const { getAppFactory } = require('@/modules/auth/repositories/apps')
|
||||
const { db } = require('@/db/knex')
|
||||
|
||||
// TODO: Secure these endpoints!
|
||||
module.exports = (app) => {
|
||||
|
@ -21,6 +22,7 @@ module.exports = (app) => {
|
|||
*/
|
||||
app.get('/auth/accesscode', async (req, res) => {
|
||||
try {
|
||||
const getApp = getAppFactory({ db })
|
||||
const preventRedirect = !!req.query.preventRedirect
|
||||
const appId = req.query.appId
|
||||
const app = await getApp({ id: appId })
|
||||
|
|
|
@ -6,11 +6,10 @@ 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 { getAppFactory } = require('@/modules/auth/repositories/apps')
|
||||
const ApiTokens = () => knex('api_tokens')
|
||||
const ServerApps = () => knex('server_apps')
|
||||
const ServerAppsScopes = () => knex('server_apps_scopes')
|
||||
const Scopes = () => knex('scopes')
|
||||
|
||||
const AuthorizationCodes = () => knex('authorization_codes')
|
||||
const RefreshTokens = () => knex('refresh_tokens')
|
||||
|
@ -22,25 +21,6 @@ const addDefaultAppOverrides = (app) => {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
async getApp({ id }) {
|
||||
const allScopes = await Scopes().select('*')
|
||||
|
||||
const app = await ServerApps().select('*').where({ id }).first()
|
||||
if (!app) return null
|
||||
|
||||
const appScopeNames = (
|
||||
await ServerAppsScopes().select('scopeName').where({ appId: id })
|
||||
).map((s) => s.scopeName)
|
||||
|
||||
app.scopes = allScopes.filter((scope) => appScopeNames.indexOf(scope.name) !== -1)
|
||||
app.author = await Users()
|
||||
.select('id', 'name', 'avatar')
|
||||
.where({ id: app.authorId })
|
||||
.first()
|
||||
|
||||
return addDefaultAppOverrides(app)
|
||||
},
|
||||
|
||||
async getAllPublicApps() {
|
||||
const apps = await ServerApps()
|
||||
.select(
|
||||
|
@ -288,8 +268,8 @@ module.exports = {
|
|||
const valid = await bcrypt.compare(refreshTokenContent, refreshTokenDb.tokenDigest)
|
||||
if (!valid) throw new Error('Invalid token') // sneky hackstors
|
||||
|
||||
const app = await module.exports.getApp({ id: appId })
|
||||
if (app.secret !== appSecret) throw new Error('Invalid request')
|
||||
const app = await getAppFactory({ db: knex })({ id: appId })
|
||||
if (!app || app.secret !== appSecret) throw new Error('Invalid request')
|
||||
|
||||
// Create the new token
|
||||
const appToken = await createAppToken({
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { getDefaultApps } from '@/modules/auth/defaultApps'
|
||||
import {
|
||||
GetAllScopes,
|
||||
GetApp,
|
||||
InitializeDefaultApps,
|
||||
RegisterDefaultApp,
|
||||
UpdateDefaultApp
|
||||
} from '@/modules/auth/domain/operations'
|
||||
import { ScopeRecord } from '@/modules/auth/helpers/types'
|
||||
import { ServerScope } from '@speckle/shared'
|
||||
|
||||
/**
|
||||
* Cached all scopes. Caching occurs on first initializeDefaultApps() call
|
||||
*/
|
||||
let allScopes: ScopeRecord[] = []
|
||||
|
||||
export const initializeDefaultAppsFactory =
|
||||
(deps: {
|
||||
getAllScopes: GetAllScopes
|
||||
getApp: GetApp
|
||||
updateDefaultApp: UpdateDefaultApp
|
||||
registerDefaultApp: RegisterDefaultApp
|
||||
}): InitializeDefaultApps =>
|
||||
async () => {
|
||||
allScopes = await deps.getAllScopes()
|
||||
|
||||
await Promise.all(
|
||||
getDefaultApps().map(async (app) => {
|
||||
const scopes =
|
||||
app?.scopes === 'all'
|
||||
? allScopes.map((s) => s.name)
|
||||
: (app.scopes as ServerScope[])
|
||||
|
||||
const existingApp = await deps.getApp({ id: app.id })
|
||||
if (existingApp) {
|
||||
await deps.updateDefaultApp(
|
||||
{
|
||||
...app,
|
||||
scopes
|
||||
},
|
||||
existingApp
|
||||
)
|
||||
} else {
|
||||
await deps.registerDefaultApp({
|
||||
...app,
|
||||
scopes
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
|
@ -5,7 +5,6 @@ const { createUser } = require(`@/modules/core/services/users`)
|
|||
const { validateToken } = require(`@/modules/core/services/tokens`)
|
||||
const { beforeEachContext } = require(`@/test/hooks`)
|
||||
const {
|
||||
getApp,
|
||||
getAllPublicApps,
|
||||
createApp,
|
||||
updateApp,
|
||||
|
@ -17,9 +16,15 @@ const {
|
|||
} = require('../services/apps')
|
||||
|
||||
const { Scopes } = require('@/modules/core/helpers/mainConstants')
|
||||
const { updateDefaultApp } = require('@/modules/auth/manageDefaultApps')
|
||||
const knex = require('@/db/knex')
|
||||
const cryptoRandomString = require('crypto-random-string')
|
||||
const {
|
||||
getAppFactory,
|
||||
updateDefaultAppFactory
|
||||
} = require('@/modules/auth/repositories/apps')
|
||||
|
||||
const getApp = getAppFactory({ db: knex })
|
||||
const updateDefaultApp = updateDefaultAppFactory({ db: knex })
|
||||
|
||||
describe('Services @apps-services', () => {
|
||||
const actor = {
|
||||
|
|
Загрузка…
Ссылка в новой задаче