1 package org.modeshape.graph;
2
3 import net.jcip.annotations.NotThreadSafe;
4 import org.modeshape.common.util.CheckArg;
5 import org.modeshape.common.util.Logger;
6 import org.modeshape.common.util.Reflection;
7
8 import javax.security.auth.Subject;
9 import javax.security.auth.callback.Callback;
10 import javax.security.auth.callback.CallbackHandler;
11 import javax.security.auth.callback.NameCallback;
12 import javax.security.auth.callback.PasswordCallback;
13 import javax.security.auth.callback.TextOutputCallback;
14 import javax.security.auth.callback.UnsupportedCallbackException;
15 import javax.security.auth.login.Configuration;
16 import javax.security.auth.login.LoginContext;
17 import javax.security.auth.login.LoginException;
18 import java.io.IOException;
19 import java.security.Principal;
20 import java.security.acl.Group;
21 import java.util.Enumeration;
22 import java.util.HashSet;
23 import java.util.Set;
24
25 /**
26 * JAAS-based {@link SecurityContext security context} that provides authentication and authorization through the JAAS
27 * {@link LoginContext login context}.
28 */
29 @NotThreadSafe
30 public final class JaasSecurityContext implements SecurityContext {
31
32 private static final Logger LOGGER = Logger.getLogger(JaasSecurityContext.class);
33
34 private final LoginContext loginContext;
35 private final String userName;
36 private final Set<String> entitlements;
37 private boolean loggedIn;
38
39 /**
40 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application
41 * configuration name}.
42 *
43 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name}
44 * ; may not be null
45 * @throws IllegalArgumentException if the <code>name</code> is null
46 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the
47 * default callback handler JAAS property was not set or could not be loaded
48 */
49 public JaasSecurityContext( String realmName ) throws LoginException {
50 this(new LoginContext(realmName));
51 }
52
53 /**
54 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application
55 * configuration name} and a {@link Subject JAAS subject}.
56 *
57 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name}
58 * @param subject the subject to authenticate
59 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), if the default
60 * callback handler JAAS property was not set or could not be loaded, or if the <code>subject</code> is null or
61 * unknown
62 */
63 public JaasSecurityContext( String realmName,
64 Subject subject ) throws LoginException {
65 this(new LoginContext(realmName, subject));
66 }
67
68 /**
69 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application
70 * configuration name} and a {@link CallbackHandler JAAS callback handler} to create a new {@link JaasSecurityContext JAAS
71 * login context} with the given user ID and password.
72 *
73 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name}
74 * @param userId the user ID to use for authentication
75 * @param password the password to use for authentication
76 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the
77 * <code>callbackHandler</code> is null
78 */
79
80 public JaasSecurityContext( String realmName,
81 String userId,
82 char[] password ) throws LoginException {
83 this(new LoginContext(realmName, new UserPasswordCallbackHandler(userId, password)));
84 }
85
86 /**
87 * Create a {@link JaasSecurityContext} with the supplied {@link Configuration#getAppConfigurationEntry(String) application
88 * configuration name} and the given callback handler.
89 *
90 * @param realmName the name of the {@link Configuration#getAppConfigurationEntry(String) JAAS application configuration name}
91 * ; may not be null
92 * @param callbackHandler the callback handler to use during the login process; may not be null
93 * @throws LoginException if there <code>name</code> is invalid (or there is no login context named "other"), or if the
94 * <code>callbackHandler</code> is null
95 */
96
97 public JaasSecurityContext( String realmName,
98 CallbackHandler callbackHandler ) throws LoginException {
99 this(new LoginContext(realmName, callbackHandler));
100 }
101
102 /**
103 * Creates a new JAAS security context based on the given login context. If {@link LoginContext#login() login} has not already
104 * been invoked on the login context, this constructor will attempt to invoke it.
105 *
106 * @param loginContext the login context to use; may not be null
107 * @throws LoginException if the context has not already had {@link LoginContext#login() its login method} invoked and an
108 * error occurs attempting to invoke the login method.
109 * @see LoginContext
110 */
111 public JaasSecurityContext( LoginContext loginContext ) throws LoginException {
112 CheckArg.isNotNull(loginContext, "loginContext");
113 this.entitlements = new HashSet<String>();
114 this.loginContext = loginContext;
115
116 if (this.loginContext.getSubject() == null) this.loginContext.login();
117
118 this.userName = initialize(loginContext.getSubject());
119 this.loggedIn = true;
120 }
121
122 /**
123 * Creates a new JAAS security context based on the user name and roles from the given subject.
124 *
125 * @param subject the subject to use as the provider of the user name and roles for this security context; may not be null
126 */
127 public JaasSecurityContext( Subject subject ) {
128 CheckArg.isNotNull(subject, "subject");
129 this.loginContext = null;
130 this.entitlements = new HashSet<String>();
131 this.userName = initialize(subject);
132 this.loggedIn = true;
133 }
134
135 private String initialize( Subject subject ) {
136 String userName = null;
137
138 if (subject != null) {
139 for (Principal principal : subject.getPrincipals()) {
140 if (principal instanceof Group) {
141 Group group = (Group)principal;
142 Enumeration<? extends Principal> roles = group.members();
143
144 while (roles.hasMoreElements()) {
145 Principal role = roles.nextElement();
146 entitlements.add(role.getName());
147 }
148 } else {
149 userName = principal.getName();
150 LOGGER.debug("Adding principal user name: " + userName);
151 }
152 }
153 }
154
155 return userName;
156 }
157
158 /**
159 * {@inheritDoc SecurityContext#getUserName()}
160 *
161 * @see SecurityContext#getUserName()
162 */
163 public String getUserName() {
164 return loggedIn ? userName : null;
165 }
166
167 /**
168 * {@inheritDoc SecurityContext#hasRole(String)}
169 *
170 * @see SecurityContext#hasRole(String)
171 */
172 public boolean hasRole( String roleName ) {
173 return loggedIn ? entitlements.contains(roleName) : false;
174 }
175
176 /**
177 * {@inheritDoc SecurityContext#logout()}
178 *
179 * @see SecurityContext#logout()
180 */
181 public void logout() {
182 try {
183 loggedIn = false;
184 if (loginContext != null) loginContext.logout();
185 } catch (LoginException le) {
186 LOGGER.info(le, null);
187 }
188 }
189
190 /**
191 * A simple {@link CallbackHandler callback handler} implementation that attempts to provide a user ID and password to any
192 * callbacks that it handles.
193 */
194 public static final class UserPasswordCallbackHandler implements CallbackHandler {
195
196 private static final boolean LOG_TO_CONSOLE = false;
197
198 private final String userId;
199 private final char[] password;
200
201 public UserPasswordCallbackHandler( String userId,
202 char[] password ) {
203 this.userId = userId;
204 this.password = password.clone();
205 }
206
207 /**
208 * {@inheritDoc}
209 *
210 * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
211 */
212 public void handle( Callback[] callbacks ) throws UnsupportedCallbackException, IOException {
213 boolean userSet = false;
214 boolean passwordSet = false;
215
216 for (int i = 0; i < callbacks.length; i++) {
217 if (callbacks[i] instanceof TextOutputCallback) {
218
219 // display the message according to the specified type
220 TextOutputCallback toc = (TextOutputCallback)callbacks[i];
221 if (!LOG_TO_CONSOLE) {
222 continue;
223 }
224
225 switch (toc.getMessageType()) {
226 case TextOutputCallback.INFORMATION:
227 System.out.println(toc.getMessage());
228 break;
229 case TextOutputCallback.ERROR:
230 System.out.println("ERROR: " + toc.getMessage());
231 break;
232 case TextOutputCallback.WARNING:
233 System.out.println("WARNING: " + toc.getMessage());
234 break;
235 default:
236 throw new IOException("Unsupported message type: " + toc.getMessageType());
237 }
238
239 } else if (callbacks[i] instanceof NameCallback) {
240
241 // prompt the user for a username
242 NameCallback nc = (NameCallback)callbacks[i];
243
244 if (LOG_TO_CONSOLE) {
245 // ignore the provided defaultName
246 System.out.print(nc.getPrompt());
247 System.out.flush();
248 }
249
250 nc.setName(this.userId);
251 userSet = true;
252
253 } else if (callbacks[i] instanceof PasswordCallback) {
254
255 // prompt the user for sensitive information
256 PasswordCallback pc = (PasswordCallback)callbacks[i];
257 if (LOG_TO_CONSOLE) {
258 System.out.print(pc.getPrompt());
259 System.out.flush();
260 }
261 pc.setPassword(this.password);
262 passwordSet = true;
263
264 } else {
265 /*
266 * Jetty uses its own callback for setting the password. Since we're using Jetty for integration
267 * testing of the web project(s), we have to accomodate this. Rather than introducing a direct
268 * dependency, we'll add code to handle the case of unexpected callback handlers with a setObject method.
269 */
270 try {
271 // Assume that a callback chain will ask for the user before the password
272 if (!userSet) {
273 new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object",
274 callbacks[i],
275 this.userId);
276 userSet = true;
277 } else if (!passwordSet) {
278 // Jetty also seems to eschew passing passwords as char arrays
279 new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object",
280 callbacks[i],
281 new String(this.password));
282 passwordSet = true;
283 }
284 // It worked - need to continue processing the callbacks
285 continue;
286 } catch (Exception ex) {
287 // If the property cannot be set, fall through to the failure
288 }
289 throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback: "
290 + callbacks[i].getClass().getName());
291 }
292 }
293
294 }
295 }
296 }