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 }