View Javadoc

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 }