Skip to content

Commit bf9c5f0

Browse files
committed
impr(BB-695): Update kafka client config generation to support basic auth
1 parent ead9d77 commit bf9c5f0

File tree

4 files changed

+246
-38
lines changed

4 files changed

+246
-38
lines changed

extensions/notification/NotificationConfigValidator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const joi = require('joi');
22
const { probeServerJoi } = require('../../lib/config/configItems.joi');
3+
const { supportedSaslProtocols } = require('./constants');
34

45
const sslSchema = joi.object({
56
ssl: joi.boolean().default(false),
@@ -10,7 +11,7 @@ const sslSchema = joi.object({
1011
});
1112

1213
const saslAuthSchema = sslSchema.append({
13-
protocol: joi.string().valid('SASL_PLAINTEXT', 'SASL_SSL').required(),
14+
protocol: joi.string().valid(...supportedSaslProtocols).required(),
1415
});
1516

1617
const kerberosAuthSchema = saslAuthSchema.append({

extensions/notification/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const constants = {
2424
eventVersion: '1.0',
2525
eventSource: 'scality:s3',
2626
eventS3SchemaVersion: '1.0',
27+
supportedSaslProtocols: ['SASL_PLAINTEXT', 'SASL_SSL']
2728
};
2829

2930
module.exports = constants;

extensions/notification/utils/auth.js

Lines changed: 74 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
const fs = require('fs');
2+
const joi = require('joi');
23

3-
const constants = require('../constants');
4+
const { authFilesFolder, supportedSaslProtocols } = require('../constants');
5+
const { credentialsFileSchema } = require('../NotificationConfigValidator');
46

57
function getAuthFilePath(fileName) {
6-
const { authFilesFolder } = constants;
78
if (process.env.CONF_DIR !== undefined) {
89
const path = `${process.env.CONF_DIR}/${authFilesFolder}/${fileName}`;
910
try {
@@ -13,7 +14,13 @@ function getAuthFilePath(fileName) {
1314
return null;
1415
}
1516
}
16-
return '';
17+
return null;
18+
}
19+
20+
function readCredentialsFile(filePath) {
21+
const raw = fs.readFileSync(filePath, 'utf8');
22+
const data = JSON.parse(raw);
23+
return joi.attempt(data, credentialsFileSchema);
1724
}
1825

1926
/**
@@ -23,51 +30,81 @@ function getAuthFilePath(fileName) {
2330
*/
2431
function generateKafkaAuthObject(auth) {
2532
const authObject = {};
26-
const { supportedAuthTypes } = constants;
27-
// TODO: S3C-3985 auth object should be validated/checked
28-
const {
29-
type,
30-
ssl,
31-
protocol,
32-
ca,
33-
client,
34-
key,
35-
keyPassword,
36-
keytab,
37-
principal,
38-
serviceName,
39-
} = auth;
33+
34+
const { type, ssl, ca, client, key } = auth;
35+
4036
if (ssl) {
4137
authObject['security.protocol'] = 'ssl';
42-
const caPath = getAuthFilePath(ca);
43-
if (caPath) {
38+
if (ca) {
39+
const caPath = getAuthFilePath(ca);
40+
if (caPath === null) {
41+
throw new Error(`CA file ${ca} not found in ${authFilesFolder}`);
42+
}
4443
authObject['ssl.ca.location'] = caPath;
4544
}
46-
const keyPath = getAuthFilePath(key);
47-
if (keyPath) {
45+
46+
if (key) {
47+
authObject['ssl.key.password'] = auth.keyPassword;
48+
const keyPath = getAuthFilePath(key);
49+
if (keyPath === null) {
50+
throw new Error(`Key file ${key} not found in ${authFilesFolder}`);
51+
}
4852
authObject['ssl.key.location'] = keyPath;
4953
}
50-
const clientPath = getAuthFilePath(client);
51-
if (clientPath) {
54+
55+
if (client) {
56+
const clientPath = getAuthFilePath(client);
57+
if (clientPath === null) {
58+
throw new Error(`Client certificate file ${client} not found in ${authFilesFolder}`);
59+
}
5260
authObject['ssl.certificate.location'] = clientPath;
5361
}
54-
authObject['ssl.key.password'] = keyPassword;
5562
}
56-
// only kereberos is supported now
57-
if (type && supportedAuthTypes.includes(type)) {
58-
authObject['security.protocol'] = protocol;
59-
authObject['sasl.kerberos.service.name'] = serviceName;
60-
authObject['sasl.kerberos.principal'] = principal;
61-
// optional, sasl protocols will have GSSAPI as their default mechanism
62-
authObject['sasl.mechanisms'] = 'GSSAPI';
63-
const keytabPath = getAuthFilePath(keytab);
64-
if (keytabPath) {
65-
authObject['sasl.kerberos.keytab'] = keytabPath;
66-
// default kinit command
67-
const kinitCommand = `kinit -k ${principal} -t ${keytabPath}`;
68-
authObject['sasl.kerberos.kinit.cmd'] = kinitCommand;
63+
64+
switch (type) {
65+
case undefined:
66+
break;
67+
case 'kerberos': {
68+
const { protocol, serviceName, principal, keytab } = auth;
69+
if (!supportedSaslProtocols.includes(protocol)) {
70+
throw new Error(`Unsupported security.protocol: ${protocol}`);
71+
}
72+
// optional, sasl protocols will have GSSAPI as their default mechanism
73+
authObject['sasl.mechanisms'] = 'GSSAPI';
74+
authObject['security.protocol'] = protocol;
75+
authObject['sasl.kerberos.service.name'] = serviceName;
76+
authObject['sasl.kerberos.principal'] = principal;
77+
if (keytab) {
78+
const keytabPath = getAuthFilePath(keytab);
79+
if (keytabPath === null) {
80+
throw new Error(`Keytab file ${keytab} not found in ${authFilesFolder}`);
81+
}
82+
authObject['sasl.kerberos.keytab'] = keytabPath;
83+
authObject['sasl.kerberos.kinit.cmd'] = `kinit -k ${principal} -t ${keytabPath}`;
84+
}
85+
break;
86+
}
87+
case 'basic': {
88+
const { protocol, credentialsFile } = auth;
89+
if (!supportedSaslProtocols.includes(protocol)) {
90+
throw new Error(`Unsupported security.protocol: ${protocol}`);
91+
}
92+
const credsFilePath = getAuthFilePath(credentialsFile);
93+
if (credsFilePath === null) {
94+
throw new Error(`Credentials file ${credentialsFile} not found in ${authFilesFolder}`);
95+
}
96+
const credentials = readCredentialsFile(credsFilePath);
97+
authObject['sasl.mechanisms'] = 'PLAIN';
98+
authObject['security.protocol'] = protocol;
99+
authObject['sasl.username'] = credentials.username;
100+
authObject['sasl.password'] = credentials.password;
101+
break;
102+
}
103+
default: {
104+
throw new Error(`Unsupported auth type: ${type}`);
69105
}
70106
}
107+
71108
return authObject;
72109
}
73110

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
const assert = require('assert');
2+
const fs = require('fs');
3+
const os = require('os');
4+
const path = require('path');
5+
6+
const { generateKafkaAuthObject } = require('../../../../extensions/notification/utils/auth');
7+
8+
9+
describe('generateKafkaAuthObject', () => {
10+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'backbeat-test-'));
11+
const currentConfDir = process.env.CONF_DIR;
12+
13+
before(() => {
14+
fs.mkdirSync(path.join(tempDir, 'ssl'), { recursive: true });
15+
[
16+
'keytab.keytab',
17+
'ca-cert.pem',
18+
'client-cert.pem',
19+
'client-key.pem'
20+
].forEach(file => fs.writeFileSync(path.join(tempDir, 'ssl', file), `fake-${file}`));
21+
fs.writeFileSync(
22+
path.join(tempDir, 'ssl', 'credentials.json'),
23+
JSON.stringify({
24+
username: 'testuser',
25+
password: 'testpassword'
26+
}
27+
));
28+
process.env.CONF_DIR = tempDir;
29+
});
30+
31+
after(() => {
32+
fs.rmSync(tempDir, { recursive: true, force: true });
33+
process.env.CONF_DIR = currentConfDir;
34+
});
35+
36+
const testCases = [
37+
{
38+
description: 'empty auth object',
39+
valid: true,
40+
input: {},
41+
expected: {},
42+
},
43+
// SSL configurations
44+
{
45+
description: 'SSL-only with ssl flag enabled',
46+
valid: true,
47+
input: {
48+
ssl: true
49+
},
50+
expected: {
51+
'security.protocol': 'ssl',
52+
},
53+
},
54+
{
55+
description: 'SSL-only with ssl flag disabled',
56+
valid: true,
57+
input: {
58+
ssl: false
59+
},
60+
expected: {},
61+
},
62+
{
63+
description: 'SSL with custom certificates',
64+
valid: true,
65+
input: {
66+
ssl: true,
67+
ca: 'ca-cert.pem',
68+
client: 'client-cert.pem',
69+
key: 'client-key.pem',
70+
keyPassword: 'test-password',
71+
},
72+
expected: {
73+
'security.protocol': 'ssl',
74+
'ssl.ca.location': path.join(tempDir, 'ssl', 'ca-cert.pem'),
75+
'ssl.certificate.location': path.join(tempDir, 'ssl', 'client-cert.pem'),
76+
'ssl.key.location': path.join(tempDir, 'ssl', 'client-key.pem'),
77+
'ssl.key.password': 'test-password',
78+
},
79+
},
80+
// Kerberos authentication
81+
{
82+
description: 'Kerberos authentication with valid parameters',
83+
valid: true,
84+
input: {
85+
type: 'kerberos',
86+
protocol: 'SASL_PLAINTEXT',
87+
keytab: 'keytab.keytab',
88+
principal: 'test-principal',
89+
serviceName: 'test-service',
90+
},
91+
expected: {
92+
'sasl.mechanisms': 'GSSAPI',
93+
'security.protocol': 'SASL_PLAINTEXT',
94+
'sasl.kerberos.service.name': 'test-service',
95+
'sasl.kerberos.principal': 'test-principal',
96+
'sasl.kerberos.keytab': path.join(tempDir, 'ssl', 'keytab.keytab'),
97+
'sasl.kerberos.kinit.cmd': `kinit -k test-principal -t ${path.join(tempDir, 'ssl', 'keytab.keytab')}`,
98+
},
99+
},
100+
{
101+
description: 'Kerberos authentication with missing keytab file',
102+
valid: false,
103+
input: {
104+
type: 'kerberos',
105+
protocol: 'SASL_PLAINTEXT',
106+
keytab: 'nonexistent.keytab',
107+
principal: 'test-principal',
108+
serviceName: 'test-service',
109+
},
110+
},
111+
{
112+
description: 'Kerberos authentication with unsupported protocol',
113+
valid: false,
114+
input: {
115+
type: 'kerberos',
116+
protocol: 'UNSUPPORTED_PROTOCOL',
117+
keytab: 'keytab.keytab',
118+
principal: 'test-principal',
119+
serviceName: 'test-service',
120+
},
121+
},
122+
// Basic authentication
123+
{
124+
description: 'Basic authentication with valid parameters',
125+
valid: true,
126+
input: {
127+
type: 'basic',
128+
protocol: 'SASL_PLAINTEXT',
129+
credentialsFile: 'credentials.json',
130+
},
131+
expected: {
132+
'security.protocol': 'SASL_PLAINTEXT',
133+
'sasl.mechanisms': 'PLAIN',
134+
'sasl.username': 'testuser',
135+
'sasl.password': 'testpassword',
136+
},
137+
},
138+
{
139+
description: 'Basic authentication with missing credentials file',
140+
valid: false,
141+
input: {
142+
type: 'basic',
143+
protocol: 'SASL_PLAINTEXT',
144+
credentialsFile: 'nonexistent.json',
145+
},
146+
},
147+
{
148+
description: 'Basic authentication with unsupported protocol',
149+
valid: false,
150+
input: {
151+
type: 'basic',
152+
protocol: 'UNSUPPORTED_PROTOCOL',
153+
credentialsFile: 'credentials.json',
154+
},
155+
}
156+
];
157+
158+
testCases.forEach(({ description, input, expected, valid }) => {
159+
it(description, () => {
160+
const tester = valid ? assert.doesNotThrow : assert.throws;
161+
tester(() => {
162+
const result = generateKafkaAuthObject(input);
163+
if (valid) {
164+
assert.deepStrictEqual(result, expected);
165+
}
166+
});
167+
});
168+
});
169+
});

0 commit comments

Comments
 (0)