1 /* Copyright (c) 2001-2014, The HSQL Development Group
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are met:
6  *
7  * Redistributions of source code must retain the above copyright notice, this
8  * list of conditions and the following disclaimer.
9  *
10  * Redistributions in binary form must reproduce the above copyright notice,
11  * this list of conditions and the following disclaimer in the documentation
12  * and/or other materials provided with the distribution.
13  *
14  * Neither the name of the HSQL Development Group nor the names of its
15  * contributors may be used to endorse or promote products derived from this
16  * software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
22  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 
31 
32 package org.hsqldb.auth;
33 
34 import java.security.Principal;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 import javax.security.auth.Subject;
40 import javax.security.auth.callback.Callback;
41 import javax.security.auth.callback.CallbackHandler;
42 import javax.security.auth.callback.NameCallback;
43 import javax.security.auth.callback.PasswordCallback;
44 import javax.security.auth.callback.UnsupportedCallbackException;
45 import javax.security.auth.login.LoginContext;
46 import javax.security.auth.login.LoginException;
47 
48 import org.hsqldb.lib.FrameworkLogger;
49 
50 /**
51  * Provides authentication and authorization (roles and initial schema)
52  * according to JAAS modules configured by the runtime JAAS implementation.
53  * <P>
54  * <b>JAAS modules used must have both a NameCallback and a PasswordCallback.</b>
55  * This is how we pass the JDBC-provided user name and password to the module.
56  * </P> <P>
57  * JAAS setup is Java-implementation-specific.
58  * For Sun Java, you set up a JAAS configuration file which resides at
59  * <code>$HOME/.java.login.config</code> or at the location that you set with
60  * Java system property <code>java.security.auth.login.config</code>.
61  * </P> <P>
62  * You can use this bean to manage just access, or also to manage roles or
63  * initial schemas.
64  * To use for roles or initial schemas, you must set the roleSchemaValuePattern
65  * property to distinguish which of the JAAS-module-provided values to use.
66  * By default, all JAAS-module-provided Principles will be candidates.
67  * If you set property roleSchemaViaCredential to true, then all
68  * JAAS-module-provided public Credentials will be candidates instead.
69  * </P>
70  *
71  * @see AuthFunctionBean
72  * @see NameCallback
73  * @see PasswordCallback
74  * @author Blaine Simpson (blaine dot simpson at admc dot com)
75  * @since 2.0.1
76  */
77 public class JaasAuthBean implements AuthFunctionBean {
78     private static FrameworkLogger logger =
79             FrameworkLogger.getLog(JaasAuthBean.class);
80 
81     private boolean initialized;
82     private String applicationKey;
83     private Pattern roleSchemaValuePattern;
84     private boolean roleSchemaViaCredential;
85 
JaasAuthBean()86     public JaasAuthBean() {
87         // Intentionally empty
88     }
89 
90     /**
91      * By default, If roleSchemaValuePattern is set, then role and schema
92      * values are obtained from principle values; otherwise existing account
93      * privileges are used (if any).
94      * If roleSchemaViaCredential is set to true and roleSchemaValuePattern is
95      * set, then credential values will be used instead.
96      * <P>
97      * Do not set roleSchemaViaCredential to true unless roleSchemaValuePattern
98      * is set.
99      * </P>
100      */
setRoleSchemaViaCredential(boolean roleSchemaViaCredential)101     public void setRoleSchemaViaCredential(boolean roleSchemaViaCredential) {
102         this.roleSchemaViaCredential = roleSchemaViaCredential;
103     }
104 
105     /**
106      * @throws IllegalStateException if any required setting has not been set.
107      */
init()108     public void init() {
109         if (applicationKey == null) {
110             throw new IllegalStateException(
111                     "Required property 'applicationKey' not set");
112         }
113         if (roleSchemaViaCredential && roleSchemaValuePattern == null) {
114             throw new IllegalStateException(
115                     "Properties 'roleSchemaViaCredential' and "
116                     + "'roleSchemaValuePattern' are mutually exclusive.  "
117                     + "If you want JaasAuthBean to manage roles or schemas, "
118                     + "you must set property 'roleSchemaValuePattern'.");
119         }
120         initialized = true;
121     }
122 
123     /**
124      * Set the key into the JAAS runtime configuration.
125      *
126      * For Sun's JAAS implementation, this is the "application" identifier for
127      * a stanza in the JAAS configuration file.
128      */
setApplicationKey(String applicationKey)129     public void setApplicationKey(String applicationKey) {
130         this.applicationKey = applicationKey;
131     }
132 
133     /**
134      * Assign a pattern to both detect honored values, and optionally
135      * to map from a single principal name or public credential string
136      * to a single HyperSQL role or schema string.
137      * Do not use this method if you are using this JaasAuthBean only to
138      * permit or reject access (with roles and schema being determined by
139      * pre-existing local HyperSQL accounts).
140      * On that case, simple success of the login() method method will allow
141      * access as the specified user.
142      * <P>
143      * If every principal name or public credentials holds only the String
144      * values precisely as HyperSQL needs them, then set the pattern to ".+".
145      * For example, if the JAAS module returns principals (or credentials) with
146      * values "one", "two", "three", then if you set this pattern to ".+",
147      * HyperSQL will attempt to assign initial schema and roles for the values
148      * "one", "two", and "three".
149      * </P><P>
150      * These are two distinct and important purposes for the specified Pattern.
151      * <OL>
152      *   <LI>
153      *      Values that do not successfully match the pattern will be ignored.
154      *      If the pattern does match, then the entire principal or credential
155      *      value will be used to assign initial schema or role (as long as it
156      *      is a valid schema name or role name in the local database).
157      *   <LI>
158      *      Optionally uses parentheses to specify a single capture group
159      *      (if you use parentheses to specify more than one matching group, we
160      *      will only capture for the first).
161      *      What is captured by this group is exactly the role or schema that
162      *      HyperSQL will attempt to assign.
163      *      If no capture parens are given then the Pattern is only used for the
164      *      acceptance decision, and the JAAS-provided value will be returned
165      *      verbatim.
166      * </OL>
167      * </P><P>
168      * N.b. this Pattern will be used for the matches() operation, therefore it
169      * must match the entire candidate value strings (this is different than
170      * the find operation which does not need to satisfy the entire candidate
171      * value).
172      * </P><P>Example1 :<CODE><PRE>
173      *     cn=([^,]+),ou=dbRole,dc=admc,dc=com
174      * </PRE></CODE>
175      *     will extract the CN value from matching attribute values.
176      * </P><P>Example1 :<CODE><PRE>
177      *     cn=[^,]+,ou=dbRole,dc=admc,dc=com
178      * </PRE></CODE>
179      *     will return the entire <CODE>cn...com</CODE> string for matching
180      *     attribute values.
181      * </P>
182      *
183      * @see Matcher#matches()
184      */
setRoleSchemaValuePattern(Pattern roleSchemaValuePattern)185     public void setRoleSchemaValuePattern(Pattern roleSchemaValuePattern) {
186         this.roleSchemaValuePattern = roleSchemaValuePattern;
187     }
188 
189     /**
190      * String wrapper for method setRoleSchemaValuePattern(Pattern)
191      *
192      * Use the (x?) Pattern constructs to set options.
193      *
194      * @throws java.util.regex.PatternSyntaxException
195      * @see #setRoleSchemaValuePattern(Pattern)
196      */
setRoleSchemaValuePatternString(String patternString)197     public void setRoleSchemaValuePatternString(String patternString) {
198         setRoleSchemaValuePattern(Pattern.compile(patternString));
199     }
200 
201     public static class UPCallbackHandler implements CallbackHandler {
202         private String u;
203         private char[] p;
204 
UPCallbackHandler(String u, String pString)205         public UPCallbackHandler(String u, String pString) {
206             this.u = u;
207             p = pString.toCharArray();
208         }
209 
handle(Callback[] callbacks)210         public void handle(Callback[] callbacks)
211                 throws UnsupportedCallbackException {
212             boolean didSetName = false;
213             boolean didSetPassword = false;
214             for (Callback cb : callbacks)
215                 if (cb instanceof NameCallback) {
216                     ((NameCallback) cb).setName(u);
217                     didSetName = true;
218                 } else if (cb instanceof PasswordCallback) {
219                     ((PasswordCallback) cb).setPassword(p);
220                     didSetPassword = true;
221                 } else {
222                     throw new UnsupportedCallbackException(cb,
223                             "Unsupported Callback type: "
224                             + cb.getClass().getName());
225                 }
226             if (!didSetName)
227                 throw new IllegalStateException(
228                         "Supplied Callbacks does not include a NameCallback");
229             if (!didSetPassword)
230                 throw new IllegalStateException("Supplied Callbacks "
231                         + "does not include a PasswordCallback");
232         }
233     }
234 
235     /**
236      * @see AuthFunctionBean#authenticate(String, String)
237      */
authenticate(String userName, String password)238     public String[] authenticate(String userName, String password)
239             throws DenyException {
240         if (!initialized) {
241             throw new IllegalStateException(
242                 "You must invoke the 'init' method to initialize the "
243                 + JaasAuthBean.class.getName() + " instance.");
244         }
245         try {
246             LoginContext lc =
247                 new LoginContext(applicationKey,
248                         new UPCallbackHandler(userName, password));
249             try {
250                 lc.login();
251             } catch (LoginException le) {
252                 // I wish there were a way to distinguish system problems from
253                 // purposeful rejections here.  :-(
254                 logger.finer("JSSE backend denying access:  " + le);
255                 throw new DenyException();
256             }
257             try {
258                 if (roleSchemaValuePattern == null) {
259                     return null;
260                 }
261                 int i = 0;
262                 Matcher m = null;
263                 List<String> rsCandidates = new ArrayList<String>();
264                 List<String> rsList = new ArrayList<String>();
265                 Subject s = lc.getSubject();
266                 if (roleSchemaViaCredential) {
267                     for (Object cred :
268                                 new ArrayList(s.getPublicCredentials())) {
269                         rsCandidates.add(cred.toString());
270                     }
271                 } else {
272                     for (Principal p :
273                             new ArrayList<Principal>(s.getPrincipals())) {
274                         rsCandidates.add(p.getName());
275                     }
276                 }
277                 logger.finer(Integer.toString(rsCandidates.size())
278                             + " candidate " + (roleSchemaViaCredential
279                             ? "Credentials" : "Principals"));
280                 for (String candid : rsCandidates) {
281                     m = roleSchemaValuePattern.matcher(candid);
282                     if (m.matches()) {
283                         logger.finer("    +" + ++i + ": "
284                                 + ((m.groupCount() > 0) ? m.group(1) : candid));
285                         rsList.add((m.groupCount() > 0) ? m.group(1) : candid);
286                     } else {
287                         logger.finer("    -" + ++i + ": " + candid);
288                     }
289                 }
290                 return rsList.toArray(new String[0]);
291             } finally {
292                 lc.logout();
293             }
294         } catch (LoginException le) {
295             logger.severe("System JaasAuthBean failure", le);
296             throw new RuntimeException(le);  // JAAS System failure
297         } catch (RuntimeException re) {
298             logger.severe("System JaasAuthBean failure", re);
299             throw re;
300         }
301     }
302 }
303