Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## v1.11.0 - 2025-07-09

### Added

- Added `authInfo` to cds.User as CDS 9.3 deprecated `tokenInfo`.

## v1.10.10 - 2025-07-09

### Fixed
Expand Down
315 changes: 156 additions & 159 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@
},
"dependencies": {
"@sap/xssec": "^4.6.0",
"cron-parser": "^5.2.0",
"cron-parser": "^5.3.1",
"redis": "^4.7.0",
"verror": "^1.10.1",
"yaml": "^2.7.1"
},
"devDependencies": {
"@cap-js/cds-test": "^0.4.0",
"@cap-js/hana": "^2.1.0",
"@cap-js/hana": "^2.2.0",
"@cap-js/sqlite": "^2.0.1",
"@sap/cds": "^9.1.0",
"@sap/cds-dk": "^9.1.0",
"@sap/cds": "^9.3.1",
"@sap/cds-dk": "^9.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.6.0",
Expand Down
10 changes: 5 additions & 5 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class Config {
#redisNamespace;
#publishEventBlockList;
#crashOnRedisUnavailable;
#tenantIdFilterTokenInfoCb;
#tenantIdFilterAuthContextCb;
#tenantIdFilterEventProcessingCb;
#configEvents;
#configPeriodicEvents;
Expand Down Expand Up @@ -770,12 +770,12 @@ class Config {
this.#crashOnRedisUnavailable = value;
}

get tenantIdFilterTokenInfo() {
return this.#tenantIdFilterTokenInfoCb;
get tenantIdFilterAuthContext() {
return this.#tenantIdFilterAuthContextCb;
}

set tenantIdFilterTokenInfo(value) {
this.#tenantIdFilterTokenInfoCb = value;
set tenantIdFilterAuthContext(value) {
this.#tenantIdFilterAuthContextCb = value;
}

get tenantIdFilterEventProcessing() {
Expand Down
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ module.exports = {
},
TenantIdCheckTypes: {
eventProcessing: "eventProcessing",
getTokenInfo: "getTokenInfo",
getAuthContext: "getAuthContext",
},
};
6 changes: 3 additions & 3 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export declare type EventProcessingStatusType = (typeof EventProcessingStatus)[E

export declare const TenantIdCheckTypes: {
eventProcessing: "eventProcessing";
getTokenInfo: "getTokenInfo";
getAuthContext: "getAuthContext";
};

export declare const TransactionMode: {
Expand Down Expand Up @@ -215,8 +215,8 @@ declare class Config {
get publishEventBlockList(): any;
set crashOnRedisUnavailable(value: any);
get crashOnRedisUnavailable(): any;
set tenantIdFilterTokenInfo(value: any);
get tenantIdFilterTokenInfo(): any;
set tenantIdFilterAuthContext(value: any);
get tenantIdFilterAuthContext(): any;
set tenantIdFilterEventProcessing(value: any);
get tenantIdFilterEventProcessing(): any;
set runInterval(value: any);
Expand Down
4 changes: 3 additions & 1 deletion src/outbox/EventQueueGenericOutboxHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,9 +327,11 @@ class EventQueueGenericOutboxHandler extends EventQueueBaseClass {
}

async #setContextUser(context, userId, reg) {
const authInfo = await common.getAuthContext(context.tenant);
context.user = new cds.User.Privileged({
id: userId,
tokenInfo: await common.getTokenInfo(this.baseContext.tenant),
authInfo,
tokenInfo: authInfo?.token,
});
if (reg) {
reg.user = context.user;
Expand Down
3 changes: 2 additions & 1 deletion src/redis/redisPub.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ const _processLocalWithoutRedis = async (tenantId, events) => {
let context = {};
if (tenantId) {
const user = await cds.tx({ tenant: tenantId }, async () => {
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
const authInfo = await common.getAuthContext(tenantId);
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo.token });
});
context = {
tenant: tenantId,
Expand Down
3 changes: 2 additions & 1 deletion src/redis/redisSub.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ const _messageHandlerProcessEvents = async (messageData) => {
}

const user = await cds.tx({ tenant: tenantId }, async () => {
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
const authInfo = await common.getAuthContext(tenantId);
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
});
const tenantContext = {
tenant: tenantId,
Expand Down
16 changes: 10 additions & 6 deletions src/runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,11 @@ const _executeEventsAllTenantsRedis = async (tenantIds) => {
tx.context,
"get-openEvents-and-publish",
async () => {
const authInfo = await common.getAuthContext(tenantId);
tx.context.user = new cds.User.Privileged({
id: config.userId,
tokenInfo: await common.getTokenInfo(tenantId),
authInfo,
tokenInfo: authInfo?.token,
});
const entries = await openEvents.getOpenQueueEntries(tx, false);
logger.info("broadcasting events for run", {
Expand Down Expand Up @@ -187,10 +189,11 @@ const _executeEventsAllTenants = async (tenantIds, runId) => {
try {
events = await trace(
{ id, tenant: tenantId },
"fetch-openEvents-and-tokenInfo",
"fetch-openEvents-and-authInfo",
async () => {
const user = await cds.tx({ tenant: tenantId }, async () => {
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
const authInfo = await common.getAuthContext(tenantId);
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
});
tenantContext = {
tenant: tenantId,
Expand Down Expand Up @@ -258,7 +261,8 @@ const _executePeriodicEventsAllTenants = async (tenantIds) => {
for (const tenantId of tenantIds) {
try {
const user = await cds.tx({ tenant: tenantId }, async () => {
return new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(tenantId) });
const authInfo = await common.getAuthContext(tenantId);
return new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
});
const tenantContext = {
tenant: tenantId,
Expand Down Expand Up @@ -289,7 +293,7 @@ const _singleTenantDb = async () => {
const id = cds.utils.uuid();
const events = await trace(
{ id },
"fetch-openEvents-and-tokenInfo",
"fetch-openEvents-and-authInfo",
async () => {
return await cds.tx({}, async (tx) => {
return await openEvents.getOpenQueueEntries(tx);
Expand Down Expand Up @@ -530,7 +534,7 @@ const _checkPeriodicEventsSingleTenant = async (context) => {
try {
logger.info("executing updating periodic events", {
tenantId: context.tenant,
subdomain: context.user?.tokenInfo?.extAttributes?.zdn,
subdomain: context.user?.authInfo?.getSubdomain?.(),
});
await periodicEvents.checkAndInsertPeriodicEvents(context);
} catch (err) {
Expand Down
3 changes: 2 additions & 1 deletion src/shared/cdsHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ async function executeInNewTransaction(context = {}, transactionTag, fn, args, {
const logger = cds.log(COMPONENT_NAME);
let transactionRollbackPromise = Promise.resolve(false);
try {
const user = new cds.User.Privileged({ id: config.userId, tokenInfo: await common.getTokenInfo(context.tenant) });
const authInfo = await common.getAuthContext(context.tenant);
const user = new cds.User.Privileged({ id: config.userId, authInfo, tokenInfo: authInfo?.token });
if (cds.db.kind === "hana") {
await cds.tx(
{
Expand Down
49 changes: 19 additions & 30 deletions src/shared/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const xssec = require("@sap/xssec");
const VError = require("verror");

const config = require("../config");
const { ExpiringLazyCache } = require("./lazyCache");
const { TenantIdCheckTypes } = require("../constants");

const MARGIN_AUTH_INFO_EXPIRY = 60 * 1000;
Expand Down Expand Up @@ -87,64 +88,52 @@ const processChunkedSync = (inputs, chunkSize, chunkHandler) => {

const hashStringTo32Bit = (value) => crypto.createHash("sha256").update(String(value)).digest("base64").slice(0, 32);

const _getNewTokenInfo = async (tenantId) => {
const tokenInfoCache = getTokenInfo._tokenInfoCache;
tokenInfoCache[tenantId] = tokenInfoCache[tenantId] ?? {};
const _getNewAuthContext = async (tenantId) => {
try {
if (!_getNewTokenInfo._xsuaaService) {
_getNewTokenInfo._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
if (!_getNewAuthContext._xsuaaService) {
_getNewAuthContext._xsuaaService = new xssec.XsuaaService(cds.requires.auth.credentials);
}
const authService = _getNewTokenInfo._xsuaaService;
const authService = _getNewAuthContext._xsuaaService;
const token = await authService.fetchClientCredentialsToken({ zid: tenantId });
const tokenInfo = new xssec.XsuaaToken(token.access_token);
tokenInfoCache[tenantId].expireTs = tokenInfo.getExpirationDate().getTime() - MARGIN_AUTH_INFO_EXPIRY;
return tokenInfo;
const authInfo = new xssec.XsuaaSecurityContext(authService, tokenInfo);
return [tokenInfo.getExpirationDate().getTime() - Date.now(), authInfo];
} catch (err) {
tokenInfoCache[tenantId] = null;
cds.log(COMPONENT_NAME).warn("failed to request tokenInfo", {
cds.log(COMPONENT_NAME).warn("failed to request authContext", {
err: err.message,
responseCode: err.responseCode,
responseText: err.responseText,
});
return [0, null];
}
};

const getTokenInfo = async (tenantId) => {
if (!(await isTenantIdValidCb(TenantIdCheckTypes.getTokenInfo, tenantId))) {
const getAuthContext = async (tenantId) => {
if (!(await isTenantIdValidCb(TenantIdCheckTypes.getAuthContext, tenantId))) {
return null;
}

if (!cds.requires?.auth?.credentials) {
return null; // no credentials not tokenInfo
return null; // no credentials not authContext
}

if (!config.isMultiTenancy) {
return null; // does only make sense for multi tenancy
}

if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i)) {
if (!cds.requires?.auth.kind.match(/jwt|xsuaa/i) && !cds.requires?.xsuaa) {
return null;
}

getTokenInfo._tokenInfoCache = getTokenInfo._tokenInfoCache ?? {};
const tokenInfoCache = getTokenInfo._tokenInfoCache;
// not existing or existing but expired
if (
!tokenInfoCache[tenantId] ||
(tokenInfoCache[tenantId] && tokenInfoCache[tenantId].expireTs && Date.now() > tokenInfoCache[tenantId].expireTs)
) {
tokenInfoCache[tenantId] ??= {};
tokenInfoCache[tenantId].value = _getNewTokenInfo(tenantId);
tokenInfoCache[tenantId].expireTs = null;
}
return await tokenInfoCache[tenantId].value;
getAuthContext._cache = getAuthContext._cache ?? new ExpiringLazyCache({ expirationGap: MARGIN_AUTH_INFO_EXPIRY });
return await getAuthContext._cache.getSetCb(tenantId, async () => _getNewAuthContext(tenantId));
};

const isTenantIdValidCb = async (checkType, tenantId) => {
let cb;
switch (checkType) {
case TenantIdCheckTypes.getTokenInfo:
cb = config.tenantIdFilterTokenInfo;
case TenantIdCheckTypes.getAuthContext:
cb = config.tenantIdFilterAuthContext;
break;
case TenantIdCheckTypes.eventProcessing:
cb = config.tenantIdFilterEventProcessing;
Expand All @@ -167,10 +156,10 @@ module.exports = {
isValidDate,
processChunkedSync,
hashStringTo32Bit,
getTokenInfo,
getAuthContext,
isTenantIdValidCb,
promiseAllDone,
__: {
clearTokenInfoCache: () => (getTokenInfo._tokenInfoCache = {}),
clearAuthContextCache: () => getAuthContext._cache?.clear(),
},
};
Loading