-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
KDCKerberosOperationHandler.java
422 lines (362 loc) · 16.5 KB
/
KDCKerberosOperationHandler.java
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ambari.server.serveraction.kerberos;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import org.apache.ambari.server.AmbariException;
import org.apache.ambari.server.security.credential.PrincipalKeyCredential;
import org.apache.ambari.server.utils.HTTPUtils;
import org.apache.ambari.server.utils.HostAndPort;
import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.commons.collections.MapUtils;
import org.apache.directory.server.kerberos.shared.keytab.Keytab;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* KDCKerberosOperationHandler is an implementation of a KerberosOperationHandler providing
* functionality KDC-based Kerberos providers.
* <p>
* This implementation provides kinit functionality and keytab file caching utilities for classes.
*/
abstract class KDCKerberosOperationHandler extends KerberosOperationHandler {
private static final Logger LOG = LoggerFactory.getLogger(KDCKerberosOperationHandler.class);
/**
* The FQDN of the host where KDC administration server is
*/
private String adminServerHost = null;
/**
* The FQDN and port where KDC administration server is
*/
private String adminServerHostAndPort = null;
/**
* A map of principal names to {@link Keytab} entries to ensure a Keyab file is not created/exported
* for the same principal more than once.
*/
private HashMap<String, Keytab> cachedKeytabs = null;
/**
* A String containing the resolved path to the kinit executable
*/
private String executableKinit = null;
/**
* The absolute path to the KDC administrator's Kerberos ticket cache.
* <p>
* This path is created as as temporary file with a randomized name when this {@link KerberosOperationHandler}
* is open. It is destoryed when this {@link KerberosOperationHandler} is closed.
*/
private File credentialsCacheFile = null;
/**
* A Map of environmet values to send to system command invocations.
* <p>
* This map is to be appened to any map of environment values passed in when
* invoking {@link KerberosOperationHandler#executeCommand(String[], Map, ShellCommandUtil.InteractiveHandler)}
*/
private Map<String, String> environmentMap = null;
@Override
public void open(PrincipalKeyCredential administratorCredentials, String realm, Map<String, String> kerberosConfiguration)
throws KerberosOperationException {
super.open(administratorCredentials, realm, kerberosConfiguration);
if (kerberosConfiguration != null) {
// Spit the host and port from the the admin_server_host value
String value = kerberosConfiguration.get(KERBEROS_ENV_ADMIN_SERVER_HOST);
HostAndPort hostAndPort = HTTPUtils.getHostAndPortFromProperty(value);
// hostAndPort will be null if the value is not in the form of host:port
if (hostAndPort == null) {
// host-only and host and port values are the same since there is no port
// both are equal to the value from the property
adminServerHost = value;
adminServerHostAndPort = value;
} else {
// host-only is the split value;
// host and port value is the value from the property
adminServerHost = hostAndPort.host;
adminServerHostAndPort = value;
}
}
// Pre-determine the paths to relevant Kerberos executables
executableKinit = getExecutable("kinit");
setOpen(init(kerberosConfiguration));
}
@Override
public void close() throws KerberosOperationException {
if (credentialsCacheFile != null) {
if (credentialsCacheFile.delete()) {
LOG.debug("Failed to remove the cache file, {}", credentialsCacheFile.getAbsolutePath());
}
credentialsCacheFile = null;
}
environmentMap = null;
executableKinit = null;
cachedKeytabs = null;
adminServerHost = null;
adminServerHostAndPort = null;
super.close();
}
/**
* Updates the password for an existing user principal in a previously configured IPA KDC
* <p/>
* This implementation creates a query to send to the ipa shell command and then interrogates
* the exit code to determine if the operation executed successfully.
*
* @param principal a String containing the principal to update
* @param password a String containing the password to set
* @param service a boolean value indicating whether the principal is for a service or not
* @return an Integer declaring the new key number
* @throws KerberosOperationException if an unexpected error occurred
*/
@Override
public Integer setPrincipalPassword(String principal, String password, boolean service) throws KerberosOperationException {
if (!isOpen()) {
throw new KerberosOperationException("This operation handler has not been opened");
}
// It is expected that KerberosPrincipalDoesNotExistException is thrown if the principal does not exist.
// The caller expects so that it can attempt to set the password for a principal without checking
// to see if it exists first. If the principal does not exist and is required, the caller will
// create it. This saves a potentially unnecessary round trip to the KDC and back.
if(!principalExists(principal, service)) {
throw new KerberosPrincipalDoesNotExistException(String.format("Principal does not exist while attempting to set its password: %s", principal));
}
// This operation does nothing since a new key will be created when exporting the keytab file...
return 0;
}
/**
* Creates a key tab by using the ipa commandline utilities. It ignores key number and password
* as this will be handled by IPA
*
* @param principal a String containing the principal to test
* @param password (IGNORED) a String containing the password to use when creating the principal
* @param keyNumber (IGNORED) a Integer indicating the key number for the keytab entries
* @return the created Keytab
* @throws KerberosOperationException
*/
@Override
protected Keytab createKeytab(String principal, String password, Integer keyNumber)
throws KerberosOperationException {
if ((principal == null) || principal.isEmpty()) {
throw new KerberosOperationException("Failed to create keytab file, missing principal");
}
// use cache if available
if (cachedKeytabs.containsKey(principal)) {
return cachedKeytabs.get(principal);
}
File keytabFile = null;
try {
try {
keytabFile = File.createTempFile("ambari_tmp", ".keytab");
// Remove the file else the command will fail...
if (!keytabFile.delete()) {
LOG.warn("Failed to remove temporary file to hold keytab. Exporting the keytab file for {} may fail.", principal);
}
} catch (IOException e) {
throw new KerberosOperationException(String.format("Failed to create the temporary file needed to hold the exported keytab file for %s: %s", principal, e.getLocalizedMessage()), e);
}
exportKeytabFile(principal, keytabFile.getAbsolutePath(), getKeyEncryptionTypes());
Keytab keytab = readKeytabFile(keytabFile);
cachedKeytabs.put(principal, keytab);
return keytab;
} finally {
if ((keytabFile != null) && keytabFile.exists()) {
if (!keytabFile.delete()) {
LOG.debug("Failed to remove the temporary keytab file, {}", keytabFile.getAbsolutePath());
}
}
}
}
/**
* Executes a shell command in a credentials context
* <p/>
* See {@link ShellCommandUtil#runCommand(String[])}
* <p>
* This implementation sets the proper environment for the custom <code>KRB5CCNAME </code> value.
*
* @param command an array of String value representing the command and its arguments
* @param envp a map of string, string of environment variables
* @param interactiveHandler a handler to provide responses to queries from the command,
* or null if no queries are expected
* @return a ShellCommandUtil.Result declaring the result of the operation
* @throws KerberosOperationException
*/
@Override
protected ShellCommandUtil.Result executeCommand(String[] command, Map<String, String> envp, ShellCommandUtil.InteractiveHandler interactiveHandler)
throws KerberosOperationException {
Map<String, String> _envp;
if (MapUtils.isEmpty(environmentMap)) {
_envp = envp;
} else if (MapUtils.isEmpty(envp)) {
_envp = environmentMap;
} else {
_envp = new HashMap<>();
_envp.putAll(envp);
_envp.putAll(environmentMap);
}
return super.executeCommand(command, _envp, interactiveHandler);
}
/**
* Returns the KDC administration server host value (with or without the port)
*
* @param includePort <code>true</code> to include the port (if available); <code>false</code> to exclude the port
* @return the KDC administration server host value (with or without the port)
*/
String getAdminServerHost(boolean includePort) {
return (includePort) ? adminServerHostAndPort : adminServerHost;
}
String getCredentialCacheFilePath() {
return (credentialsCacheFile == null) ? null : credentialsCacheFile.getAbsolutePath();
}
/**
* Return an array of Strings containing the command and the relavant arguments needed authenticate
* with the KDC and create the Kerberos ticket/credential cache.
*
* @param executableKinit the absolute path to the kinit executable
* @param credentials the KDC adminisrator's credentials
* @param credentialsCache the absolute path to the expected location of the Kerberos ticket/credential cache file
* @param kerberosConfigurations a Map of key/value pairs containing data from the kerberos-env configuration set
* @throws KerberosOperationException in case there was any error during kinit command creation
* @return an array of Strings containing the command to execute
*/
protected abstract String[] getKinitCommand(String executableKinit, PrincipalKeyCredential credentials, String credentialsCache, Map<String, String> kerberosConfigurations) throws KerberosOperationException;
/**
* Export the requested keytab entries for a given principal into the specified file.
*
* @param principal the principal name
* @param keytabFileDestinationPath the absolute path to the keytab file
* @param keyEncryptionTypes a collection of encrption algorithm types indicating which ketyab entries are requested
* @throws KerberosOperationException
*/
protected abstract void exportKeytabFile(String principal, String keytabFileDestinationPath, Set<EncryptionType> keyEncryptionTypes) throws KerberosOperationException;
/**
* Initialize the Kerberos ticket cache using the supplied KDC administrator's credentials.
* <p>
* A randomly named temporary file is created to store the Kerberos ticket cache for this {@link KerberosOperationHandler}'s
* session. The file will be removed upon closing when the session is complete. The geneated ticket cache
* filename is set in the environment variable map using the variable name "KRB5CCNAME". This will be passed
* in for all relevant-system commands.
*
* @return
* @throws KerberosOperationException
*/
protected boolean init(Map<String, String> kerberosConfiguration) throws KerberosOperationException {
if (credentialsCacheFile != null) {
if (!credentialsCacheFile.delete()) {
LOG.debug("Failed to remove the orphaned cache file, {}", credentialsCacheFile.getAbsolutePath());
}
credentialsCacheFile = null;
}
try {
credentialsCacheFile = File.createTempFile("ambari_krb_", "cc");
credentialsCacheFile.deleteOnExit();
ensureAmbariOnlyAccess(credentialsCacheFile);
} catch (IOException e) {
throw new KerberosOperationException(String.format("Failed to create the temporary file needed to hold the administrator ticket cache: %s", e.getLocalizedMessage()), e);
}
String credentialsCache = String.format("FILE:%s", credentialsCacheFile.getAbsolutePath());
environmentMap = new HashMap<>();
environmentMap.put("KRB5CCNAME", credentialsCache);
PrincipalKeyCredential credentials = getAdministratorCredential();
ShellCommandUtil.Result result = executeCommand(getKinitCommand(executableKinit, credentials, credentialsCache, kerberosConfiguration),
environmentMap,
new InteractivePasswordHandler(String.valueOf(credentials.getKey()), null));
if (!result.isSuccessful()) {
String message = String.format("Failed to kinit as the KDC administrator user, %s:\n\tExitCode: %s\n\tSTDOUT: %s\n\tSTDERR: %s",
credentials.getPrincipal(), result.getExitCode(), result.getStdout(), result.getStderr());
LOG.warn(message);
throw new KerberosAdminAuthenticationException(message);
}
cachedKeytabs = new HashMap<>();
return true;
}
/**
* Ensures that the owner of the Ambari server process is the only local user account able to
* read and write to the specified file or read, write to, and execute the specified directory.
*
* @param file the file or directory for which to modify access
*/
private void ensureAmbariOnlyAccess(File file) throws AmbariException {
if (file.exists()) {
if (!file.setReadable(false, false) || !file.setReadable(true, true)) {
String message = String.format("Failed to set %s readable only by Ambari", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
if (!file.setWritable(false, false) || !file.setWritable(true, true)) {
String message = String.format("Failed to set %s writable only by Ambari", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
if (file.isDirectory()) {
if (!file.setExecutable(false, false) || !file.setExecutable(true, true)) {
String message = String.format("Failed to set %s executable by Ambari", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
} else {
if (!file.setExecutable(false, false)) {
String message = String.format("Failed to set %s not executable", file.getAbsolutePath());
LOG.warn(message);
throw new AmbariException(message);
}
}
}
}
/**
* InteractivePasswordHandler is a {@link ShellCommandUtil.InteractiveHandler}
* implementation that answers queries from kadmin or kdamin.local command for the admin and/or user
* passwords.
*/
protected static class InteractivePasswordHandler implements ShellCommandUtil.InteractiveHandler {
/**
* The queue of responses to return
*/
private LinkedList<String> responses;
private Queue<String> currentResponses;
/**
* Constructor.
*
* @param adminPassword the KDC administrator's password (optional)
* @param userPassword the user's password (optional)
*/
InteractivePasswordHandler(String adminPassword, String userPassword) {
responses = new LinkedList<>();
if (adminPassword != null) {
responses.offer(adminPassword);
}
if (userPassword != null) {
responses.offer(userPassword);
responses.offer(userPassword); // Add a 2nd time for the password "confirmation" request
}
currentResponses = new LinkedList<>(responses);
}
@Override
public boolean done() {
return currentResponses.size() == 0;
}
@Override
public String getResponse(String query) {
return currentResponses.poll();
}
@Override
public void start() {
currentResponses = new LinkedList<>(responses);
}
}
}