Socket
Socket
Sign inDemoInstall

@sap/cds-mtxs

Package Overview
Dependencies
Maintainers
1
Versions
60
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-mtxs - npm Package Compare versions

Comparing version 1.14.4 to 1.15.0

srv/cf/abstract-provisioning-service.js

21

CHANGELOG.md

@@ -9,2 +9,23 @@ # Change Log

## Version 1.15.0 - 2024-01-30
### Added
- MTXS now supports subscription via Subscription Manager Service also for Node.js applications.
### Fixed
- Additional services needed when using `SERVICE_REPLACEMENTS` for HDI deployment can now also be consumed in Kyma after adding them to the `cds` configuration like
```
"requires": {
"myuserprovided": {
"vcap": {
"label": "user-provided",
"name": "myuserprovided"
}
},
```
See also https://help.sap.com/docs/SAP_HANA_PLATFORM/4505d0bdaf4948449b7f7379d24d0f0d/a4bbc2dd8a20442387dc7b706e8d3070.html#environment-variables-for-hdi-configuration
- Temporary files for build and deployment are created in the OS temp directory if file system permissions do not allow the creation in the cds root directory.
## Version 1.14.4 - 2024-01-24

@@ -11,0 +32,0 @@

15

env.js

@@ -23,2 +23,5 @@ /* eslint-disable quote-props */

kind: "saas-registry",
"[subscription-manager]": {
kind: "subscription-manager",
},
t0: "t0"

@@ -44,2 +47,6 @@ },

},
"cds.xt.SmsProvisioningService": {
model: "@sap/cds-mtxs/srv/cf/sms-provisioning-service",
kind: "subscription-manager",
},
"cds.xt.DeploymentService": {

@@ -80,2 +87,3 @@ model: "@sap/cds-mtxs/srv/deployment-service",

"cds.xt.SaasProvisioningService": false,
"cds.xt.SmsProvisioningService": false,
"cds.xt.DeploymentService": false,

@@ -102,2 +110,6 @@ "cds.xt.ExtensibilityService": false,

"cds.xt.ExtensibilityService": true,
"[subscription-manager]": {
"cds.xt.SmsProvisioningService": true,
"cds.xt.SaasProvisioningService": false,
}
},

@@ -108,4 +120,5 @@ "[development]": {

}
}
},
}

2

