-
Notifications
You must be signed in to change notification settings - Fork 102
Expand file tree
/
Copy pathsts_sdk.js
More file actions
277 lines (252 loc) · 11.2 KB
/
Copy pathsts_sdk.js
File metadata and controls
277 lines (252 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/* Copyright (C) 2016 NooBaa */
'use strict';
const cloud_utils = require('../util/cloud_utils');
const dbg = require('../util/debug_module')(__filename);
const { RpcError } = require('../rpc');
const signature_utils = require('../util/signature_utils');
const { account_cache, dn_cache } = require('./object_sdk');
const BucketSpaceNB = require('./bucketspace_nb');
const jwt = require('jsonwebtoken');
const { resolve_iam_role_by_arn } = require('../endpoint/iam/iam_utils');
const ldap_client = require('../util/ldap_client');
const keycloak_client = require('../util/keycloak_client');
const { get_tags_claim } = require('../util/access_policy_utils');
class StsSDK {
/**
* @param {nb.AccountSpace} [accountspace] - NC only (AccountSpaceFS). Unused in containerized.
*/
constructor(rpc_client, internal_rpc_client, bucketspace, accountspace) {
this.rpc_client = rpc_client;
this.internal_rpc_client = internal_rpc_client;
this.requesting_account = undefined;
this.auth_token = undefined;
this.bucketspace = bucketspace || new BucketSpaceNB({ rpc_client, internal_rpc_client });
this.accountspace = accountspace;
}
set_auth_token(auth_token) {
this.auth_token = auth_token;
if (this.rpc_client) this.rpc_client.options.auth_token = auth_token;
}
get_auth_token() {
return this.auth_token;
}
/**
* @returns {nb.BucketSpace}
*/
_get_bucketspace() {
return this.bucketspace;
}
async load_requesting_account(req) {
try {
const token = this.get_auth_token();
if (!token) return;
this.requesting_account = await account_cache.get_with_cache({
bucketspace: this._get_bucketspace(),
access_key: token.access_key,
});
if (this.requesting_account?.nsfs_account_config?.distinguished_name) {
const distinguished_name = this.requesting_account.nsfs_account_config.distinguished_name.unwrap();
const user = await dn_cache.get_with_cache({
bucketspace: this._get_bucketspace(),
distinguished_name,
});
this.requesting_account.nsfs_account_config.uid = user.uid;
this.requesting_account.nsfs_account_config.gid = user.gid;
}
} catch (error) {
dbg.error('load_requesting_account error:', error);
if (error.rpc_code === 'NO_SUCH_ACCOUNT') {
throw new RpcError('INVALID_ACCESS_KEY_ID', `Account with access_key not found`);
}
if (error.rpc_code === 'NO_SUCH_USER') {
throw new RpcError('UNAUTHORIZED', `Distinguished name associated with access_key not found`);
}
throw error;
}
}
/**
* _assume_role resolves a role from the correct backend based on deployment mode.
* @param {string} role_arn arn:aws:iam::<account_id>:role/<role_name>
* @returns {Promise<Object>}
*/
async _assume_role(role_arn) {
const resolved_role = await resolve_iam_role_by_arn(role_arn, this._get_bucketspace());
const iam_role = resolved_role.iam_role;
if (!iam_role || resolved_role.error) {
throw new RpcError('NO_SUCH_ROLE',
`No such Role found with name: ${resolved_role.role_name || 'unknown'} and account id : ${resolved_role.account_id || 'unknown'}`);
}
dbg.log1('sts_sdk._assume_role:', 'iam_role:', iam_role.role_name);
return {
...iam_role,
role_name: iam_role.role_name,
account_id: String(resolved_role.account_id),
access_key: iam_role.owner_access_key.unwrap(),
assume_role_policy: iam_role.assume_role_policy_document
};
}
async get_assumed_role(req) {
dbg.log1('sts_sdk.get_assumed_role body', req.body);
const role_config = await this._assume_role(req.body.role_arn);
return {
account_id: role_config.account_id,
access_key: role_config.access_key,
role_config,
};
}
async authenticate_web_identity(req) {
dbg.log1('sts_sdk.get_assumed_ldap_user body', req.body);
let web_token;
const jwt_secret = ldap_client.instance().ldap_params?.jwt_secret;
if (jwt_secret) {
try {
web_token = jwt.verify(req.body.web_identity_token, jwt_secret);
} catch (err) {
dbg.error('get_assumed_ldap_user error: JWT token verification failed', err);
if (err.message.includes('TokenExpiredError')) {
throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
} else {
throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message);
}
}
} else {
dbg.warn('get_assumed_ldap_user: No LDAP JWT secret found, failing back to decoding');
web_token = jwt.decode(req.body.web_identity_token);
if (!web_token) throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'jwt malformed');
}
if (!web_token.user) {
throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing a required claim: user');
}
if (!web_token.password) {
throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing a required claim: password');
}
// TODO: we should see if we can move to the authentication phase
const ldap_user = web_token.user;
const ldap_password = web_token.password;
if (!(await ldap_client.is_ldap_configured()) || !ldap_client.instance().is_connected()) {
throw new RpcError('ACCESS_DENIED', 'LDAP is not configured or not connected');
}
let ldap_auth_result = {};
try {
ldap_auth_result = await ldap_client.instance().authenticate(ldap_user, ldap_password);
} catch (err) {
dbg.error('get_assumed_ldap_user error:', err);
throw new RpcError('ACCESS_DENIED', 'issue with LDAP authentication');
}
return ldap_auth_result;
}
/**
* Get assumed LDAP user
* @param {Object} req - Request object
*/
async get_assumed_ldap_user(req) {
const ldap_auth_result = this.identity_info || await this.authenticate_web_identity(req);
const role_config = await this._assume_role(req.body.role_arn);
dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', 'account.role_config: ', role_config);
return {
access_key: role_config.access_key,
account_id: role_config.account_id,
role_config,
dn: ldap_auth_result.dn,
};
}
/**
* Get assumed role for OIDC/Keycloak user
* Validates JWT token using introspection with client_id, client_secret, and access_token
* @param {Object} req - Request object
* @returns {Promise<Object>} - Assumed role info with session tags
*/
async get_assumed_oidc_user(req) {
dbg.log1('sts_sdk.get_assumed_oidc_user body', req.body.role_arn);
try {
// Initialize OIDC client if not already done
const keycloak_instance = keycloak_client.get_instance();
if (!keycloak_instance.initialized) {
await keycloak_instance.initialize();
}
// JWT token decoded and check token issuer is in provider list
const decoded_token = await keycloak_instance.verify_token(req.body.web_identity_token);
// Introspect token with Keycloak using client_id, client_secret, and access_token
// This is the key implementation for Keycloak - validates token is active and not revoked
const introspection_resp = await keycloak_instance.introspect_token(
req.body.web_identity_token
);
// Extract session tags from decoded token.
const session_tags = get_tags_claim(decoded_token);
// Assume role
const role_config = await this._assume_role(req.body.role_arn);
dbg.log1('sts_sdk.get_assumed_oidc_user _assume_role res',
'account.role_config:', role_config);
return {
access_key: role_config.access_key,
account_id: role_config.account_id,
role_config,
sub: introspection_resp.sub,
aud: introspection_resp.client_id || introspection_resp.aud,
iss: introspection_resp.iss,
session_tags,
// Store additional claims for audit
email: introspection_resp.email,
name: introspection_resp.name,
};
} catch (err) {
dbg.error('get_assumed_oidc_user error :', err, err.rpc_code);
if (err.rpc_code === 'EXPIRED_WEB_IDENTITY_TOKEN' || err.rpc_code === 'INVALID_WEB_IDENTITY_TOKEN') {
throw err;
}
throw new RpcError('ACCESS_DENIED', 'Not authorized to perform sts:AssumeRoleWithWebIdentity');
}
}
/**
* Unified method to get assumed user (LDAP or OIDC/Keycloak)
* Detects token type and routes to appropriate handler
* @param {Object} req - Request object
* @returns {Promise<Object>} - Assumed role info
*/
async get_assumed_web_identity_role(req) {
const decoded = jwt.decode(req.body.web_identity_token, { json: true });
if (!decoded) {
throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'jwt malformed');
}
// Check if OIDC is configured and token is from OIDC provider
if (await keycloak_client.is_keycloak_configured()) {
const keycloak_instance = keycloak_client.get_instance();
const provider = keycloak_instance.get_provider(decoded.iss);
if (provider) {
dbg.log1('Routing to KeyCloak handler for issuer:', decoded.iss);
return await this.get_assumed_oidc_user(req);
} else {
dbg.log0('Routing to Web Identity handler missing', decoded.iss);
}
}
// Fall back to LDAP Web Identity handler
dbg.log0('Routing to LDAP handler');
return await this.get_assumed_ldap_user(req);
}
/**
* Generates a temporary access key for the requesting account
* @returns {Object} - Access token and secret object
*/
generate_temp_access_keys() {
return cloud_utils.generate_access_keys();
}
/**
* Authorizes request account
* @param {Object} req - Request object
* @throws {RpcError} - If the request is not signed or the requesting account is not authorized
*/
authorize_request_account(req) {
const token = this.get_auth_token();
// If the request is signed (authenticated)
if (token) {
signature_utils.authorize_request_account_by_token(token, this.requesting_account);
return;
}
// assume role with web identity is Anonymous
if (req.op_name === 'post_assume_role_with_web_identity') {
return;
}
throw new RpcError('UNAUTHORIZED', `No permission to sts ops`);
}
}
module.exports = StsSDK;