-
Notifications
You must be signed in to change notification settings - Fork 25k
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
Internalise run-as user and simplify the regular User class #86246
Conversation
180eb22
to
e621d44
Compare
Pinging @elastic/es-security (Team:Security) |
public static User readFrom(StreamInput input) throws IOException { | ||
final boolean isInternalUser = input.readBoolean(); | ||
assert isInternalUser == false : "should always return false. Internal users should use the InternalUserSerializationHelper"; | ||
final String username = input.readString(); | ||
return partialReadFrom(username, input); | ||
} |
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.
This method is replaced by the new AuthenticationSerializationHelper#readUserFrom
. The replacement is basically the old InternalUserSerializationHelper.readFrom
.
Note that this method assert that the user cannot be internal. I don't think it is necessary at this method level. I have replaced it with assertions at the caller sites (there are only 2 of them).
@@ -29,7 +30,9 @@ public GetUsersResponse(StreamInput in) throws IOException { | |||
} else { | |||
users = new User[size]; | |||
for (int i = 0; i < size; i++) { | |||
users[i] = User.readFrom(in); | |||
final User user = Authentication.AuthenticationSerializationHelper.readUserFrom(in); | |||
assert false == User.isInternal(user) : "should not get internal users"; |
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.
This is the new call site assertion metioned above.
boolean hasInnerUser = input.readBoolean(); | ||
if (hasInnerUser) { | ||
User innerUser = readUserFrom(input); | ||
assert false == User.isInternal(innerUser) : "authenticating user cannot be internal"; |
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.
This is the other call site assertion.
user = new User("u1", new String[] { "r1" }, new User("u2", "r2", "r3")); | ||
assertThat( | ||
user.toString(), | ||
is( | ||
"User[username=u1,roles=[r1],fullName=null,email=null,metadata={}," | ||
+ "authenticatedUser=[User[username=u2,roles=[r2,r3],fullName=null,email=null,metadata={}]]]" | ||
) | ||
); |
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.
No need to test anymore because User no longer has inner user.
A large number of changes to test classes, e.g.
For 1, the randomisation is simply removed. |
final List<User> filteredUsers = filteredUsernames.stream().map(u -> { | ||
if (randomBoolean()) { | ||
return new User(u); | ||
} else { | ||
return new User(new User(u), new User(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 4))); | ||
} | ||
}).collect(Collectors.toList()); |
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.
This is an example where it is no longer necessary to randomise whether User has an inner user.
authentication = createAuthentication( | ||
new User("user1", new String[] { "r1" }, new User("authUsername", new String[] { "r2" })), | ||
"effectiveRealmName" | ||
); | ||
authentication = createAuthentication(new User("user1", "r1"), new User("authUsername", "r2"), "effectiveRealmName"); |
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.
This is an example where a single User (with inner user) argument is split into 2 User arguments.
authentication = mock(Authentication.class); | ||
when(authentication.getUser()).thenReturn(user); | ||
authentication = AuthenticationTestHelper.builder().user(user).build(false); |
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.
Mocking Authentication is no longer recommended. In future PPs, I plan to remove all mocking usage from tests and make Authentication final.
} else if (XPackSecurityUser.is(user)) { | ||
output.writeBoolean(true); | ||
output.writeString(XPackSecurityUser.NAME); | ||
} else if (SecurityProfileUser.is(user)) { |
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.
This isn't an issue with this PR, but I noticed it here.
How do we handle BWC/mixed-nodes here? If output
has a version before 8.3, then the other side won't know about, and won't be able to deserialize the SecurityProfileUser
.
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.
Do we need BWC for this given the feature is still beta? I had assumed that we don't need it. For example, the profile index mappings changed in both 8.2 and 8.3 in a non-compatible way. It is unlikely things would work in a mixed cluster even without this new internal user. That said, maybe we should have phased the changes better so that they are BWC all the way. Unfortunately it was not the case. What do you think?
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.
But what happens when someone does a rolling upgrade from 7.17 to 8.7 ? Will the new internal user get passed around in cross-node requests and cause failures on the 7.17 nodes?
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.
If they call User Profile APIs on the 8.7 node, then yes, it is possible that they can see the failure on a 7.17 node. A BWC for this internal user will not fix the failure entirely. But one may argue that the error message could be slightly more friendy?
Instead of a mysterious serialisation failure (user [_security_profile] is not an internal user
), it could be a 404 index_not_found (no such index [_security]
) or 400 (request body is required
) or some other errors. Now I actually don't know which one is less friendly. The serialisation failure is probably more mysterious but it fails consistently and mentions profile
in the failure message, while the other option could be more unpredictable.
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.
Sorry I have to correct myself: the 400 or 404 error is what user can get on the REST layer. Since this talks about transport layer, I think the error should always be something along the line of action handler not found, which is arguably a more friend error message. I think this is enough to convince me to handle BWC for this user. I will raise a separate PR. Thanks for the discussion.
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.
I raised #86345
Btw, the BWC with the above PR is better than I expected. As long as the request hits nodes with newer version first, it should just work because requests that actually got sent across nodes are basic index and search. Feature specific actions are all handled locally.
assert false == runAs.isRunAs(); | ||
assert false == getUser().isRunAs(); | ||
assert false == runAs instanceof RunAsUser; | ||
assert false == getUser() instanceof RunAsUser; | ||
assert AuthenticationType.REALM == getAuthenticationType() || AuthenticationType.API_KEY == getAuthenticationType(); |
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.
Should we also check that runAs.getAuthenticationType() == AuthenticationType.REALM
?
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.
Not sure if I miss something, but runAs
is a User
, not an Authentication
. So it does not have an AuthenticationType
. Do you mean we should check lookUpRealmRef
is not one of the sythetic realms? Unfortunately we cannot because of custom realms and we don't retrict their names or types.
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.
Sorry, yeah - too many classes with things in the wrong place.
Logically we ought to check that runAs
is a realm based used, and not an Internal user or an API Key, or ServiceAccount.
Since the proposal is an assert
, I don't think we need to worry about custom realms. ES doesn't run with assertions in production, and if it interferes with someone's development environment then it just serves as a reminder that they picked a terrible name & type for their realm.
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.
I added assertion for lookupRealmRef not having synthetic realm name or type.
PS: I also plan to consolidate all the assertions from different Authentication methods into its own private method and call it inside the constructors. This method will also serve a good reference for the internal logic of authentication object.
final String authenticationType = changePasswordRequest | ||
? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) | ||
: randomAlphaOfLengthBetween(4, 12); |
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.
I don't think we need this conditional do we?
This is used for the authUser
- we don't care what realm type it has, we only care which realm user
was lookedUpBy
.
final String authenticationType = changePasswordRequest | |
? randomFrom(ReservedRealm.TYPE, NativeRealmSettings.TYPE) | |
: randomAlphaOfLengthBetween(4, 12); | |
final String authenticationType = randomAlphaOfLengthBetween(4, 12); |
Though authenticationRealmType
is probably a better name.
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.
You are right. It does not matter. I inlined it with simply randomAlphaOfLengthBetween(4, 12)
.
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.
LGTM
@elasticmachine update branch |
Another refactor leftover. Relates: elastic#86246 Resolves: elastic#86421
This PR is a follow-up of elastic#86246 to further clean up the Authentication class by: * Promoting usage of the Subject class. The User field and a few other related fields are now removed from Authentication. Relevant methods have their implementation replaced by using Subjects and same behaviours are retained. * Removed the temporary internal RunAsUser class. It essence is about wire serialisation which is now merged into Authentication itself. * Simplify serialisation of regular User object. All the complexities of handling inner user is now completely within the Authentication class itself. * Cosnolidate assertions in different places into a single method that is called in constructors. Also removed a few assertions because there is no RunAsUser class anymore and a User object is just a simple user. Relates: elastic#86246 Relates: elastic#86544
This PR is a follow-up of #86246 to further clean up the Authentication class by: * Promote usage of the Subject class. The User field and a few other related fields are now removed from Authentication. Relevant methods have their implementation replaced by using Subjects and same behaviours are retained. * Remove the temporary internal RunAsUser class. Its essence is about wire serialisation which is now merged into Authentication itself. * Simplify serialisation of regular User object. All the complexities of handling inner user is now completely within the Authentication class itself. * Consolidate assertions in different places into a single method that is called in constructors. Also removed a few assertions because there is no RunAsUser class anymore and a User object is just a simple user. Relates: #86246 Relates: #86544 Resolves: #80117
Today the run-as information is spread between User (authenticatedUser) and Authentication (lookup realm). They have to be configured consistently to work correctly. Previously there was also no inherent logic to ensure the consistency. Recent refacotring of Authentication class has made the situation better by favour Authentication creation with dedicated convenient methods over free-for-all constructors. #86206 is the ongoing PR that will finally remove public access of Authentication constructors. Now that the Authentication class is being tightly controlled, it makes possible to clean-up the User class. Specifically, the run-as information is already provided by the Authentication class, there is no need for the User class to also keep track of it. In fact, the way User class tracking the authenticating user with an inner user is less straightforward and not friendly to serialisation. Also, conceptually run-as is an information at Authentication level instead of User level.
This PR refactors the User class so that it no longer keeps track of run-as information so that there is a single consistent way to check whether an Authentication object is run-as. The essential changes are:
User.isRunAs()
methodUser.authenticatedUser()
method and its backing instance variableauthenticatedUser
as an argumentRunAsUser
class insideAuthentication
Note that this
RunAsUser
class is not really necessary in long term, the plan is to remove it in the later refactoring of Authentication class (whereSubject
variable and methods will become primary over the currentUser
). It is added mostly to make the refactoring easier and reduce the change to how things work conceptually. This class is not exposed and there should be no need to use this class outside ofAuthentication
itself. This tight scope should make it relatively easy to remove it later.Relates: #86206
Relates: #80117