package.json
{
"name": "@sap/cds-mtxs",
"version": "1.14.4",
"version": "1.15.0",
"description": "SAP Cloud Application Programming Model - Multitenancy library",

@@ -5,0 +5,0 @@ "homepage": "https://cap.cloud.sap/",

const cds = require('@sap/cds/lib')
const LOG = cds.log('mtx'), DEBUG = cds.debug('mtx')
const { parseHeaders, sendResult } = require('./saas-registry-util')
const migration = require('../../lib/migration/migration')
const DeploymentService = 'cds.xt.DeploymentService'
const JobsService = 'cds.xt.JobsService'
const Tenants = 'cds.xt.Tenants'
const axiosInstance = require('axios').create()
axiosInstance.interceptors.response.use(response => response, require('../../lib/pruneAxiosErrors'))
if (!cds.env.requires.multitenancy) cds.env.requires.multitenancy = true // we need to run in multitenancy mode for t0 ops
const { t0 = 't0' } = cds.env.requires.multitenancy
const isExtensible = !!cds.requires.extensibility || !!cds.requires['cds.xt.ExtensibilityService']
module.exports = class SaasProvisioningService extends require('./abstract-provisioning-service') {
module.exports = class SaasProvisioningService extends cds.ApplicationService {
async init() {
this.on('UPDATE', 'tenant', this.create)
this.on('READ', 'tenant', this.read)
this.on('DELETE', 'tenant', this.delete)
await super.init()
}
_options4(data) {
if (!data?.options && !data._ && !data._application_) return undefined
const _ = data._application_?.sap ? { hdi: { create: data._application_.sap['service-manager'] } } : data._
return { ...data.options, _ }
}
async _tenantsByDb(tenants) {
let tenantToDbUrl
if (cds.requires.db.kind === 'hana') {
const hana = require('../plugins/hana/srv-mgr')
tenantToDbUrl = (await hana.getAll(tenants.length > 0 ? tenants : '*')).reduce((res, t) => {
const id = t.tenant_id ?? t.labels.tenant_id[0]
if (!t.credentials) throw new Error('Credentials for tenant ' + id + ' are not available.')
return { ...res, [id]: `${t.credentials.host}:${t.credentials.port}` }
}, {})
} else {
tenantToDbUrl = tenants.reduce((res, t) => ({ ...res, [t]: cds.db.url4(t) }), {})
async init() {
this.on('UPDATE', 'tenant', this._create)
this.on('READ', 'tenant', super._read)
this.on('DELETE', 'tenant', super._delete)
this.on('getAppUrl', super._getAppUrl)
this.on('dependencies', super._dependencies)
this.on('upgrade', super._upgrade)
this.on('upgradeAll', super._upgradeAll)
await super.init()
}
const dbToTenants = {}
for (const tenant of Object.keys(tenantToDbUrl)) {
const dbUrl = tenantToDbUrl[tenant]
if (!dbToTenants[dbUrl]) dbToTenants[dbUrl] = new Set
dbToTenants[dbUrl].add(tenant)
}
return dbToTenants
}
getAppUrl(subscriptionPayload, subscriptionHeaders) {
return subscriptionHeaders?.application_url
?? process.env.SUBSCRIPTION_URL?.replace(`\${tenant_subdomain}`, subscriptionPayload.subscribedSubdomain)
?? 'Tenant successfully subscribed - no application URL provided'
}
_getSubscribedTenant(context) {
const { data, params } = context ?? {}
const { subscribedTenantId } = data ?? {}
return subscribedTenantId ?? params?.[0]?.subscribedTenantId
}
async create(context) {
const { headers, data, http } = context
DEBUG?.('received subscription request with', { data })
const options = this._options4(data)
// REVISIT: removing as they are polluting logs -> clearer data/options separation
delete data._; delete data._application_; delete data.options
const tenant = this._getSubscribedTenant(context)
const { isSync } = parseHeaders(http?.req.headers)
const sps = await cds.connect.to('cds.xt.SaasProvisioningService')
const appUrl = await sps.getAppUrl(data, headers)
if (isSync) {
LOG.info(`subscribing tenant ${tenant}`)
try {
const ds = await cds.connect.to(DeploymentService)
const tx = ds.tx(context)
await tx.subscribe(tenant, data, options)
await this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)
cds.context.http.res.set('content-type', 'text/plain')
} catch (error) {
await this._sendCallback('FAILED', 'Tenant creation failed')
throw error
}
return appUrl
} else {
const { lazyT0 } = cds.requires['cds.xt.DeploymentService'] ?? cds.requires.multitenancy ?? {}
if (lazyT0) {
await require('../plugins/common').resubscribeT0IfNeeded(options?._)
}
const js = await cds.connect.to(JobsService)
const tx = js.tx(context)
return tx.enqueue('subscribe', [new Set([tenant])], { data, options }, error => {
if (error) this._sendCallback('FAILED', 'Tenant creation failed')
else this._sendCallback('SUCCEEDED', 'Tenant creation succeeded', appUrl)
})
async _create(context) {
return super._create(context, context.data)
}
}
async read(context) {
const tenant = this._getSubscribedTenant(context)
if (tenant) {
const one = await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.one.from(Tenants).columns(['metadata', 'createdAt', 'modifiedAt']).where({ ID: tenant }))
)
if (!one) cds.error(`Tenant ${tenant} not found`, { status: 404 })
const { metadata, createdAt, modifiedAt } = one
return { subscribedTenantId: tenant, ...JSON.parse(metadata ?? '{}'), createdAt, modifiedAt }
}
return (await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.from(Tenants).columns(['ID', 'metadata', 'createdAt', 'modifiedAt']))
)).map(({ ID, metadata, createdAt, modifiedAt }) => ({ subscribedTenantId: ID, ...JSON.parse(metadata), createdAt, modifiedAt }))
}
async _getTenants() {
const tenants = (await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.from(Tenants, tenant => { tenant.ID }))
)).map(({ ID }) => ID)
const mtxTenants = await migration.getMissingMtxTenants(tenants)
return [...tenants, ...mtxTenants]
}
async upgrade(tenantIds, options) {
if (!tenantIds?.length) return
const all = tenantIds.includes('*')
const sharedGenDir = !isExtensible
if (sharedGenDir) (options ??= {}).skipResources ??= sharedGenDir
const tenants = all ? await this._getTenants() : tenantIds
if (sharedGenDir && cds.requires.db.kind === 'hana') { // REVISIT: Ideally part of HANA plugin
const { resources4, csvs4 } = require('../plugins/hana')
await resources4('base')
await csvs4('base')
}
const { isSync } = parseHeaders(cds.context.http?.req.headers)
const {
clusterSize = 1, workerSize = 1, poolSize = 1
} = cds.env.requires.multitenancy.jobs ?? cds.env.requires['cds.xt.SaasProvisioningService']?.jobs ?? {}
const dbToTenants = clusterSize > 1 ? await this._tenantsByDb(tenants) : [new Set(tenants)]
LOG.info('upgrading', { tenants })
if (isSync) {
try {
const ds = await cds.connect.to(DeploymentService)
await this.limiter(clusterSize, Object.values(dbToTenants), tenants =>
this.limiter(workerSize ?? poolSize, Array.from(tenants), t => ds.tx({tenant:t}, tx => tx.upgrade(t, options)))
)
await this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded')
} catch (error) {
await this._sendCallback('FAILED', 'Tenant upgrade failed')
throw error
_parseHeaders(headers) {
const { prefer, status_callback, mtx_status_callback } = headers ?? {}
const { multitenancy, 'cds.xt.SaasProvisioningService': sps } = cds.env.requires
const { saas_registry_url } = multitenancy?.credentials ?? sps?.credentials ?? {}
const callbackUrl = mtx_status_callback ?? (status_callback && saas_registry_url && new URL(status_callback, saas_registry_url).toString())
return {
callbackUrl,
isCustomCallback: !!mtx_status_callback,
saasCallbackUrlPath: status_callback,
isSync: !(prefer?.includes('respond-async') || callbackUrl)
}
} else {
const js = await cds.connect.to(JobsService)
const tx = js.tx(cds.context)
// REVISIT: use jobs service for sync and async operations (might also be interesting for concurrency control)
return tx.enqueue('upgrade', dbToTenants, { options }, error => {
if (error) this._sendCallback('FAILED', 'Tenant upgrade failed')
else this._sendCallback('SUCCEEDED', 'Tenant upgrade succeeded')
})
}
}
async delete(context) {
DEBUG?.('received unsubscription request', context.data)
const { isSync } = parseHeaders(context.http?.req.headers)
async _sendCallback(status, message, subscriptionUrl) {
const originalRequest = cds.context?.http?.req
const { isSync, isCustomCallback, saasCallbackUrlPath, callbackUrl } = this._parseHeaders(originalRequest?.headers)
if (!isSync && callbackUrl) {
const tenant = this._getSubscribedTenant(originalRequest.body)
const payload = { status, message, subscriptionUrl }
const tenant = this._getSubscribedTenant(context)
LOG.info(`unsubscribing tenant ${tenant}`)
if (tenant === t0) {
const ds = await cds.connect.to(DeploymentService)
const tx = ds.tx(context)
return tx.unsubscribe(tenant)
}
const one = await cds.tx({ tenant: t0 }, tx =>
tx.run(SELECT.one.from(Tenants, { ID: tenant }, t => { t.metadata }))
) ?? {}
const metadata = JSON.parse(one?.metadata ?? '{}')
if (isSync) {
const ds = await cds.connect.to(DeploymentService)
const tx = ds.tx(context)
try {
await tx.unsubscribe(tenant, { metadata })
await this._sendCallback('SUCCEEDED', 'Tenant deletion succeeded')
} catch (error) {
if (error.statusCode === 404) {
LOG.info(`tenant ${tenant} is currently not subscribed`)
} else {
await this._sendCallback('FAILED', 'Tenant deletion failed')
throw error
// additional payload for internal callback (java)
let customPayload
if (isCustomCallback) {
customPayload = {
saasRequestPayload: originalRequest.body,
saasCallbackUrl: saasCallbackUrlPath,
tenant
}
}
DEBUG?.(`send callback to ${callbackUrl}`)
try {
const authHeader = isCustomCallback ? originalRequest.headers.authorization : `Bearer ${await this._saasRegistryToken()}`
await this.sendResult(callbackUrl, payload, customPayload, authHeader)
} catch (error) {
LOG.error(error)
}
}
}
} else {
const lcs = await cds.connect.to(JobsService)
const tx = lcs.tx(context)
return tx.enqueue('unsubscribe', [new Set([tenant])], { metadata }, error => {
if (error) this._sendCallback('FAILED', 'Tenant deletion failed')
else this._sendCallback('SUCCEEDED', 'Tenant deletion succeeded')
})
}
}
upgradeAll(tenants) {
LOG.warn(`upgradeAll is deprecated. Use /-/cds/saas-provisioning/upgrade instead.`)
return this.upgrade(tenants ?? ['*'])
}
dependencies() {
return cds.env.requires['cds.xt.SaasProvisioningService']?.dependencies?.map(d => ({ xsappname: d })) ?? []
}
async limiter(limit, payloads, fn) {
const pending = [], all = []
for (const payload of payloads) {
const execute = Promise.resolve().then(() => fn(payload))
all.push(execute)
const executeAndRemove = execute.then(() => pending.splice(pending.indexOf(executeAndRemove), 1))
pending.push(executeAndRemove)
if (pending.length >= limit) {
await Promise.race(pending) // eslint-disable-line no-await-in-loop
async _saasRegistryToken() {
const { multitenancy, 'cds.xt.SaasProvisioningService': sps } = cds.env.requires
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 })
}
}
return Promise.allSettled(all)
}
async _sendCallback(status, message, subscriptionUrl) {
const originalRequest = cds.context?.http?.req
const { isSync, isCustomCallback, saasCallbackUrlPath, callbackUrl } = parseHeaders(originalRequest?.headers)
if (!isSync && callbackUrl) {
const tenant = this._getSubscribedTenant(originalRequest.body)
const payload = { status, message, subscriptionUrl }
// additional payload for internal callback (java)
if (isCustomCallback) {
Object.assign(payload, {
saasRequestPayload: originalRequest.body,
saasCallbackUrl: saasCallbackUrlPath
try {
const authUrl = `${certurl ?? url}/oauth/token`
LOG.info(`getting saas-registry auth token from ${authUrl}`)
const { data: { access_token } } = await axiosInstance(authUrl, {
method: 'POST',
...auth,
params: {
grant_type: 'client_credentials',
response_type: 'token'
}
})
}
DEBUG?.(`send callback to ${callbackUrl}`)
try {
await sendResult(callbackUrl, tenant, payload, isCustomCallback ? originalRequest.headers.authorization : undefined)
if (!access_token) {
cds.error('Could not get saas-registry token: token is empty', { status: 401 })
}
return access_token
} catch (error) {
LOG.error(error)
cds.error('Could not get auth token for saas-registry: ' + error.message, { status: 401 })
}
}
}
}

@@ -165,3 +165,2 @@ const crypto = require('crypto')

)
csn = cds.reflect(csn)

@@ -180,5 +179,5 @@ if (extensions) {

* @template T
* @param {Map} cache – The cache.
* @param {string} key - Unique string for cache look-up.
* @param {() => T} fn - Function producing result to be cached.
* @param {Map} cache The cache.
* @param {string} key Unique string for cache look-up.
* @param {() => T} fn Function producing result to be cached.
* @returns {T}

@@ -185,0 +184,0 @@ */

@@ -5,6 +5,8 @@ const cds = require('@sap/cds/lib'), {db} = cds.env.requires

const { retry } = require('../../lib/utils')
module.exports = exports = { resources4, build, _imCreateParams }
const TEMP_DIR = require('fs').realpathSync(require('os').tmpdir())
module.exports = exports = { resources4, build, _imCreateParams, _buildResultDir }
const { cacheBindings = true, t0 = 't0' } = cds.requires.multitenancy ?? {}
const { accessSync, constants: fsConstants, existsSync } = require('fs')
const { mkdirp } = cds.utils
const TEMP_DIR = require('fs').realpathSync(require('os').tmpdir())
const { readData } = require('../extensibility/utils')

@@ -132,3 +134,3 @@

const { 'cds.xt.ModelProviderService': mp } = cds.services
const out = await fs.mkdirp ('gen',tenant)
const out = await fs.mkdirp (await _buildResultDir(),tenant)
try {

@@ -154,3 +156,3 @@ const rscs = await mp.getResources(true)

if (!csvs) return
const out = await fs.mkdirp ('gen',tenant,'src','gen','data'), gen = []
const out = await fs.mkdirp (await _buildResultDir(),tenant,'src','gen','data'), gen = []
for (const [filename,csv] of Object.entries(csvs)) {

@@ -181,3 +183,3 @@ // store files in src/gen/data

async function build (csn, tenant, updateCsvs) {
const out = await fs.mkdirp('gen',tenant,'src','gen'), gen = []
const out = await fs.mkdirp(await _buildResultDir(),tenant,'src','gen'), gen = []
const options = { messages: [], sql_mapping: cds.env.sql.names, assertIntegrity:false }

@@ -216,2 +218,14 @@ const { definitions: hanaArtifacts } = cds.compiler.to.hdi.migration(csn, options);

async function _buildResultDir() {
const defaultDir = path.join(cds.root, 'gen')
try {
if (!existsSync(defaultDir)) await mkdirp(defaultDir)
return defaultDir
} catch (e) {
if (e.code !== 'EACCES') throw e
LOG?.(`Using temporary directory ${TEMP_DIR} for build result`)
return path.join(TEMP_DIR, 'gen')
}
}
async function _deploy (req, _container, { skipExt = false, skipResources = false } = {}) {

@@ -225,3 +239,3 @@ const { tenant, options: { _: params = {}, csn: csnFromParameter } = {} } = req.data

const out = await fs.mkdirp ('gen', skipResources ? 'base' : tenant)
const out = await fs.mkdirp (await _buildResultDir(), skipResources ? 'base' : tenant)

@@ -228,0 +242,0 @@ DEBUG?.('preparing HANA deployment artifacts')

@@ -6,10 +6,24 @@ const { join } = require('path')

const { fs, mkdirp } = cds.utils
const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
exports._logDirectory = async () => {
const defaultDir = join(cds.root, 'logs')
try {
await mkdirp(defaultDir)
return defaultDir
} catch (e) {
if (e.code !== 'EACCES') throw e
LOG?.(`Using temporary directory ${TEMP_DIR} for deployment logs`)
return join(TEMP_DIR, 'logs')
}
}
exports.deploy = async (hana, tenant, cwd, options) => {
const env = _hdi_env4(tenant,hana,options)
const env = exports._hdi_env4(tenant,hana,options)
DEBUG?.(`deployment directory: ${cwd}`)
DEBUG?.(`effective HDI options:`, env.HDI_DEPLOY_OPTIONS)
const logPath = join(cds.root, 'logs', `${cds.context.tenant}.log`)
await mkdirp('logs')
const logDir = await exports._logDirectory()
const logPath = join(logDir, `${cds.context.tenant}.log`)
await mkdirp(logDir)
const writeStream = fs.createWriteStream(logPath)

@@ -45,3 +59,3 @@ DEBUG?.('------------[BEGIN HDI-DEPLOY-OUTPUT]---------------')

const _hdi_env4 = (t,container,options)=>{
exports._hdi_env4 = (t,container,options)=>{
const env = { ...clean_env(process.env), TARGET_CONTAINER:t }

@@ -51,6 +65,8 @@ env.SERVICE_REPLACEMENTS = process.env.SERVICE_REPLACEMENTS

const { hana=[], 'user-provided':up } = _parse_env ('VCAP_SERVICES')
env.VCAP_SERVICES = JSON.stringify ({
const vcapFromEnv = {
hana:[ { ...container, name:t, tenant_id:t }, ...hana ],
'user-provided':up
})
}
const emulatedVcap = _emulated_vcap_services()
env.VCAP_SERVICES = JSON.stringify ({ ...vcapFromEnv, ...emulatedVcap})

@@ -71,1 +87,26 @@ const hdi_opts = _parse_env ('HDI_DEPLOY_OPTIONS', options)

}
/**
* Build VCAP_SERVICES for compatibility (for example for CloudSDK) or for running
* locally with credentials (hybrid mode).
* Copied from @sap/cds/lib/env/cds-env.js#L333
*/
function _emulated_vcap_services() {
const vcap_services = {}, names = new Set()
for (const service in cds.env.requires) {
let { vcap, credentials, binding } = cds.env.requires[service]
// "binding.vcap" is chosen over "vcap" because it is meta data resolved from the real service (-> cds bind)
if (binding && binding.vcap) vcap = binding.vcap
if (vcap && vcap.label && credentials && Object.keys(credentials).length > 0) {
// Only one entry for a (instance) name. Generate name from label and plan if not given.
const { label, plan } = vcap
const name = vcap.name || `instance:${label}:${plan || ""}`
if (names.has(name)) continue
names.add(name)
if (!vcap_services[label]) vcap_services[label] = []
vcap_services[label].push(Object.assign({ name }, vcap, { credentials }))
}
}
return vcap_services
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc