New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enhancement: Additional user attributes queried by (some) realms #428
Enhancement: Additional user attributes queried by (some) realms #428
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please explain the purpose of the deniedAttributes
? Why is it necessary, what is the usecase for?
java/org/apache/tomcat/util/collections/CaseInsensitiveKeyLinkedMap.java
Show resolved
Hide resolved
Denied Attributes is the internal term of attributes, for which access is denied to. Those attributes could never be exposed as user attributes in the Principal's attributes map. Basically, this applies to attributes/fields that contain the user's password. Requesting such an attribute causes a Both MemoryRealm and UserDatabaseRealm provide all possible (and actually used, MemoryRealm does not use the group attribute) XML attributes of the DataSourceRealm makes the column name configured in |
So you basically want to protect the admin/developer to shoot in the foot? |
Yes, as it may hurt badly. Also, only these denied attributes makes using the (otherwise quite comfortable) wildcard character * possible/safe. |
This looks good enough to put it in. I can probably think about enhancements late. The only thing that I'm really wondering about is why the UserDatabaseRealm did not get arbitrary attributes. |
My first focus was on UserDatabaseRealm and JDNIRealm. Arbitrary user attributes for UserDatabaseRealm sounds interesting. However, I did not yet think about it in detail. AFAIK, there is a schema file for tomcat-users.xml. Some new Digester rules must be defined. Actually, the attributes come from the I'm almost finished with all the changes caused by that code review. Also, I've added support for SQL types ARRAY, BLOB, and CLOB. I will push my latest commits soon. Still open/pending:
The latter might be quite challenging. Needs additions in Has anyone thought about putting these methods to a more official interface |
No, the Tomcat project is not responsible for Jakarta and has no control over it, so we cannot make that change. I'm perfectly fine as it is, you're not supposed to do everything by yourself and I'm interested in improving things a bit too if I can. |
Please, wait at least until I've pushed my latest changes. Also, JDBC stuff in DataSourceRealm needs some adjustments in order not to put arbitrary |
I've committed the changes that we've agreed on during the code review. However, I don't know what to do with the still unresolved conversations. |
What are the next steps? The recent review process was suddenly aborted and nothing more happened till now. Should a re-request another review? @markt-asf Could you please share your ideas on defensive copies? (as suggested/requested by @michael-o) @rmaucher Is there anything I could do to get things moving? |
@cklein05 I am waiting for other committers to review. |
Still looks good to me, needs a super minor merging for UserDatabaseRealm. |
For defensive copies, I see several approaches:
Option one will create unnecessary copies in some scenarios but it should be impossible for a security sensitive internal object to be exposed to a potentially untrusted application. |
On defensive copies: For my understanding, defensive copies do not prevent exposing sensitive user information to (potentially untrusted) applications due to an inappropriate configuration. So, the Realm always needs to be configured carefully with regards to keeping secrets secret. That has not much to do with defensive copies. The only thing defensive copies could help is to prevent that an evil application is able to modify a cached attribute value (we've agreed on that attribute values should be immutable). However, in that case, GenericPrincipal cannot trust the caller and has to generically create these copies itself. I guess, the question is whether we rely on Java's understanding of immutable objects (position A, e. g. String is an immutable type so, no copy is needed) or whether we account for that that's not true with reflection (position B, e. g. a String can be changed inline with reflection or native code). With position A, we could at least save some copies for immutable types (according to Java's definition of immutable). After all, since we can expect that most applications don't request the Principal's attributes too frequently, we could easily go with option 1 and copy every object returned. |
When considering defensive copying, one must remember that no other code can be trusted, but our own code can be trusted. So structures passed-into the realm must be copied to protect our code from a malicious (or incompetent) client breaking things. When returning structures, there is more flexibility depending upon how much you trust the JVM and whether or not you want to protect against truly malicious code. For example, for most cases, it's reasonable to return Collections.unmodifiableCollection to ensure that foreign code doesn't mess with your structure. But, reflection is a possibility and therefore it's not entirely safe. If it's reasonable to return But again, it depends upon hos security-sensitive these items are. Unfortunately, it's impossible to know how security-sensitive these things can be, because they are application-defined. Having a user-attribute "locale" is unlikely to be highly security-sensitive. But maybe "totp-seed" may be. Or "user-is-unrestricted-administrator" or something similar. Security features are always a trade-off, and most applications don't need this kind of uber-paranoid data-handling. But some environments may require it. Since nobody has been beating-down the door asking for user-attributes on realms in the first place, perhaps engineering a solution to include uber-paranoia is over-engineering. Anything that can be written can be re-written, improved, etc. I suggest we do the minimum possible to build the useful feature and ship it. If "casual" damage to structures would be Bad, let's prevent that from happening. But let's not worry too much about an application using native code to modify the internal char array of a |
Okay, let's make some decisions (Bob Ross? Here's a class, and it has a friend...) Since there's no public Both With defensive copies, we could do two things:
Although option 2 has much more cons, that may not be a big issue in practice, since most of the attributes are actually Strings, Numbers and Booleans only (e.g. Sun's standard LDAP provider returns Strings only). The only types for which this may hurt a bit are arrays (both With option 2, I would just modify method So, what to do? (checked my personal choices)
|
...no. I use byte arrays with this all the time. |
In its default configuration it does. You can configure it for byte array as well, I know. |
Security concerns only arise with untrusted apps. Untrusted apps have to run under a SecurityManager else they have access to reflection and can do pretty much whatever they like. Therefore, we don't need to worry about what an app can do via reflection. I'd be happy with a requirement that any attributes passed to the GenericPrincipal constructor should be safe to expose to a potentially untrusted app (e.g. immutable, already a defensive copy, etc). |
So, I guess, it's best to just remove any defensive copies from GenericPrincipal. I just added these due to the recommendations on the User's mailing list in order to make user attributes behave immutable. Since attributes are added by the Realms only, which obtain these values directly from either JDBC or JNDI, I believe, that there's no need to defensively copy these when they are added to the map. Although it would be cheap and simple, I do not expect any help from wrapping the attributes map in GenericPrincipal with If you agree, I will remove methods Awaiting your responses. |
Looking back at this since I thin I have time to merge it. Shouldn't some of the changes be dropped and focused only on the realms that show a benefit ?
|
Rémy, I agree with you that, for MemoryRealm and UserDatabaseRealm, this feature is not that much useful. However, for the sake of completeness (aka all realms should behave the same, in a way) these do support additional user attributes as well. Nevertheless, the feature is primarily intended to provide a user friendly user name easily. The UserDatabase supports the With DataSourceRealm it's nearly as much as useful than with JNDIRealm, since one can always add many interesting columns to the I agreed with Mark and Michael to remove the whole defensive copy stuff. However, that's not yet pushed back onto my GitHub repository. Shall I do so? |
Now, the code should be as we've discussed before. I will additionally provide some initial documentation and a change log entry soon. You are always welcome to improve these while merging. |
Inspired by Rémy's suggestion (@rmaucher), I've added support for arbitrary extra attributes to the
With my current arbitrary attributes implementation, one can just add any extra attributes to the Using nested |
If the attirbute unofficial and undocumented, it should probably be removed on master and deprecated for the rest. |
The code should be finished and in good shape now. <user username="tomcat" password="tomcat" roles="admin" fullName="Tomcat User"
home="The Apache Software Foundation" description="User has full admin rights" /> Obviously, this required some changes in |
The JSP example application under In order to make the example application get these attributes, we should as well consider adding the new No user attributes have been queried. You could configure some in your Realm's configuration. Learn more about it (link to the docs) |
That's it for now. Is anyone willing to merge and port back? :) |
I really need this with JDBCRealm. We have a bunch of customers, that do not want to use LDAP/AD but only JDBC based user database. Why should there be no benefit from having extra attribute fields from a user database table/view? Also, wasn't it kind of your recommendation to have arbitrary user attributes from |
If you really want to do this, then let's focus on adding this to JNDIRealm for now, since this is where this has obvious usefulness. If everything is merged ok, then maybe we can move on to DataSourceRealm. But there, it only seems to be adding a lot of code which will have lower performance. Your customer can live with a custom extended realm for now IMO. I obviously never wanted any arbitrary user attributes in tomcat-users.xml. I said adding the feature to the other realms is pointless with a fixed list of attribute. Somehow, you seem to understand it as: let's add more. That was the opposite: I requested to avoid changing the other realms (except JNDIRealm). |
Why? I don't understand your position. This makes sense on any realm since the principal source shouldn't really matter. I have been using a similiar approach for the last 10 years with my own |
@rmaucher |
@rmaucher However, maybe I just got you wrong :( |
It seems by September I had already changed my mind on this after seeing some actual code. Anyway, the focus should be on JNDIRealm and then later DataSourceRealm. |
Maybe this should be done in stages (PRs)?
|
ec424fe
to
9ee0738
Compare
@michael-o |
Waiting for @rmaucher answer for my last proposal. |
@rmaucher You want to avoid changing the other Realms except JNDIRealm? Why? What's wrong with my code? Focusing on DataSourceRealm you're saying
It does not only add a lot of code. It actually works as it should. I've only added the code required to actually query these attributes in a safe way. Also, it does not really lower performance. If there are no attributes defined, it only takes two extra method calls with a total of two If there are attributes defined, there is one extra SQL query executed for the normal case (plus one extra SQL query executed for the first login attempt in the Realm's lifetime). Of course, that takes some time. However, if the Realm didn't perform that query, the application will have to do that after login, which in turn will take the same time or even more, since the application must use another JDBC connection from the pool. After all, I don't see an effective performance problem with the DataSourceRealm code here. If you do, could you please explain in more detail? You where saying
Yes, that may be right. However, even that fixed list of attributes gives the application the opportunity to obtain the User's fullName property (if defined), which otherwise was just not accessible. Actually, the user's full name (aka display name) is one of the most important attributes users may want, as it is much more end-user friendly than a cryptic logon name. Now, with support for arbitrary attributes, the feature is even more useful. I personally do not need these attributes with the UserDatabaseRealm, since we actually do not use tomcat-users.xml in any setup. However, since that is Tomcat's default Realm, I believe it should support these attributes as well. E. g. I've extended example application at Nevertheless, doing this in steps is an option. How to proceed? Are you able to merge the PR partially? |
I will oppose changes to the MemoryRealm and the UserDatabase realm, and I will ignore this topic for the two other realms (so I will not veto it either). |
@michael-o It should be easy to merge only the files you want to merge, all the realms are independent. |
@michael-o files to skip:
Also, remove attribute Adjust change log entry (remove MemoryRealm and UserDatabaseRealm) in file Remove description of config option Adjust new text (remove references to the now unsupported Realms) in file |
@cklein05 I know, it is quite some work, but can we stick to my proposal here: This will break up this PR in at least three making it much easier to come an agreement. |
@michael-o |
@cklein05 Let's start with the principals (interfaces) first. |
@michael-o You mean the |
That's what you meant? Did not yet create a PR in your repo. |
@michael-o https://github.com/cklein05/tomcat/tree/user-attributes-principal |
Yes, that is what I have expected because with this change you are already able to solve your problem within your realm or modifying Tomcat provided realms. |
So, lets move to new PR #463 |
@michael-o So, lets move to new PR #463 |
Closing this as it has been superseded. |
This enhancement adds support for additional user attributes to be queried by MemoryRealm, UserDatabaseRealm, DataSourceRealm and JNDIRealm. That has already been discussed here
http://tomcat.10.x6.nabble.com/Getting-additional-attributes-for-logged-on-users-tt5110316.html
and here
http://tomcat.10.x6.nabble.com/Enhancement-Additional-user-attributes-queried-by-some-realms-td5111884.html
Bold sentences require your special attention (like questions, decisions, changes not related to the enhancement, etc).
That's still work in progress. There's no documentation and change log entries yet. Let's agree on the code first.
Have a look at example at
/examples/jsp/security/protected/
. It has been modified to list all the Principal's attributes.General implementation notes:
Interface
TomcatPrincipal
now supports an attributes map, accessible by methodsObject getAttribute(String name);
Enumeration<String> getAttributeNames();
(removed)boolean isAttributesIgnoreCase();
Have a look at these method's Javadoc comments.
Note, that it's not named userAttributes, but simply attributes. It uses that more generic name, since the Principal IS (or at least represents) the user.
With JNDIRealm, and, depending on the directory server used, attribute names may be case-insensitive (if
javax.naming.directory.Attributes.isCaseIgnored()
returnstrue
). If so, the Principal's attributes map uses case-insensitive names as well.Class
GenericPrincipal
, which implements that interface, also adds serialization support for the attributes map.Realm classes
MemoryRealm
,UserDatabaseRealm
,DataSourceRealm
andJNDIRealm
now have a new configuration optionuserAttributes
. It takes a delimiter separated list of attribute/field names to query from the user table/entry. These additional attributes are then provided through the Principal's attributes map.That option supports a wildcard character (*), which indicates to retrieve all available attributes/fields (except the password field). Both the delimiter and the wildcard character are defined by a constant in class
RealmBase
.Using the
userAttributes
option requires some preprocessing, depending on the Realm implementation (parsing, validating etc.). Its result is then cached in a Realm's private field.The preprocessing may be quite expensive for some Realms. For example, DataSourceRealm must retrieve all the user table's field names to determine, which requested attributes are valid (if no wildcard character was specified).
I decided not to use any JDBC- or JNDI connections prior to the first authentication attempt so, preprocessing is done lazily for some Realms. As Realms are used by concurrent threads, it uses double-checked locking, DCL in order to be thread-safe (like in
UserDatabaseRealm.getDatabase()
). That, in particular, applies toDataSourceRealm
.Since JNDI simply ignores requested non-existing attributes, it's much easier to use than JDBC and SQL. Together with the fact, that JNDIRealm anyway logs missing or invalid fields for every login attempt (in contrast to while preprocessing only), lazy initialization and the DCL is not needed with that Realm.
Preprocessing logs a warning to the container log for every unknown or invalid attribute requested. Except for
MemoryRealm
, warnings are logged on the first authentication attempt.JNDIRealm
even logs warnings on every authentication attempt, since in a directory users may have different sets of attributes.realmBase.userAttributeAccessDenied
, if an attribute containing the user's password is requested.realmBase.userAttributeNotFound
, if the attribute could not be found.jndiRealm.userAttributeAccessDenied
, if an attribute containing the user's password is requested.jndiRealm.userAttributeNotFound
, if the attribute could not be found.jndiRealm.userAttributeNotRequested
, if a related attribute was sent by the directory server, but was not explicitly requestedWe may still agree on the log levels used.
Realms (are encouraged to) use a LinkedHashMap to store user attributes, so insertion order is preserved. That is,
getAttributeNames()
returns names in the order specified in theuserAttributes
option (with invalid attributes removed).Realm-specific notes
MemoryRealm
username
fullname
roles
(comma separated list of roles like in the XML attribute)password
attribute.startInternal()
.Refactored role accumulation using aLinkedHashSet
to remove duplicate roles. Uses a LinkedHashSet to preserve order for user attributeroles
.UserDatabaseRealm
username
fullname
groups
(comma sep. list of groups like in the XML attribute)roles
(comma sep. list of roles like in the XML attribute)effectiveRoles
(comma sep. list of effective roles w/ group-based roles resolved)password
attribute.MemoryRealm
is not).However, that could easily be changed if you like.
User
object for every invocation of methodgetAttribute
so, changes applied to theUserDatabase
are reflected immediately.DataSourceRealm
userCredCol
.Refactored role accumulation using aHashSet
to remove duplicate roles.JDNIRealm
There is no extra LDAP query required to retrieve additional user attributes. Requested user attributes are merged with the attributes list created in method
getUser(JNDIConnection, String, String, int)
(now uses aSet
to remove duplicates).Emits an AccessDenied warning when requesting the
userPassword
attribute (if specified).Needs no lazy preprocessing and logging.
Logs messages for non-existing and denied attributes (WARNING level) as well as for attributes, not explicitly requested (DEBUG Level), on every authentication attempt (users may have different sets of attributes).
With
userAttributes
containing the wildcard character, it is now legal to query ALL attributes from the user's directory entry. The current implementation of JNDIRealm denied that explicitly. That has been changed and marked with a TODO comment.A directory's attribute names may be case-insensitive, that is
Attributes.isIgnoreCase()
returnstrue
. In that case, the keys of the attributes map provided through the user's Principal are case-insensitive as well.Provides all available values for multiple value (ordered) attributes as an Object[] array.
Supports the Sun LDAP provider's ";binary" option to be appended to an attribute in order to get the value as an array of bytes. However, the ";binary" suffix is then also part of the attribute's name in the Principal's attributes map.
See Attribute Values at https://docs.oracle.com/javase/jndi/tutorial/ldap/misc/attrs.html
I only have access to a single Active Directory Server and did not manage to make it work with the
userPattern
option. So, the user attributes implementation in methodgetUserByPattern(DirContext, String, String[], String)
is not yet completely tested! Although I believe it should work as expected, could someone with another directory server around test this case?Defensive copies of attribute values
As already discussed, the implementers of method
TomcatPrincipal.getAttribute(String)
always return a defensive copy of the attribute's value. That is done by a couple ofinstanceof
checks, implementing a fastpath copy for well-known and commonly used types, like String, Integer, Date etc.For all other types, creating a copy through serialization/deserialization to/from a memory byte buffer is tried. That only works if all classes involved are serializable. Otherwise, the attribute value's string representation returned from its
toString()
method is returned.The fastpath copy process used copy constructors where possible. However, as recently pointed out by Adam Rauch, many of those are deprecated as of Java 9 and will be removed in the future:
https://www.oracle.com/java/technologies/javase/9-deprecated-features.html#JDK-8065614
So, I now use
valueOf()
for those as recommended by Oracle. Since for numbers there is a number cache, providing singletons for values smaller than 256, the whole defensive copying is quite pointless. Also, the number cache is pointless or even dangerous, if someone could ever change the native value of such an instance.How shall we deal with the immutable attributes problem? Is the proposed solution over-secured or even paranoid?
Final notes
ClassGenericPrincipal
: removed trailing comma from roles list intoString()
method.CaseInsensitiveKeyLinkedMap
: Extends classCaseInsensitiveKeyMap
and uses aLinkedHashMap
instance as its backing store.CaseInsensitiveKeyMap
in order to be extendable easily. Also enhanced Javadoc comments to better distinguish between...Map
and...LinkedMap
versions.TomcatPrincipal
to a new official interface 'jakarta.servlet.Principal' (or 'jakarta.servlet.ServletPrincipal')?