@sap/cds-mtxs
Advanced tools
Comparing version 1.8.4 to 1.9.0
@@ -32,3 +32,2 @@ #!/usr/bin/env node | ||
} | ||
console.log(`cds-mtx ${cmd} successful.`) | ||
} | ||
@@ -35,0 +34,0 @@ |
@@ -120,2 +120,14 @@ const path = require('path') | ||
module.exports.checkMigration = async function checkMigration(req) { | ||
const { tenant } = req.data | ||
if (await mtxAdapter.hasExtensions(tenant)) { | ||
if (module.exports._hasExtensibilityEnv()) { | ||
if (!await mtxAdapter.isMigrated(tenant)) | ||
req.reject(422, `Upgrade of tenant ${tenant} aborted. Extensions have not been migrated yet`) | ||
} else { | ||
req.reject(422, `Upgrade of tenant ${tenant} aborted. Old MTX Extensions exist but extensibility is not configured (cds.requires.extensibility is false)`) | ||
} | ||
} | ||
} | ||
module.exports.migrate = async function migrate(tenants, options) { | ||
@@ -153,3 +165,3 @@ | ||
const migrated = await mtxAdapter.isMigrated(tenant) | ||
if (migrated.timestamp && !force) { | ||
if (migrated?.timestamp && !force) { | ||
migrationResult.log(tenant, `Tenant ${tenant} is already migrated. Skipping migration.`) | ||
@@ -216,3 +228,3 @@ continue | ||
} catch (error) { | ||
migrationResult.error(tenant, `Extension verification failed for tenant ${tenant} [${tenantProjectFolder}]), skipping migration`, error) | ||
migrationResult.error(tenant, `Extension verification failed for tenant ${tenant} [${tenantProjectFolder}]), skipping migration.`, error) | ||
continue | ||
@@ -243,2 +255,5 @@ } | ||
} else { | ||
// check if extensions exist -> abort if yes | ||
if (await mtxAdapter.hasExtensions(tenant)) throw new Error(`Extensions exist but extensibility is not configured (cds.requires.extensibilty is false)`) | ||
if (!dry) { | ||
@@ -305,6 +320,8 @@ await _addMetadata(tenant, metadata) | ||
const diffMessages = [] | ||
// are artifacts lost? | ||
const hanaDiffNewToOld = cds.compiler.to.hdi.migration(cds.minify(previewCsn), {}, cds.minify(existingHana.afterImage)) | ||
if (hanaDiffNewToOld.deletions.length) { | ||
throw new Error(`Verification error for tenant ${tenant}: migrated model is missing artifacts:\n ${hanaDiffNewToOld.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
diffMessages.push(`Migrated model is missing artifacts:\n ${hanaDiffNewToOld.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
} | ||
@@ -317,4 +334,4 @@ | ||
if (relevantMigrations.length) throw new Error(`Verification error for tenant ${tenant}: table migrations found\n` + | ||
`${relevantMigrations.map( ({ name, suffix, changeset }) => `${name}${suffix}: ${changeset.map(({sql}) => sql)}\n`)}`) | ||
if (relevantMigrations.length) diffMessages.push(`Table migrations found\n` + | ||
`${relevantMigrations.map( ({ name, suffix, changeset }) => ` ${name}${suffix}: ${changeset.map(({sql}) => sql)}\n`)}`) | ||
} | ||
@@ -327,4 +344,7 @@ | ||
if (filteredDeletions.length) { | ||
diffMessages.push(`Migrated model has additional artifacts:\n ${hanaDiffOldToNew.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
throw new Error(`Verification error for tenant ${tenant}: migrated model has additional artifacts:\n ${hanaDiffOldToNew.deletions.map( ({ name, suffix }) => `${name}${suffix}\n`)}`) | ||
} | ||
if (diffMessages.length) throw new Error(`Verification error for tenant ${tenant}:\n${diffMessages.join('\n')}`) | ||
} |
@@ -15,3 +15,3 @@ const cds = require('@sap/cds') | ||
const GLOBAL_DATA_META_TENANT = '__META__' | ||
function _tenantFilter(tenantId) { | ||
function _tenantFilter(tenantId, index, allTenants) { | ||
const t0 = cds.env.requires.multitenancy?.t0 ?? 't0' | ||
@@ -23,2 +23,3 @@ return tenantId | ||
&& tenantId !== t0 | ||
&& allTenants.includes(_prefixTenant(tenantId)) | ||
} | ||
@@ -51,7 +52,12 @@ | ||
const result = (await cds.tx({ tenant: _prefixTenant(tenant) }, tx => tx.run(CONTENT_METADATA_QUERY, [type, domain]))) | ||
const { CONTENT } = result && result[0] || {} | ||
const metadata = CONTENT ? JSON.parse(CONTENT) : {} | ||
// more robust behaviour | ||
if (!metadata.subscribedTenantId) metadata.subscribedTenantId = tenant | ||
return metadata | ||
const CONTENT = result?.[0]?.CONTENT || result?.[0]?.content // small tribute to sqlite for testing | ||
if (type === 'onboarding') { | ||
const metadata = CONTENT ? JSON.parse(CONTENT) : {} | ||
// more robust behaviour | ||
if (!metadata.subscribedTenantId) metadata.subscribedTenantId = tenant | ||
return metadata | ||
} else { | ||
return CONTENT ? JSON.parse(CONTENT) : null | ||
} | ||
} catch (error) { | ||
@@ -133,3 +139,3 @@ LOG.error(`Failed to query metadata for tenant '${tenant}' (domain: ${domain}): ${error}`) | ||
} catch (error) { | ||
this.logger.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
LOG.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
throw error; | ||
@@ -147,15 +153,29 @@ } | ||
module.exports.verifyMigration = async (tenant) => { | ||
if (!module.exports.wasOldMtx()) return true | ||
return module.exports.isMigrated(tenant) | ||
module.exports.hasExtensions = async (tenant) => { | ||
if (!(await module.exports.wasOldMtx())) return false | ||
const MODEL_FILE_QUERY = 'SELECT FILENAME FROM TENANT_FILES WHERE TYPE = ? AND DOMAIN = ?'; | ||
const domain = cds.env.mtx && cds.env.mtx.domain || '__default__' | ||
try { | ||
const files = (await cds.tx({ tenant: _prefixTenant(tenant) }, tx => tx.run(MODEL_FILE_QUERY, ['extension', domain]))); | ||
return files.length | ||
} catch (error) { | ||
LOG.error(`Failed to query extensions for tenant '${tenant}' (domain: ${domain}): ${error}`); | ||
return false; | ||
} | ||
} | ||
const wasOldMtx = [] | ||
// use this before automatic tenant list update in upgrade | ||
module.exports.wasOldMtx = async () => { | ||
const hana = require('@sap/cds-mtxs/srv/plugins/hana/srv-mgr') | ||
try { | ||
return !!(await hana.get('__META__')) | ||
} catch (error) { | ||
if (error.status === 404) return false | ||
else throw e | ||
if (!wasOldMtx.length) { | ||
const hana = require('@sap/cds-mtxs/srv/plugins/hana/srv-mgr') | ||
try { | ||
wasOldMtx.push(!!(await hana.get('__META__'))) | ||
} catch (error) { | ||
if (error.status === 404) wasOldMtx.push(false) | ||
else throw e | ||
} | ||
} | ||
return wasOldMtx[0] | ||
} | ||
@@ -162,0 +182,0 @@ |
{ | ||
"name": "@sap/cds-mtxs", | ||
"version": "1.8.4", | ||
"version": "1.9.0", | ||
"description": "SAP Cloud Application Programming Model - Multitenancy library", | ||
@@ -16,3 +16,4 @@ "homepage": "https://cap.cloud.sap/", | ||
"db/", | ||
"env.js" | ||
"env.js", | ||
"CHANGELOG.md" | ||
], | ||
@@ -19,0 +20,0 @@ "bin": { |
@@ -5,8 +5,2 @@ const cds = require ('@sap/cds/lib') | ||
class MTXServices extends cds.Service { async init(){ | ||
if (cds.mtx) { | ||
DEBUG?.('bootstrapping old MTX...') | ||
await cds.mtx.in (cds.app) // old mtxExtension | ||
if (!process.env.MTX_MIGRATION) return | ||
} | ||
// else... | ||
DEBUG?.('bootstrapping MTX services...') | ||
@@ -13,0 +7,0 @@ let { definitions } = cds.model |
@@ -55,3 +55,3 @@ const cds = require('@sap/cds/lib') | ||
const { subscribedTenantId } = data ?? {} | ||
return params?.[0]?.subscribedTenantId ?? subscribedTenantId | ||
return subscribedTenantId ?? params?.[0]?.subscribedTenantId | ||
} | ||
@@ -101,10 +101,10 @@ | ||
if (!one) cds.error(`Tenant ${tenant} not found`, { status: 404 }) | ||
return JSON.parse(one.metadata ?? '{}') | ||
return { tenant, ...JSON.parse(one.metadata ?? '{}') } | ||
} | ||
return (await cds.tx({ tenant: t0 }, tx => | ||
tx.run(SELECT.from(Tenants, tenant => { tenant.ID, tenant.metadata })) | ||
)).map(tenant => JSON.parse(tenant.metadata)) | ||
)).map(({ ID, metadata }) => ({ tenant: ID, ...JSON.parse(metadata) })) | ||
} | ||
async upgrade(tenantsIds) { | ||
async upgrade(tenantsIds, options) { | ||
if (!tenantsIds?.length) return | ||
@@ -126,3 +126,3 @@ const tenantList = tenantsIds.includes('*') ? undefined : tenantsIds | ||
await this.limiter(clusterSize, dbToTenants, tenants => | ||
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => ds.tx({tenant:t}, tx => tx.upgrade(t))) | ||
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => ds.tx({tenant:t}, tx => tx.upgrade(t, options))) | ||
) | ||
@@ -138,3 +138,3 @@ await this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded') | ||
// REVISIT: use jobs service for sync and async operations (might also be interesting for concurrency control) | ||
return tx.enqueue(dbToTenants, 'upgrade', [], error => { | ||
return tx.enqueue(dbToTenants, 'upgrade', [options], error => { | ||
if (error) this._sendCallback('FAILED', 'Tenant upgrade failed') | ||
@@ -141,0 +141,0 @@ else this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded') |
@@ -0,1 +1,2 @@ | ||
const https = require('https') | ||
const { URL } = require('url') | ||
@@ -51,4 +52,6 @@ const axios = require('axios') | ||
const { multitenancy, 'cds.xt.SaasProvisioningService': sps } = cds.env.requires | ||
const { clientid, clientsecret, url } = multitenancy?.credentials ?? sps?.credentials ?? {} | ||
if (!clientid || !clientsecret || !url) { | ||
const { clientid, clientsecret, certurl, url, certificate, key } = multitenancy?.credentials ?? sps?.credentials ?? {} | ||
const auth = certificate ? { maxRedirects: 0, httpsAgent: new https.Agent({ cert: certificate, key }) } | ||
: { auth: { username: clientid, password: clientsecret } } | ||
if (!clientid) { | ||
cds.error('No saas-registry credentials available from the application environment.', { status: 401 }) | ||
@@ -58,16 +61,11 @@ } | ||
try { | ||
LOG.info(`getting saas-registry auth token from ${url}`) | ||
const { data: { access_token } } = await axios(`${url}/oauth/token`, { | ||
const authUrl = `${certurl ?? url}/oauth/token` | ||
LOG.info(`getting saas-registry auth token from ${authUrl}`) | ||
const { data: { access_token } } = await axios(authUrl, { | ||
method: 'POST', | ||
auth: { | ||
username: clientid, | ||
password: clientsecret, | ||
}, | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
...auth, | ||
params: { | ||
grant_type: 'client_credentials', | ||
response_type: 'token' | ||
}, | ||
} | ||
}) | ||
@@ -79,4 +77,4 @@ if (!access_token) { | ||
} catch (error) { | ||
cds.error('Could not get auth token for saas-registry: ' + error.message, { status: 401 }) // REVISIT: Just throw error? | ||
cds.error('Could not get auth token for saas-registry: ' + error.message, { status: 401 }) | ||
} | ||
} |
@@ -33,3 +33,8 @@ const cds = require('@sap/cds/lib') | ||
cds.on('served', () => { if (cds.app) cds.app.get('/-/cds/login/token', token) }) | ||
cds.on('served', () => { | ||
if (cds.app) { | ||
cds.app.post('/-/cds/login/token', token) | ||
cds.app.get('/-/cds/login/token', token) | ||
} | ||
}) | ||
@@ -36,0 +41,0 @@ const { 'cds.xt.ModelProviderService': mps } = cds.services |
@@ -28,4 +28,2 @@ const { URL } = require('url'); | ||
} | ||
assertDefined('query', query); | ||
} | ||
@@ -32,0 +30,0 @@ |
const ClientCredentialsAuthProvider = require('./ClientCredentialsAuthProvider'); | ||
const PasswordAuthProvider = require('./PasswordAuthProvider'); | ||
const RefreshTokenAuthProvider = require('./RefreshTokenAuthProvider'); | ||
const { assertDefined } = require('./util/SecretsUtil'); | ||
module.exports = class AuthProviderFactory { | ||
/** | ||
* Validates all data relevant to the determination of the grant type. | ||
*/ | ||
static #validate(credentials, query) { | ||
assertDefined('query', query); | ||
assertDefined('presence of refresh token or passcode or clientid', !!query.refresh_token || !!query.passcode || !!query.clientid); | ||
} | ||
static getAuthProvider(credentials, query) { | ||
AuthProviderFactory.#validate(credentials, query); | ||
return query.refresh_token | ||
@@ -12,6 +22,4 @@ ? new RefreshTokenAuthProvider(credentials, query) | ||
? new PasswordAuthProvider(credentials, query) | ||
: query.clientid | ||
? new ClientCredentialsAuthProvider(credentials, query) | ||
: new Error('Unknown grant type'); | ||
: new ClientCredentialsAuthProvider(credentials, query); | ||
} | ||
} |
@@ -8,2 +8,18 @@ const util = require('util'); | ||
async function parseBody(request) { | ||
return new Promise((resolve, reject) => { | ||
const chunks = []; | ||
request.on('data', chunk => chunks.push(chunk)); | ||
request.on('end', () => { | ||
try { | ||
const body = Buffer.concat(chunks).toString(); | ||
request.body = Object.fromEntries(new URLSearchParams(body).entries()); | ||
resolve(request.body); | ||
} catch (error) { | ||
reject(error); | ||
} | ||
}); | ||
}); | ||
} | ||
module.exports = async function token(request, response) { | ||
@@ -17,3 +33,5 @@ if (request.method === 'HEAD') { | ||
const { credentials } = cds.env.requires.auth; | ||
const { query } = request; | ||
const query = request.method === 'POST' | ||
? await parseBody(request) | ||
: request.query; | ||
@@ -34,3 +52,3 @@ let authProvider; | ||
...authProvider.clientAuth, | ||
timeout: 10000 | ||
timeout: 1e4 // ms | ||
} | ||
@@ -37,0 +55,0 @@ ); |
@@ -28,10 +28,15 @@ const cds = require('@sap/cds/lib'), { uuid } = cds.utils | ||
await retry(() => cds.tx({ tenant: t0 }, tx => tx.run(INSERT.into(Tasks, tasks))), LOG) | ||
if (tasks.length) { | ||
await retry(() => cds.tx({ tenant: t0 }, tx => tx.run(INSERT.into(Tasks, tasks))), LOG) | ||
const ds = await cds.connect.to(DeploymentService) | ||
const tx = ds.tx(cds.context) | ||
_nextJob(jobs, task => { | ||
const { 'cds.xt.DeploymentService': ds } = cds.services | ||
return ds.tx({ tenant: cds.context.tenant }, tx => tx[op](task.tenant, ...args)) | ||
}, onJobDone).catch(err => LOG.error('next job raised an error', err)) | ||
} else { | ||
await retry(() => cds.tx({ tenant: t0 }, tx => | ||
tx.run(UPDATE(Jobs, { ID: job_ID }).with({ status: FINISHED })) | ||
), LOG) | ||
} | ||
_nextJob(jobs, task => tx[op](task.tenant, ...args), onJobDone) | ||
.catch(err => LOG.error('next job raised an error', err)) | ||
const url = process.env.VCAP_APPLICATION ? 'https://' + JSON.parse(process.env.VCAP_APPLICATION).uris?.[0] : cds.server.url | ||
@@ -38,0 +43,0 @@ cds.context.http?.res.set('Location', `${url}/-/cds/jobs/pollJob(ID='${job_ID}')`) |
@@ -122,3 +122,3 @@ const fs = require('fs').promises | ||
const extensions = !base && await _getExtensions4 (tenant, activated) | ||
const extensions = !base && main.requires.extensibility && await _getExtensions4 (tenant, activated) | ||
if (!extensions && checkExt) req.reject(404, 'Missing extensions') | ||
@@ -125,0 +125,0 @@ |
@@ -18,2 +18,4 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires | ||
const migration = require('../../lib/migration/migration') | ||
if (db?.kind === 'hana') { | ||
@@ -58,3 +60,8 @@ | ||
}) | ||
// check migration before upgrade | ||
ds.before('upgrade', async (req) => { | ||
await migration.checkMigration(req) | ||
}) | ||
}) | ||
@@ -192,4 +199,5 @@ } | ||
if (updateCsvs) { | ||
const _tabledata4 = require('@sap/cds/bin/build/provider/hana/2tabledata') | ||
const tdata = await _tabledata4 (csn, { dirs: [path.join('gen',tenant,'src','gen','data')] }) | ||
const toHdbtabledata = cds.compile.to.hdbtabledata ?? require(path.join(cds.home, 'bin/build/provider/hana/2tabledata')) // cds@6 compatibility | ||
const tdata = await toHdbtabledata(csn, { dirs: [path.join('gen', tenant, 'src', 'gen', 'data')] }) | ||
for (const [data, { file, csvFolder }] of tdata) { | ||
@@ -214,3 +222,3 @@ gen.push (fs.promises.writeFile(path.join(csvFolder,file), JSON.stringify(data))) | ||
DEBUG?.('preparing HANA deployment artifacts') | ||
const _resources = ( csnFromParameter ) ? null : resources4(tenant) | ||
const _resources = csnFromParameter ? null : resources4(tenant) | ||
@@ -217,0 +225,0 @@ let container |
const { join } = require('path') | ||
const HdiDeployUtil = require('@sap/cds/bin/deploy/to-hana/hdiDeployUtil') | ||
const { clean_env } = require('@sap/hdi-deploy/library') | ||
const { deploy, clean_env } = require('@sap/hdi-deploy/library') | ||
const cds = require ('@sap/cds/lib') | ||
const LOG = cds.log('mtx|deploy'), DEBUG = cds.debug('mtx|deploy') | ||
const { fs, mkdirp } = cds.utils | ||
exports.deploy = async (hana, tenant, cwd, options) => { | ||
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx') | ||
const env = _hdi_env4(tenant,hana,options) | ||
@@ -14,7 +13,26 @@ DEBUG?.(`deployment directory: ${cwd}`) | ||
const logPath = join(cds.root, 'logs', `${cds.context.tenant}.log`) | ||
await mkdirp('logs') | ||
const writeStream = fs.createWriteStream(logPath) | ||
await mkdirp('logs') | ||
LOG.info('------------[BEGIN HDI-DEPLOY-OUTPUT]---------------') | ||
try { | ||
await HdiDeployUtil.deployTenant (cwd, env, LOG) | ||
return await new Promise((resolve, reject) => { | ||
deploy(cwd, env, (error, response) => { | ||
if (error) return reject(error) | ||
if (response?.exitCode) { | ||
let message = `HDI deployment failed with exit code ${response.exitCode}` | ||
if (response.signal) message += `. ${response.signal}` | ||
return reject(new Error(message)) | ||
} | ||
return resolve() | ||
}, { | ||
stderrCB: buffer => { | ||
LOG.error(buffer.toString()) | ||
writeStream.write(buffer) | ||
}, | ||
stdoutCB: buffer => { | ||
DEBUG?.(buffer.toString()) | ||
writeStream.write(buffer) | ||
} | ||
}) | ||
}) | ||
} finally { | ||
@@ -61,3 +79,3 @@ LOG.info('-------------[END HDI-DEPLOY-OUTPUT]----------------') | ||
const _parse_env = (key, options) => { | ||
const val = process.env[key]; if (!val) return {} | ||
const val = process.env[key]; if (!val) return { ...options } | ||
try { | ||
@@ -64,0 +82,0 @@ return { ...JSON.parse (val), ...options } |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
213528
64
4194
3