001 package org.jboss.dna.graph; 002 003 import java.io.IOException; 004 import java.security.Principal; 005 import java.security.acl.Group; 006 import java.util.Enumeration; 007 import java.util.HashSet; 008 import java.util.Set; 009 import javax.security.auth.Subject; 010 import javax.security.auth.callback.Callback; 011 import javax.security.auth.callback.CallbackHandler; 012 import javax.security.auth.callback.NameCallback; 013 import javax.security.auth.callback.PasswordCallback; 014 import javax.security.auth.callback.TextOutputCallback; 015 import javax.security.auth.callback.UnsupportedCallbackException; 016 import javax.security.auth.login.Configuration; 017 import javax.security.auth.login.LoginContext; 018 import javax.security.auth.login.LoginException; 019 import org.jboss.dna.common.util.CheckArg; 020 import org.jboss.dna.common.util.Logger; 021 import org.jboss.dna.common.util.Reflection; 022 023 /** 024 * JAAS-based {@link SecurityContext security context} that provides authentication and authorization through the JAAS 025 * {@link LoginContext login context}. 026 */ 027 public final class JaasSecurityContext implements SecurityContext { 028 029 private final Logger log = Logger.getLogger(getClass()); 030 031 private final LoginContext loginContext; 032 private final String userName; 033 private final Set<String> entitlements; 034 private boolean loggedIn; 035 036 /** 037 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application 038 * configuration name}. 039 * 040 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name} 041 * ; may not be null 042 * @throws IllegalArgumentException if the <code>name</code> is null 043 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the 044 * default callback handler JAAS property was not set or could not be loaded 045 */ 046 public JaasSecurityContext( String realmName ) throws LoginException { 047 this(new LoginContext(realmName)); 048 } 049 050 /** 051 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application 052 * configuration name} and a {@link Subject JAAS subject}. 053 * 054 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name} 055 * @param subject the subject to authenticate 056 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), if the default 057 * callback handler JAAS property was not set or could not be loaded, or if the <code>subject</code> is null or 058 * unknown 059 */ 060 public JaasSecurityContext( String realmName, 061 Subject subject ) throws LoginException { 062 this(new LoginContext(realmName, subject)); 063 } 064 065 /** 066 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application 067 * configuration name} and a {@link CallbackHandler JAAS callback handler} to create a new {@link JaasSecurityContext JAAS 068 * login context} with the given user ID and password. 069 * 070 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name} 071 * @param userId the user ID to use for authentication 072 * @param password the password to use for authentication 073 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the 074 * <code>callbackHandler</code> is null 075 */ 076 077 public JaasSecurityContext( String realmName, 078 String userId, 079 char[] password ) throws LoginException { 080 this(new LoginContext(realmName, new UserPasswordCallbackHandler(userId, password))); 081 } 082 083 /** 084 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application 085 * configuration name} and the given callback handler. 086 * 087 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name} 088 * ; may not be null 089 * @param callbackHandler the callback handler to use during the login process; may not be null 090 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the 091 * <code>callbackHandler</code> is null 092 */ 093 094 public JaasSecurityContext( String realmName, 095 CallbackHandler callbackHandler ) throws LoginException { 096 this(new LoginContext(realmName, callbackHandler)); 097 } 098 099 /** 100 * Creates a new JAAS security context based on the given login context. If {@link LoginContext#login() login} has not already 101 * been invoked on the login context, this constructor will attempt to invoke it. 102 * 103 * @param loginContext the login context to use; may not be null 104 * @throws LoginException if the context has not already had {@link LoginContext#login() its login method} invoked and an 105 * error occurs attempting to invoke the login method. 106 * @see LoginContext 107 */ 108 public JaasSecurityContext( LoginContext loginContext ) throws LoginException { 109 CheckArg.isNotNull(loginContext, "loginContext"); 110 this.entitlements = new HashSet<String>(); 111 this.loginContext = loginContext; 112 113 if (this.loginContext.getSubject() == null) this.loginContext.login(); 114 115 this.userName = initialize(loginContext.getSubject()); 116 this.loggedIn = true; 117 } 118 119 /** 120 * Creates a new JAAS security context based on the user name and roles from the given subject. 121 * 122 * @param subject the subject to use as the provider of the user name and roles for this security context; may not be null 123 */ 124 public JaasSecurityContext( Subject subject ) { 125 CheckArg.isNotNull(subject, "subject"); 126 this.loginContext = null; 127 this.entitlements = new HashSet<String>(); 128 this.userName = initialize(subject); 129 this.loggedIn = true; 130 } 131 132 private String initialize( Subject subject ) { 133 String userName = null; 134 135 if (subject != null) { 136 for (Principal principal : subject.getPrincipals()) { 137 if (principal instanceof Group) { 138 Group group = (Group)principal; 139 Enumeration<? extends Principal> roles = group.members(); 140 141 while (roles.hasMoreElements()) { 142 Principal role = roles.nextElement(); 143 entitlements.add(role.getName()); 144 } 145 } else { 146 userName = principal.getName(); 147 log.debug("Adding principal user name: " + userName); 148 } 149 } 150 } 151 152 return userName; 153 } 154 155 /** 156 * {@inheritDoc SecurityContext#getUserName()} 157 * 158 * @see SecurityContext#getUserName() 159 */ 160 public String getUserName() { 161 return loggedIn ? userName : null; 162 } 163 164 /** 165 * {@inheritDoc SecurityContext#hasRole(String)} 166 * 167 * @see SecurityContext#hasRole(String) 168 */ 169 public boolean hasRole( String roleName ) { 170 return loggedIn ? entitlements.contains(roleName) : false; 171 } 172 173 /** 174 * {@inheritDoc SecurityContext#logout()} 175 * 176 * @see SecurityContext#logout() 177 */ 178 public void logout() { 179 try { 180 loggedIn = false; 181 if (loginContext != null) loginContext.logout(); 182 } catch (LoginException le) { 183 log.info(le, null); 184 } 185 } 186 187 /** 188 * A simple {@link CallbackHandler callback handler} implementation that attempts to provide a user ID and password to any 189 * callbacks that it handles. 190 */ 191 public static final class UserPasswordCallbackHandler implements CallbackHandler { 192 193 private static final boolean LOG_TO_CONSOLE = false; 194 195 private final String userId; 196 private final char[] password; 197 198 public UserPasswordCallbackHandler( String userId, 199 char[] password ) { 200 this.userId = userId; 201 this.password = password.clone(); 202 } 203 204 /** 205 * {@inheritDoc} 206 * 207 * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[]) 208 */ 209 public void handle( Callback[] callbacks ) throws UnsupportedCallbackException, IOException { 210 boolean userSet = false; 211 boolean passwordSet = false; 212 213 for (int i = 0; i < callbacks.length; i++) { 214 if (callbacks[i] instanceof TextOutputCallback) { 215 216 // display the message according to the specified type 217 TextOutputCallback toc = (TextOutputCallback)callbacks[i]; 218 if (!LOG_TO_CONSOLE) { 219 continue; 220 } 221 222 switch (toc.getMessageType()) { 223 case TextOutputCallback.INFORMATION: 224 System.out.println(toc.getMessage()); 225 break; 226 case TextOutputCallback.ERROR: 227 System.out.println("ERROR: " + toc.getMessage()); 228 break; 229 case TextOutputCallback.WARNING: 230 System.out.println("WARNING: " + toc.getMessage()); 231 break; 232 default: 233 throw new IOException("Unsupported message type: " + toc.getMessageType()); 234 } 235 236 } else if (callbacks[i] instanceof NameCallback) { 237 238 // prompt the user for a username 239 NameCallback nc = (NameCallback)callbacks[i]; 240 241 if (LOG_TO_CONSOLE) { 242 // ignore the provided defaultName 243 System.out.print(nc.getPrompt()); 244 System.out.flush(); 245 } 246 247 nc.setName(this.userId); 248 userSet = true; 249 250 } else if (callbacks[i] instanceof PasswordCallback) { 251 252 // prompt the user for sensitive information 253 PasswordCallback pc = (PasswordCallback)callbacks[i]; 254 if (LOG_TO_CONSOLE) { 255 System.out.print(pc.getPrompt()); 256 System.out.flush(); 257 } 258 pc.setPassword(this.password); 259 passwordSet = true; 260 261 } else { 262 /* 263 * Jetty uses its own callback for setting the password. Since we're using Jetty for integration 264 * testing of the web project(s), we have to accomodate this. Rather than introducing a direct 265 * dependency, we'll add code to handle the case of unexpected callback handlers with a setObject method. 266 */ 267 try { 268 // Assume that a callback chain will ask for the user before the password 269 if (!userSet) { 270 new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object", 271 callbacks[i], 272 this.userId); 273 userSet = true; 274 } else if (!passwordSet) { 275 // Jetty also seems to eschew passing passwords as char arrays 276 new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object", 277 callbacks[i], 278 new String(this.password)); 279 passwordSet = true; 280 } 281 // It worked - need to continue processing the callbacks 282 continue; 283 } catch (Exception ex) { 284 // If the property cannot be set, fall through to the failure 285 } 286 throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback: " 287 + callbacks[i].getClass().getName()); 288 } 289 } 290 291 } 292 } 293 }