Skip to content

Commit 90f71c4

Browse files
authored
feat: Add skipAuth flag (#4080)
* feat: refactor headers code & add skipAuth flag * feat: add rate limiting to companion requests * test: always mock companion api requests before each test case
1 parent 7fca8bb commit 90f71c4

File tree

4 files changed

+109
-121
lines changed

4 files changed

+109
-121
lines changed
Lines changed: 97 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import express from 'express';
2+
import rateLimit from 'express-rate-limit';
23
import { TokenManager } from './TokenManager';
34
import { pipeline } from 'stream/promises';
45
import { Readable } from 'stream';
@@ -10,51 +11,79 @@ const config = require('../config.js');
1011
const tokenManager = new TokenManager();
1112
const COMPANION_API_BASE_URL = `${config.features?.KYMA_COMPANION?.config
1213
?.apiBaseUrl ?? ''}/api/conversations/`;
14+
const SKIP_AUTH = config.features?.KYMA_COMPANION?.config?.skipAuth ?? false;
1315
const router = express.Router();
1416

17+
// Rate limiter: Max 200 requests per 1 minutes per IP
18+
const companionRateLimiter = rateLimit({
19+
windowMs: 1 * 60 * 1000,
20+
max: 200,
21+
message: 'Too many requests, please try again later.',
22+
standardHeaders: true,
23+
legacyHeaders: false,
24+
});
25+
1526
router.use(express.json());
1627

28+
function extractAuthHeaders(req) {
29+
return {
30+
clusterUrl: req.headers['x-cluster-url'],
31+
certificateAuthorityData:
32+
req.headers['x-cluster-certificate-authority-data'],
33+
clusterToken: req.headers['x-k8s-authorization']?.replace(
34+
/^Bearer\s+/i,
35+
'',
36+
),
37+
clientCertificateData: req.headers['x-client-certificate-data'],
38+
clientKeyData: req.headers['x-client-key-data'],
39+
sessionId: req.headers['session-id'],
40+
};
41+
}
42+
43+
async function buildApiHeaders(authData, contentType = 'application/json') {
44+
const headers = {
45+
Accept: contentType,
46+
'Content-Type': 'application/json',
47+
'X-Cluster-Certificate-Authority-Data': authData.certificateAuthorityData,
48+
'X-Cluster-Url': authData.clusterUrl,
49+
};
50+
51+
if (!SKIP_AUTH) {
52+
const AUTH_TOKEN = await tokenManager.getToken();
53+
headers.Authorization = `Bearer ${AUTH_TOKEN}`;
54+
}
55+
56+
if (authData.sessionId) {
57+
headers['Session-Id'] = authData.sessionId;
58+
}
59+
60+
if (authData.clusterToken) {
61+
headers['X-K8s-Authorization'] = authData.clusterToken;
62+
} else if (authData.clientCertificateData && authData.clientKeyData) {
63+
headers['X-Client-Certificate-Data'] = authData.clientCertificateData;
64+
headers['X-Client-Key-Data'] = authData.clientKeyData;
65+
} else {
66+
throw new Error('Missing authentication credentials');
67+
}
68+
69+
return headers;
70+
}
71+
1772
async function handlePromptSuggestions(req, res) {
1873
const { namespace, resourceType, groupVersion, resourceName } = JSON.parse(
1974
req.body.toString(),
2075
);
21-
const clusterUrl = req.headers['x-cluster-url'];
22-
const certificateAuthorityData =
23-
req.headers['x-cluster-certificate-authority-data'];
24-
const clusterToken = req.headers['x-k8s-authorization']?.replace(
25-
/^Bearer\s+/i,
26-
'',
27-
);
28-
const clientCertificateData = req.headers['x-client-certificate-data'];
29-
const clientKeyData = req.headers['x-client-key-data'];
76+
const authData = extractAuthHeaders(req);
77+
const endpointUrl = COMPANION_API_BASE_URL;
78+
const payload = {
79+
resource_kind: resourceType,
80+
resource_api_version: groupVersion,
81+
resource_name: resourceName,
82+
namespace: namespace,
83+
};
3084

3185
try {
32-
const endpointUrl = COMPANION_API_BASE_URL;
33-
const payload = {
34-
resource_kind: resourceType,
35-
resource_api_version: groupVersion,
36-
resource_name: resourceName,
37-
namespace: namespace,
38-
};
39-
40-
const AUTH_TOKEN = await tokenManager.getToken();
41-
42-
const headers = {
43-
Accept: 'application/json',
44-
'Content-Type': 'application/json',
45-
Authorization: `Bearer ${AUTH_TOKEN}`,
46-
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
47-
'X-Cluster-Url': clusterUrl,
48-
};
49-
50-
if (clusterToken) {
51-
headers['X-K8s-Authorization'] = clusterToken;
52-
} else if (clientCertificateData && clientKeyData) {
53-
headers['X-Client-Certificate-Data'] = clientCertificateData;
54-
headers['X-Client-Key-Data'] = clientKeyData;
55-
} else {
56-
throw new Error('Missing authentication credentials');
57-
}
86+
const headers = await buildApiHeaders(authData);
5887

5988
const response = await fetch(endpointUrl, {
6089
method: 'POST',
@@ -85,61 +114,33 @@ async function handleChatMessage(req, res) {
85114
resourceName,
86115
} = JSON.parse(req.body.toString());
87116

88-
const clusterUrl = req.headers['x-cluster-url'];
89-
const certificateAuthorityData =
90-
req.headers['x-cluster-certificate-authority-data'];
91-
const clusterToken = req.headers['x-k8s-authorization']?.replace(
92-
/^Bearer\s+/i,
93-
'',
117+
const authData = extractAuthHeaders(req);
118+
const conversationId = authData.sessionId;
119+
120+
const endpointUrl = new URL(
121+
`${encodeURIComponent(conversationId)}/messages`,
122+
COMPANION_API_BASE_URL,
94123
);
95-
const clientCertificateData = req.headers['x-client-certificate-data'];
96-
const clientKeyData = req.headers['x-client-key-data'];
97-
const sessionId = req.headers['session-id'];
98-
const conversationId = sessionId;
124+
125+
const payload = {
126+
query,
127+
resource_kind: resourceType,
128+
resource_api_version: groupVersion,
129+
resource_name: resourceName,
130+
namespace: namespace,
131+
};
99132

100133
try {
101134
const uuidPattern = /^[a-f0-9]{32}$/i;
102135
if (!uuidPattern.test(conversationId)) {
103136
throw new Error('Invalid session ID ');
104137
}
105-
106-
const endpointUrl = new URL(
107-
`${encodeURIComponent(conversationId)}/messages`,
108-
COMPANION_API_BASE_URL,
109-
);
110-
111-
const payload = {
112-
query,
113-
resource_kind: resourceType,
114-
resource_api_version: groupVersion,
115-
resource_name: resourceName,
116-
namespace: namespace,
117-
};
118-
119-
const AUTH_TOKEN = await tokenManager.getToken();
120-
121138
// Set up headers for streaming response
122139
res.setHeader('Content-Type', 'text/event-stream');
123140
res.setHeader('Cache-Control', 'no-cache');
124141
res.setHeader('Connection', 'keep-alive');
125142

126-
const headers = {
127-
Accept: 'text/event-stream',
128-
'Content-Type': 'application/json',
129-
Authorization: `Bearer ${AUTH_TOKEN}`,
130-
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
131-
'X-Cluster-Url': clusterUrl,
132-
'Session-Id': sessionId,
133-
};
134-
135-
if (clusterToken) {
136-
headers['X-K8s-Authorization'] = clusterToken;
137-
} else if (clientCertificateData && clientKeyData) {
138-
headers['X-Client-Certificate-Data'] = clientCertificateData;
139-
headers['X-Client-Key-Data'] = clientKeyData;
140-
} else {
141-
throw new Error('Missing authentication credentials');
142-
}
143+
const headers = await buildApiHeaders(authData, 'text/event-stream');
143144

144145
const response = await fetch(endpointUrl, {
145146
method: 'POST',
@@ -190,43 +191,16 @@ async function handleChatMessage(req, res) {
190191
}
191192

192193
async function handleFollowUpSuggestions(req, res) {
193-
const clusterUrl = req.headers['x-cluster-url'];
194-
const certificateAuthorityData =
195-
req.headers['x-cluster-certificate-authority-data'];
196-
const clusterToken = req.headers['x-k8s-authorization']?.replace(
197-
/^Bearer\s+/i,
198-
'',
194+
const authData = extractAuthHeaders(req);
195+
const conversationId = authData.sessionId;
196+
197+
const endpointUrl = new URL(
198+
`${encodeURIComponent(conversationId)}/questions`,
199+
COMPANION_API_BASE_URL,
199200
);
200-
const clientCertificateData = req.headers['x-client-certificate-data'];
201-
const clientKeyData = req.headers['x-client-key-data'];
202-
const sessionId = req.headers['session-id'];
203-
const conversationId = sessionId;
204201

205202
try {
206-
const endpointUrl = new URL(
207-
`${encodeURIComponent(conversationId)}/questions`,
208-
COMPANION_API_BASE_URL,
209-
);
210-
211-
const AUTH_TOKEN = await tokenManager.getToken();
212-
213-
const headers = {
214-
Accept: 'application/json',
215-
'Content-Type': 'application/json',
216-
Authorization: `Bearer ${AUTH_TOKEN}`,
217-
'X-Cluster-Certificate-Authority-Data': certificateAuthorityData,
218-
'X-Cluster-Url': clusterUrl,
219-
'Session-Id': sessionId,
220-
};
221-
222-
if (clusterToken) {
223-
headers['X-K8s-Authorization'] = clusterToken;
224-
} else if (clientCertificateData && clientKeyData) {
225-
headers['X-Client-Certificate-Data'] = clientCertificateData;
226-
headers['X-Client-Key-Data'] = clientKeyData;
227-
} else {
228-
throw new Error('Missing authentication credentials');
229-
}
203+
const headers = await buildApiHeaders(authData);
230204

231205
const response = await fetch(endpointUrl, {
232206
method: 'GET',
@@ -243,8 +217,16 @@ async function handleFollowUpSuggestions(req, res) {
243217
}
244218
}
245219

246-
router.post('/suggestions', addLogger(handlePromptSuggestions));
247-
router.post('/messages', addLogger(handleChatMessage));
248-
router.post('/followup', addLogger(handleFollowUpSuggestions));
220+
router.post(
221+
'/suggestions',
222+
companionRateLimiter,
223+
addLogger(handlePromptSuggestions),
224+
);
225+
router.post('/messages', companionRateLimiter, addLogger(handleChatMessage));
226+
router.post(
227+
'/followup',
228+
companionRateLimiter,
229+
addLogger(handleFollowUpSuggestions),
230+
);
249231

250232
export default router;

backend/settings/defaultConfig.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ config:
88
config:
99
apiBaseUrl: 'https://companion.cp.dev.kyma.cloud.sap'
1010
tokenUrl: 'https://kymatest.accounts400.ondemand.com/oauth2/token'
11+
skipAuth: false

tests/integration/tests/companion/test-companion-chat-feedback.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ context('Test Companion Chat Error Handling', () => {
88
cy.loginAndSelectCluster();
99
});
1010

11-
it('Feedback response handling', () => {
11+
beforeEach(() => {
12+
cy.mockPromptSuggestions();
1213
cy.intercept('POST', '/backend/ai-chat/messages', req => {
1314
const mockResponse =
1415
JSON.stringify({
@@ -26,7 +27,10 @@ context('Test Companion Chat Error Handling', () => {
2627
body: mockResponse,
2728
});
2829
}).as('getChatFeedbackResponse');
30+
cy.mockFollowups();
31+
});
2932

33+
it('Feedback response handling', () => {
3034
cy.openCompanion();
3135
cy.get('.kyma-companion').as('companion');
3236
cy.sendPrompt('Thanks');

tests/integration/tests/companion/test-companion-ui.spec.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ context('Test Companion UI', () => {
88
cy.loginAndSelectCluster();
99
});
1010

11+
beforeEach(() => {
12+
cy.mockPromptSuggestions();
13+
cy.mockChatResponse();
14+
cy.mockFollowups();
15+
});
16+
1117
describe('Test Welcome screen', () => {
1218
it('Initial loading screen should appear on first open', () => {
1319
cy.openCompanion();
@@ -23,7 +29,6 @@ context('Test Companion UI', () => {
2329
});
2430

2531
it('Welcome screen should be visible on first open', () => {
26-
cy.mockPromptSuggestions();
2732
cy.openCompanion();
2833
cy.wait(3000);
2934
cy.get('.kyma-companion').as('companion');
@@ -40,8 +45,6 @@ context('Test Companion UI', () => {
4045
});
4146

4247
it('Loading screen should not be visible after reset', () => {
43-
cy.mockPromptSuggestions();
44-
cy.mockChatResponse();
4548
cy.openCompanion();
4649
cy.get('.kyma-companion').as('companion');
4750

@@ -65,8 +68,6 @@ context('Test Companion UI', () => {
6568
});
6669

6770
it('Welcome screen should be visible after reset', () => {
68-
cy.mockPromptSuggestions();
69-
cy.mockChatResponse();
7071
cy.openCompanion();
7172
cy.get('.kyma-companion').as('companion');
7273

0 commit comments

Comments
 (0)