001: /*
002: * Copyright (C) The MX4J Contributors.
003: * All rights reserved.
004: *
005: * This software is distributed under the terms of the MX4J License version 1.0.
006: * See the terms of the MX4J License in the documentation provided with this software.
007: */
008:
009: package mx4j.remote;
010:
011: import java.io.ByteArrayOutputStream;
012: import java.io.IOException;
013: import java.io.ObjectOutputStream;
014: import java.io.Serializable;
015: import java.lang.reflect.Constructor;
016: import java.net.URL;
017: import java.security.AccessControlContext;
018: import java.security.AccessControlException;
019: import java.security.AccessController;
020: import java.security.CodeSource;
021: import java.security.DomainCombiner;
022: import java.security.Permission;
023: import java.security.PermissionCollection;
024: import java.security.Principal;
025: import java.security.PrivilegedAction;
026: import java.security.PrivilegedActionException;
027: import java.security.PrivilegedExceptionAction;
028: import java.security.ProtectionDomain;
029: import java.security.cert.Certificate;
030: import java.util.HashMap;
031: import java.util.Iterator;
032: import java.util.Map;
033: import java.util.Set;
034: import javax.management.remote.SubjectDelegationPermission;
035: import javax.security.auth.AuthPermission;
036: import javax.security.auth.Policy;
037: import javax.security.auth.Subject;
038:
039: import mx4j.log.Log;
040: import mx4j.log.Logger;
041:
042: /**
043: * @version $Revision: 1.19 $
044: */
045: public class MX4JRemoteUtils {
046: private static int connectionNumber;
047:
048: /**
049: * Returns a copy of the given Map that does not contain non-serializable entries
050: */
051: public static Map removeNonSerializableEntries(Map map) {
052: Map newMap = new HashMap(map.size());
053: for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
054: Map.Entry entry = (Map.Entry) i.next();
055: if (isSerializable(entry))
056: newMap.put(entry.getKey(), entry.getValue());
057: }
058: return newMap;
059: }
060:
061: private static boolean isSerializable(Object object) {
062: if (object instanceof Map.Entry)
063: return isSerializable(((Map.Entry) object).getKey())
064: && isSerializable(((Map.Entry) object).getValue());
065: if (object == null)
066: return true;
067: if (object instanceof String)
068: return true;
069: if (object instanceof Number)
070: return true;
071: if (!(object instanceof Serializable))
072: return false;
073:
074: return isTrulySerializable(object);
075: }
076:
077: public static boolean isTrulySerializable(Object object) {
078: // Give up and serialize the object
079: try {
080: ByteArrayOutputStream baos = new ByteArrayOutputStream();
081: ObjectOutputStream oos = new ObjectOutputStream(baos);
082: oos.writeObject(object);
083: oos.close();
084: return true;
085: } catch (IOException ignored) {
086: }
087: return false;
088: }
089:
090: public static String createConnectionID(String protocol,
091: String callerAddress, int callerPort, Subject subject) {
092: // See JSR 160 specification at javax/management/remote/package-summary.html
093:
094: StringBuffer buffer = new StringBuffer(protocol);
095: buffer.append(':');
096: if (callerAddress != null)
097: buffer.append("//").append(callerAddress);
098: if (callerPort >= 0)
099: buffer.append(':').append(callerPort);
100: buffer.append(' ');
101:
102: if (subject != null) {
103: Set principals = subject.getPrincipals();
104: for (Iterator i = principals.iterator(); i.hasNext();) {
105: Principal principal = (Principal) i.next();
106: String name = principal.getName();
107: name = name.replace(' ', '_');
108: buffer.append(name);
109: if (i.hasNext())
110: buffer.append(';');
111: }
112: }
113: buffer.append(' ');
114:
115: buffer.append("0x").append(
116: Integer.toHexString(getNextConnectionNumber())
117: .toUpperCase());
118:
119: return buffer.toString();
120: }
121:
122: private static synchronized int getNextConnectionNumber() {
123: return ++connectionNumber;
124: }
125:
126: private static Logger getLogger() {
127: return Log.getLogger(MX4JRemoteUtils.class.getName());
128: }
129:
130: public static Object subjectInvoke(Subject subject,
131: Subject delegate, AccessControlContext context,
132: Map environment, PrivilegedExceptionAction action)
133: throws Exception {
134: if (delegate != null) {
135: if (subject == null)
136: throw new SecurityException(
137: "There is no authenticated subject to delegate to");
138: checkSubjectDelegationPermission(delegate,
139: getSubjectContext(subject, null, context,
140: environment));
141: }
142:
143: Logger logger = getLogger();
144:
145: // If there is no authenticated subject, I leave the transport library to perform its job.
146: // In the RMIConnectorServer, the context at start() time is used by the RMI runtime to
147: // restrict permissions via a doPrivileged() call.
148: // In HTTP JMXConnectorServer, it's the HTTP server responsibility to give such semantic,
149: // if it wants to.
150: // Here, I just execute the action and trust the transport library to do its job right.
151: if (subject == null) {
152: if (logger.isEnabledFor(Logger.TRACE))
153: logger
154: .trace("No authenticated subject, invoking action without using Subject.doAs");
155: return action.run();
156: }
157:
158: // The precedent stack frames have normally AllPermission, since - for example in RMI - they
159: // are JDK domains or JMX/MX4J domains. Below I take the context, and I
160: // inject the JSR 160 domain with the authenticated Subject, then call Subject.doAsPrivileged()
161: // with, eventually, the delegate Subject.
162: // Must call Subject.doAs, since anyone down in the stack call can call Subject.getSubject()
163: // and expect to get the Subject or the delegate, even in absence of the SecurityManager
164: try {
165: if (delegate == null) {
166: if (logger.isEnabledFor(Logger.TRACE))
167: logger
168: .trace("Invoking Subject.doAs using authenticated subject "
169: + subject);
170: return Subject.doAsPrivileged(subject, action,
171: getSubjectContext(subject, delegate, context,
172: environment));
173: } else {
174: if (logger.isEnabledFor(Logger.TRACE))
175: logger
176: .trace("Invoking Subject.doAs using delegate subject "
177: + delegate);
178: return Subject.doAsPrivileged(delegate, action,
179: getSubjectContext(subject, delegate, context,
180: environment));
181: }
182: } catch (PrivilegedActionException x) {
183: throw x.getException();
184: }
185: }
186:
187: private static void checkSubjectDelegationPermission(
188: final Subject delegate, AccessControlContext context)
189: throws SecurityException {
190: Logger logger = getLogger();
191:
192: SecurityManager sm = System.getSecurityManager();
193: if (sm == null) {
194: if (logger.isEnabledFor(Logger.TRACE))
195: logger
196: .trace("No SecurityManager, skipping Subject delegation permission check");
197: return;
198: }
199:
200: AccessController.doPrivileged(new PrivilegedAction() {
201: public Object run() {
202: StringBuffer buffer = new StringBuffer();
203: Set principals = delegate.getPrincipals();
204: for (Iterator i = principals.iterator(); i.hasNext();) {
205: Principal principal = (Principal) i.next();
206: buffer.setLength(0);
207: String permission = buffer.append(
208: principal.getClass().getName()).append(".")
209: .append(principal.getName()).toString();
210: AccessController
211: .checkPermission(new SubjectDelegationPermission(
212: permission));
213: }
214: return null;
215: }
216: }, context);
217: }
218:
219: /**
220: * Returns a suitable AccessControlContext that restricts access in a {@link Subject#doAsPrivileged} call
221: * based on the current JAAS authorization policy, and combined with the given context.
222: * <br/>
223: * This is needed because the server stack frames in a call to a JMXConnectorServer are,
224: * for example for RMI, like this:
225: * <pre>
226: * java.lang.Thread.run()
227: * [rmi runtime classes]
228: * javax.management.remote.rmi.RMIConnectionImpl
229: * [mx4j JSR 160 implementation code]
230: * javax.security.auth.Subject.doAsPrivileged()
231: * [mx4j JSR 160 implementation code]
232: * [mx4j JSR 3 implementation code]
233: * java.lang.SecurityManager.checkPermission()
234: * </pre>
235: * All protection domains in this stack frames have AllPermission, normally, so that when the JMX implementation
236: * checks for permissions, it will always pass the check.
237: * <br/>
238: * One solution would be to use a doPrivileged() call with a restricting context (normally created at the start()
239: * of the connector server), but this forces to grant to the code that starts the connector server all the
240: * permissions needed by clients, and furthermore, grants to clients the permissions needed to start the connector
241: * server.
242: * <br/>
243: * Therefore, a "special" ProtectionDomain will be injected in the AccessControlContext returned by this method.
244: * This special ProtectionDomain will have a CodeSource with null location and the principals specified by the
245: * subject passed as argument.
246: * <br/>
247: * The "injection" of this synthetic ProtectionDomain allows to give AllPermission to the JSR 3 and 160 classes
248: * and implementation, but still have the possibility to specify a JAAS policy with MBeanPermissions in this way:
249: * <pre>
250: * grant principal javax.management.remote.JMXPrincipal "mx4j"
251: * {
252: * permission javax.management.MBeanPermission "*", "getAttribute";
253: * };
254: * </pre>
255: * MX4J also offer an alternative implementation that checks if the given context has a
256: * {@link SubjectDelegationPermission} for the given subject; if so, the policy configuration is much simpler
257: * since does not require that the context has all the possible permissions needed by code down the stack.
258: * This also allows to specify separately the permissions to start the connector server
259: * and the permissions needed by clients.
260: */
261: private static AccessControlContext getSubjectContext(
262: final Subject subject, Subject delegate,
263: final AccessControlContext context, Map environment) {
264: final Logger logger = getLogger();
265:
266: SecurityManager sm = System.getSecurityManager();
267: if (sm == null) {
268: if (logger.isEnabledFor(Logger.TRACE))
269: logger
270: .trace("No security manager, injecting JSR 160 domain only");
271: // Just return the injected domain, to allow Subject.getSubject() return correct values
272: InjectingDomainCombiner combiner = new InjectingDomainCombiner(
273: delegate != null ? delegate : subject);
274: return new AccessControlContext(
275: new ProtectionDomain[] { combiner
276: .getInjectedProtectionDomain() });
277: }
278:
279: // Check if the caller can delegate to a subject
280: boolean combine = ((Boolean) AccessController.doPrivileged(
281: new PrivilegedAction() {
282: public Object run() {
283: try {
284: // Here use the authenticated subject, not the delegate
285: checkSubjectDelegationPermission(subject,
286: context);
287: if (logger.isEnabledFor(Logger.TRACE))
288: logger
289: .trace("Check for SubjectDelegationPermission passed, avoiding security domains combination");
290: return Boolean.FALSE;
291: } catch (AccessControlException x) {
292: if (logger.isEnabledFor(Logger.TRACE))
293: logger
294: .trace("Check for SubjectDelegationPermission not passed, combining security domains");
295: return Boolean.TRUE;
296: }
297: }
298: }, context)).booleanValue();
299:
300: if (combine) {
301: final InjectingDomainCombiner combiner = new InjectingDomainCombiner(
302: delegate != null ? delegate : subject);
303: AccessControlContext acc = (AccessControlContext) AccessController
304: .doPrivileged(new PrivilegedAction() {
305: public Object run() {
306: return new AccessControlContext(context,
307: combiner);
308: }
309: });
310: AccessController.doPrivileged(new PrivilegedAction() {
311: public Object run() {
312: try {
313: // Check this permission, that is required anyway, to combine the domains
314: AccessController
315: .checkPermission(new AuthPermission(
316: "doAsPrivileged"));
317: } catch (AccessControlException ignored) {
318: }
319: return null;
320: }
321: }, acc);
322: ProtectionDomain[] combined = combiner.getCombinedDomains();
323: return new AccessControlContext(combined);
324: } else {
325: InjectingDomainCombiner combiner = new InjectingDomainCombiner(
326: delegate != null ? delegate : subject);
327: return new AccessControlContext(
328: new ProtectionDomain[] { combiner
329: .getInjectedProtectionDomain() });
330: }
331: }
332:
333: private static class InjectingDomainCombiner implements
334: DomainCombiner {
335: private static Constructor domainConstructor;
336:
337: static {
338: try {
339: domainConstructor = ProtectionDomain.class
340: .getConstructor(new Class[] { CodeSource.class,
341: PermissionCollection.class,
342: ClassLoader.class, Principal[].class });
343: } catch (Exception x) {
344: }
345: }
346:
347: private ProtectionDomain domain;
348: private ProtectionDomain[] combined;
349:
350: public InjectingDomainCombiner(Subject subject) {
351: if (domainConstructor != null) {
352: Principal[] principals = (Principal[]) subject
353: .getPrincipals().toArray(new Principal[0]);
354: try {
355: Object[] args = new Object[] {
356: new CodeSource((URL) null,
357: (Certificate[]) null), null, null,
358: principals };
359: domain = (ProtectionDomain) domainConstructor
360: .newInstance(args);
361: } catch (Exception x) {
362: }
363: }
364:
365: if (domain == null) {
366: // This is done for JDK 1.3 compatibility.
367: domain = new SubjectProtectionDomain(new CodeSource(
368: (URL) null, (Certificate[]) null), subject);
369: }
370: }
371:
372: public ProtectionDomain getInjectedProtectionDomain() {
373: return domain;
374: }
375:
376: public ProtectionDomain[] combine(ProtectionDomain[] current,
377: ProtectionDomain[] assigned) {
378: ProtectionDomain[] result = null;
379:
380: if (current == null || current.length == 0) {
381: if (assigned == null || assigned.length == 0) {
382: result = new ProtectionDomain[1];
383: } else {
384: result = new ProtectionDomain[assigned.length + 1];
385: System.arraycopy(assigned, 0, result, 1,
386: assigned.length);
387: }
388: } else {
389: if (assigned == null || assigned.length == 0) {
390: result = new ProtectionDomain[current.length + 1];
391: System.arraycopy(current, 0, result, 1,
392: current.length);
393: } else {
394: result = new ProtectionDomain[current.length
395: + assigned.length + 1];
396: System.arraycopy(current, 0, result, 1,
397: current.length);
398: System.arraycopy(assigned, 0, result,
399: current.length + 1, assigned.length);
400: }
401: }
402:
403: result[0] = domain;
404: this .combined = result;
405:
406: Logger logger = getLogger();
407: if (logger.isEnabledFor(Logger.TRACE)) {
408: logger.trace("Security domains combination");
409: logger.trace("Current domains");
410: logger.trace(dumpDomains(current));
411: logger.trace("Assigned domains");
412: logger.trace(dumpDomains(assigned));
413: logger.trace("Combined domains");
414: logger.trace(dumpDomains(result));
415: }
416:
417: return result;
418: }
419:
420: private String dumpDomains(ProtectionDomain[] domains) {
421: if (domains == null)
422: return "null";
423: StringBuffer buffer = new StringBuffer();
424: for (int i = domains.length - 1; i >= 0; --i) {
425: int k = domains.length - 1 - i;
426: while (k-- > 0)
427: buffer.append(" ");
428: buffer.append(domains[i].getCodeSource().getLocation());
429: // Only work in JDK 1.4
430: // buffer.append(" - ");
431: // buffer.append(java.util.Arrays.asList(domains[i].getPrincipals()));
432: buffer.append("\n");
433: }
434: return buffer.toString();
435: }
436:
437: public ProtectionDomain[] getCombinedDomains() {
438: return combined;
439: }
440:
441: private static class SubjectProtectionDomain extends
442: ProtectionDomain {
443: private final Subject subject;
444:
445: public SubjectProtectionDomain(CodeSource codesource,
446: Subject subject) {
447: super (codesource, null);
448: this .subject = subject;
449: }
450:
451: public boolean implies(Permission permission) {
452: Policy policy = (Policy) AccessController
453: .doPrivileged(new PrivilegedAction() {
454: public Object run() {
455: return Policy.getPolicy();
456: }
457: });
458: PermissionCollection permissions = policy
459: .getPermissions(subject, getCodeSource());
460: return permissions.implies(permission);
461: }
462: }
463: }
464: }
|