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    }