Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Updated: Comment #N
Problem/Motivation
-
Any service that has 'current_user' as a dependency will call the authentication manager (see definition of current_user service in core.services.yml). So this can get called when dependencies are resolved for services before the AuthenticationSubscriber gets a chance to do its thing.
So, for example, what currently happens is RouteListener > DynamicRouter > RouteManager > csrfRouteSubscriber > csrfGenerator > Current user. So when the event dispatcher lazy loads the services it needs for KernelEvents::REQUEST, RouteListener is first, so this happens. Then later... the AuthenticationSubscriber runs.
- When one needs to change the current_user inside a request, well, they need to first have a ContainerAware object. second even after they set the new current_user to the container, all other objects that had the current_user injected, will have the outdated object (expect the services that have a setCurrentUser call registered in services.yml) This can cause serious security issues.
And of course we cant keep everything synchronized because not everything is a service. - calling \Drupal::currentUser() in session.inc leads to circular exceptions: see #2062771: #2062771: Remove calls to deprecated global $user in \Drupal\Core\Session\SessionManager and #2062069: Remove calls to deprecated global $user in core/includes/bootstrap.inc which makes getting rid of $GLOBALS['user'] a very hard thing to do
Proposed resolution
Remove current_user service, add a (get/set)Account method in the authentication service and make \Drupal::currentUser() call that.
Remaining tasks
patch is rtbc, commit it
User interface changes
None
API changes
current_user service is removed. Inject the authentication instead and call getAccount()
Comment | File | Size | Author |
---|---|---|---|
#180 | interdiff.txt | 487 bytes | ParisLiakos |
#180 | current_user-2180109-180.patch | 29.95 KB | ParisLiakos |
#177 | current_user-2180109-176.patch | 29.81 KB | ParisLiakos |
#177 | interdiff.txt | 1.01 KB | ParisLiakos |
#173 | current_user-2180109-172.patch | 29.34 KB | MartijnBraam |
Comments
Comment #1
damiankloip CreditAttribution: damiankloip commentedLets try with the simplest approach.
This will still run before the AuthenticationSubscriber runs, which is weird.
Comment #2
damiankloip CreditAttribution: damiankloip commentedComment #3
Crell CreditAttribution: Crell commentedSeems simple enough... Although I thought there was discussion of something more involved/complete in IRC?
Comment #4
damiankloip CreditAttribution: damiankloip commentedIndeed, was thinking about doing something more like this - where we have some sort of current user factory service that manages the current user instead.
Comment #5
damiankloip CreditAttribution: damiankloip commentedLet's see how implementing that for dependents goes, see what fails...
Comment #7
damiankloip CreditAttribution: damiankloip commentedSearch plugins.
Comment #9
damiankloip CreditAttribution: damiankloip commentedComment #10
damiankloip CreditAttribution: damiankloip commentedAfter speaking to berdir yesterday, he likes the idea of just using the auth manager instead and not having an additional user factory thing. I am ok with that too.
Here's a new patch. No interdiff, and the whole thing is different.
Drupal installs, expecting alot of test failures atm though.
Comment #11
BerdirSo that means that calling this before the access listener stuff did run, the current user object is anonymous. Makes sense, but should be documented then.
Other than the listener, should this even be called anymore publicly? Maybe at least document as such, possibly remove from the interface?
This should be AccountInterface I think?
To be honest, I don't understand why the current service/naming was user, the idea was that $account could be anything that implements AccountInterface and $user is a user entity.
For example basic auth afaik currently uses the storage controller to load the user, so it would be a entity for them, and setUser() is quite often used for a loaded user entity.
In general, this is obviously a bit more code for services that get it injected, but I do like a lot that we can interact with an actual service when switching/setting the user instead of having a direct dependency of the container and interacting directly with that.
Comment #13
damiankloip CreditAttribution: damiankloip commentedMade the change to AccountInterface, and get/setAccount methods.
I am not sure about removing the authenticate() method from the interface etc... as the manager also implements AuthenticationProviderInterface, with relies on that method.
Comment #14
dawehnerThis has the potential to cause all kind of circular dependency issues, this is clearly major.
Comment #15
damiankloip CreditAttribution: damiankloip commentedI will work on the tests...
Comment #17
damiankloip CreditAttribution: damiankloip commentedBetter assign it. This patch is going to get pretty big.
Comment #18
andypostRelated about comment module manager #2182055-14: Comment module causes Circular reference error on a REST request
Comment #19
damiankloip CreditAttribution: damiankloip commentedLet me give this a try.
Comment #20
dawehnerGreat work!
Given that we remove the current_user service do we longer want to define a currentUser function?
I guess yes?
I wonder whether we want to authenticate if something asks for the user?
Should we just set the global in the authManager?
Nice!
Comment #22
tim.plunkettThis kinda sucks.
This is nice, but very internal.
Filed under *DX regression*
Please please please do not do this.
Comment #23
dawehnerWell, I have to say that loading the current user don't have to be perse a trivial operation. Technically it was always wrong to put
a 100% state dependent service onto the container.
An alternative implementation could be http://symfony.com/doc/current/components/dependency_injection/lazy_serv...
This would create a proxy user object which won't trigger authentication automatically, just if someone calls any method on the user.
Comment #24
BerdirDrupal::currentUser() can obviously stay, although I consider it to be named incorrectly but whatever :)
Yes, it is a bit more code, but everything else is just wrong.
It is IMHO exactly the same problem/principle as http://symfony.com/blog/new-in-symfony-2-4-the-request-stack. Having something that can change during a request as a service and inject it into services is weird, having to interact directly with the container to switch the logged in user is wrong :)
Comment #25
damiankloip CreditAttribution: damiankloip commentedFixed some units tests, as well as some changes in update.php and authorize.php. Let's see how far this gets us now.
Also made the changes to move the setting of the global user into the setAccount method, as dawehner suggested.
Comment #26
damiankloip CreditAttribution: damiankloip commentedRE #18, Let's add it to the related issues then.
Comment #28
damiankloip CreditAttribution: damiankloip commentedThis should fix remaining failures..I think.
Comment #29
ParisLiakos CreditAttribution: ParisLiakos commented+1 and a better title:)
coming from #2187431: Remove current_user service, retrieve it from the authentication manager
Comment #30
ParisLiakos CreditAttribution: ParisLiakos commentedwhy this needs to happen twice?
i guess we could add a hellper method for this, i keep seeing the same logic in different tests
Comment #31
XanoRe-roll.
Comment #32
damiankloip CreditAttribution: damiankloip commentedThanks for the review, Paris! Much appreciated.
I created a getAuthManagerStub() method on UnitTestCase and switched the tests to use this.
The authenication is needed to be called twice as the container is rebuilt along the way, so we lose the account set on the auth manager. This only worked before as it was still relying on the global $user that was set. So this kind of still just worked by chance during update :)
Comment #34
damiankloip CreditAttribution: damiankloip commentedComment #35
tim.plunkettComment #36
ParisLiakos CreditAttribution: ParisLiakos commentedpatch-wise i think this is good to go:)
damianklloip++
Comment #37
ParisLiakos CreditAttribution: ParisLiakos commentedComment #38
ParisLiakos CreditAttribution: ParisLiakos commentedComment #39
ParisLiakos CreditAttribution: ParisLiakos commentedComment #40
dawehnerI really wonder whether exposeing the authentication like that is a good idea. Can't we at least provide some service which wraps the authentication manager, so that it for example won't have to store the current user, which feels a bit like a design flaw :(
Comment #41
catchComment #42
ParisLiakos CreditAttribution: ParisLiakos commentedwe could have the session do that, but that issue is postponed to this one
Comment #43
damiankloip CreditAttribution: damiankloip commented@dawehner, That is what the initial patch did. See above somewhere. Then berdir said it would be a good idea to just shift to the auth manager so we don't just proxy the calls. Blame diverted :)
I think it's fine to live on the auth manager though, as this kind of makes sense as the place to go to get this.
Comment #44
Berdir@dawehner: Can you explain why you think the authentication manager is a bad place for this?
IMHO, the authentication manager doesn't simply manage the authentication backends, he manages the currently logged in user so this makes sense.
To be able to properly abstract the drupal_save_session() stuff that we have in place when switching the currently logged in user, then the authentication manager feels like the only possible place for this, as he is the only one that knows about the active authentication provider instance to which $authenticationManager->disableSaveOrWhateverYouCallIt() can be forwarded to?
@Paris: Wouldn't the session service only be used by the cookie authentication provider and isn't supposed to be accessed directly?
Comment #45
ParisLiakos CreditAttribution: ParisLiakos commentedWell the session service should be accessed directly for many common things, like setting/getting $_SESSION values (and hopefully drupal_set_message() if we manage to convert it).
So it will be exposed more regularly to developers than authentication (right now i think we have just 2 services asking for the authentication even in core.services.yml).
We could have the session service providing the getter/setter for the current user object.
One would say it feels wrong and essentially kills the pluggability of our authentication system. but this is just a lie..there is no pluggability and there will never be as long as we have uid field in the session table
So yes, the patch in #34 makes complete sense and adding more layer (a service that has two methods (get/set)Account) makes the whole thing even more unnecessary complex. i would rather we remove the authentication system and retrieve the current logged in user from session, but that might be a step backwards and also probably out of scope here
Comment #46
klausiComment #47
dawehnerI don't want to block the issue, it just feels wrong to expose such subsystems for that important pieces of functionality.
Comment #48
tim.plunkettI think this needs a bit more discussion first, sorry.
I understand why we need to retrieve it from the authentication manager.
I do not understand why we need to expose that.
Can't we keep a current user service of some sort? Or call it a factory, or whatever. That has two methods, get/set, and it uses the authentication manager.
I don't care if the container has authentication. I care if I can get the current user.
The local task system does not care about authentication. It cares about having a user.
If the problem is that the service is assumed to return the user object directly, that's fine. We can switch all calls to use getAccount or whatever, and hide the authentication stuff internally.
Especially if we find out this was all overkill, or someone finds another solution, they can actually swap out the current user service separately from the authentication one.
Comment #49
BerdirI'm totally fine with not rushing anything and discussing it through, too many changes related to this were already rushed in, which resulted in the current confusing state :)
Trying to understand your reasons, but I'm not sure I do:
- The current user isn't a service, it's a state/data object just like request. Yes, that is a service too right now, but the referenced blog post above documents that it has been deprecated and will be removed as a standalone service in Symfony 3.0.
- Given that, I think we can not make it a standalone service. Sure, we could try to only use setter injection and what not, but again, the referenced issue states that Symfony tried all that for the request and finally came to the conclusion that it simply doesn't work and replaced it with RequestStack.
- So then next thing that I don't understand is why you consider the authentication manager to be some "irrelevant subsystem thing that code shouldn't have to interact with"? How is this different from getting a config object from the config factory, for example? I don't understand how it would be easier to understand if we'd have a CurrentUserStack (or whatever, just basing on RequestStack) service, instead of asking the authentication manager? I think we have a large amount of managers and factories now that code is interacting with (cache, config, entity, state, hooks, pretty much everything?), so why is having an authentication manager confusing but not the others?
#48.1: Yep that's weird, but so is the code in HEAD, when would the current_user service (not having a logged in user, but the actual current_user object existing or not) not exist? Maybe we can assume that authentication always exists and we would ask it if there is a current user? "Drupal::service('authentication')->hasCurrentUser()? Would that be better? Isn't the fact that current_user can conditionally exist or not an indicator that it is not a service? This seems like another example where we directly interact with the DIC and make it an essential part of an API while it should be just a very dump container of services? Instead of interacting with a service, that has an explicit API and responsibility.
#48.2: Also agreed, but if the authentication manager is *the* way to get the current user, that's the thing to inject? Again, just like you inject the entity manager if you want to do anything with entities, or the key value factory if you want to use a given key value collection? True, we do have a few shortcuts, like a @state service, and @database for the default database connection, and each cache bin is exposed as a service, but those are still all services and not their own data objects.
Comment #50
tim.plunkettIt's all about naming. Those are both "foo" and "foo factory" pairs.
I think @ParisLiakos was joking, but in IRC he said:
I think that maybe just seeing s/current_user/authentication is my problem here, and that authentication is just a weird name. Maybe I'll get used to it, maybe I won't.
If it was the current_user.factory service, I would get it. Even if that is the authentication service underneath, great.
In addition to CurrentUserFactory, we floated CurrentUserProvider.
Comment #51
tim.plunkettTentative renaming.
Comment #52
msonnabaum CreditAttribution: msonnabaum commentedI apologize for coming in late here, but I'm baffled as to why we're re-architecting this based on the need to change the current user mid-request.
The point of moving away from global $user is to avoid global, mutable state, because it makes our code complex and unpredictable. What we're doing here is just a fancy version of global $user. As stated in this thread, it's still totally unsafe to hold on to a current user object.
I cannot think of a single use case for supporting this. Anytime I've changed the current user in the past was because I was calling code that referenced global $user. We should no longer have code that does that, and if we do, that code needs to be fixed.
The only case I see in core is cron, which can easily be handled with a separate front controller.
The current trend of injecting "managers" and "factories" instead of what you actually need is not a good one. Let's not continue it here.
Comment #53
catchComment #54
damiankloip CreditAttribution: damiankloip commentedSo, we really need to decide one thing before we go any further:
Do we want to support being able to change the current user during a request?
Discuss.
Comment #55
tim.plunkettIf we don't support changing it directly, we need to support a mechanism to switch to a different user for the *next* request. Think logging in, or masquerade module, or even running cron.
Comment #56
damiankloip CreditAttribution: damiankloip commentedSee, this is the idea I do NOT like very much. Storing state somewhere about what to do with the user during the next request. Sounds like a car crash.
Comment #57
BerdirIMHO, we have to support it.
Login/Masquerade: session.inc does not support to save a session for something that is not current_user, so we have to switch on request, save it to the session, so that we can become that user on the next request. Yes, we have issues to replace that with symfony code, but we've been trying that for years now, with different approaches and haven't gotten far.
Cron is an interesting example, we could theoretically have a specific controller thing that that enforces an anon user from the beginning. We currently have two routes to run cron though, one that needs an administrative permission and does a redirect back to the status report and the other that does checks access based on the cron key. So that idea would only work for the second case?
Also my use case that I mentioned in IRC with simplenews.module generating personalized e-mails in the context of a user (think access, history/read information for that user, views current user arguments) and so on either requires being able to switch it *or* not having current user as global state at all, which would require passing it through everything that needs it, which seems more than unrealistic to me. It also *not* related to the request (the object or in "info from browser" concept) in anyway. So I don't see how ideas like re-boot kernel with a different request helps.
As much as @msonnabaum's theory about "global state should be unmutable" might be true, I don't see how that could possibly work in Drupal (not just current user, also for example current language).
Comment #58
andypostSuppose better to fix code and pass user as required argument.
The question is about services that need current user like Comment manager that we need to split #2188287: Split CommentManager in two other case here is #2081585: [META] Modernize the History module but in perfect world current user should be passed here and UID should be used as static cache key or just skip the cache.
In my pov services should not depend on user at all.
Controllers need to get user from request context.
EDIT Masquerade module does logout/login so does not affected this change, the module just need to save original UID in new session somewhere
Comment #59
dawehnerYes, it is not ideal to have global
state, everyone understands that, though we cannot try to sacrifice usecases which aren't really rare, as berdir has proven.
Drupal is not the system known for the best possible architecture but for a system which is flexible enough as well as has many features out of the box.
As a compromise we should try to keep out the current user out of as many places as possible. One small example, which might be worth to consider, would be to figure out how to integrate $user->hasPermission with #access on render arrays directly. '#access_permission' => array('name'), so just the render service knows about users.
Comment #60
ParisLiakos CreditAttribution: ParisLiakos commentedI dont see why we are discussing if we want to support changing user mid-request here.
Unless i am mistaken, there are two more bullets unrelated to that in OP.
The current_user approach we have now in HEAD, will never work and that was proven with the request service, which had the same approach and made everyone in symfony world endup to the request_stack.
Lets move on here, and open a new issue and discuss the setAccount() method. we can even deprecate it to show that using it is a bad practice and in the meantime fix session to not change the user but only send the related cookies. And also the cron service..but i dont see why we need to block this issue on it
Comment #61
ParisLiakos CreditAttribution: ParisLiakos commentedbtw, we can later add a CurrentUserTrait for this (
$this->getCurrentUser()
or$this->getAccount()
or whatever) and hide the authentication part in there for those who dont like itComment #62
BerdirI think we need the ability to set the account to be able to replace those cases where we currently have to set it directly on the container.
Comment #63
damiankloip CreditAttribution: damiankloip commentedOk, new patch, going right back to the CurrentUserFactory idea.
If someone has a better name, let me know and i'll happily change it!
Comment #65
damiankloip CreditAttribution: damiankloip commentedThis should install now, atleast.
Comment #67
msonnabaum CreditAttribution: msonnabaum commentedk, so we basically made a complex version of global $user, that's actually more work to stub in tests. Seems legit.
Comment #68
damiankloip CreditAttribution: damiankloip commentedStill working on this.
Comment #69
dawehnergo damian!
Comment #70
damiankloip CreditAttribution: damiankloip commentedOk, this should fix the tests now.
Mark did mention something in IRC yesterday (Just to put it out there) that if we are doing something like this then we might as well just store the current user on \Drupal, i.e. Drupal::currentUser/Drupal::setCurrentUser or something. I personally prefer this way. probably biased though.
Comment #71
damiankloip CreditAttribution: damiankloip commentedForgot the change the status.
Comment #72
jibranTo the guy who is currently on the plan @damiankloip it is an empty patch.
Comment #73
jibranI tried recreate the patch by combining the patch in #65 and interdiff in #70 but I am unable to apply the interdiff cleanly. Perhaps interdiff is also wrong.
Comment #74
dawehnerRight, we could actually do quite a bit, but using your way allows us to replace the component with different implementations without adding knowledge to our Drupal class.
Comment #75
damiankloip CreditAttribution: damiankloip commentedWhoops, sorry about that! That's what you get for rushing at the airport I guess :)
Daniel, I think I agree. This gives us more flexibility, and who knows what people may want to do.
Comment #76
damiankloip CreditAttribution: damiankloip commentedBah! sorry.
Comment #78
damiankloip CreditAttribution: damiankloip commentedSorry, and a reroll!
Comment #79
Damien Tournoud CreditAttribution: Damien Tournoud commentedI'm not sure I'm following all the debates here, but wouldn't it make more sense to simply define the
current_user
as:That seems to make a lot more sense to me then this weird factory.
Comment #80
Damien Tournoud CreditAttribution: Damien Tournoud commentedThe back-and-forth on this issue over the past few months is more then painful (we are storing the account on the request, that's cool, no? oh, no, let's store it as a synchronized
current_user
service instead synchronized with the request scope; oh no, let's not make it synchronized; oh, no let's store it inside a factory service).How do we know this is the last design?
Comment #82
damiankloip CreditAttribution: damiankloip commentedNope, I don't think so. Then we are back to injecting the whole container if we want to change the user etc.. which is partly what this issue is trying to get away from.
How do you ever know anything is the last design? :)
Comment #83
damiankloip CreditAttribution: damiankloip commentedSmall things, let's try this.
Comment #85
msonnabaum CreditAttribution: msonnabaum commented@Damian - If I understand scopes, that would mean changing the current user would be a subrequest? If so, #57 seems to be arguing that even that isn't good enough, and we just need it to work like it did in D7.
Comment #86
damiankloip CreditAttribution: damiankloip commentedUgh, sorry.
Comment #87
sunUpfront: Sorry, while also subscribed to comments, I mainly followed this issue via discussions in IRC thus far.
We're spending at least half of the time in this debate to discuss potential use-cases and whatnot. That's what a technical requirement is and what test expectations are for.
So, architecturally and strictly technically speaking, the one and only proper way to move forward is to actually codify the possible and officially supported expectations in tests.
From there, we should theoretically have a better picture and understanding of which proposal works and which does not.
That said: Overall I agree with @msonnabaum:
The entire current_user service should not be a thing in the first place. — We're just duct-taping a flawed architecture, because we're not injecting the dependency into all consumers, and thus have to rely on a global state construct instead.
→ Whether that global state construct is
global $user
or\Drupal::currentUser()
factually makes no difference.Due to that, there's little point in turning that global state construct into a "factory" service. As long as it is a global state construct, any additional complexity like a factory or whatnot does not solve a single problem.
On architectural grounds, @Damien Tournoud's input of turning the current_user service both into a scope as well as synchronized service makes some more sense, since that at least allows us to properly "invalidate" all dependent services by re-instantiating them upon scope-change.
But in the end, our intention/use-case of changing the current user/language/context "mid-request" is not to invalidate/re-instantiate services upon scope change. Instead, we want to enter a new context with changed parameters, so as to (temporarily) execute some operations within that context. And upon completion, we want to switch back and revert to the original context — without re-initializing the context, because stuff like [external] authentication either does not support that or the attempt to do so would be terribly slow.
→ That's the concept of a stack. A context stack.
We had plenty of in-depth discussions about a context stack ~2-3 years ago already. One of the primary consumers back then being Panels, Context, and similar stuff. Somehow, all of that collectively fell off our radar. It seems it would be a good idea to take a big step back, take a deep breathe, and re-evaluate that prior art.
Given that Symfony 3.0 introduced a new RequestStack, which seemingly appears to attack a very similar conceptual problem, it would be a good idea to take some time to study that revised architectural approach in-depth. (We should actually update, immediately, TBH.)
Speaking of, in the end, this entire debate leaves me with the impression we're trying to solve a generic architectural problem that should actually be solved upstream instead. Did we reach out to @fabpot and others already?
Comment #88
BerdirAs commented in #24 with the link to http://symfony.com/blog/new-in-symfony-2-4-the-request-stack, they clearly came to the conclusion that all that scope and synchronized stuff for request did not work. So they deprecated it in favor of this is IMHO the same approach, just applied to current user. So IMHO, we are already following upstream?
I don't get why it has to be called factory as it doesn't factor anything, it just contains it. And with the impersonate issue, setAccount(), it would essentially become a stack.
But +1 on asking @fabpot on his opinion.
Comment #89
damiankloip CreditAttribution: damiankloip commentedOk, so we decide to go with scopes; Let's entertain the idea for a minute.
1. You would have to convey to people that they have to enter a new scope, and set the user on the container again. Hmm, yes, that will be the obviously choice for people.
2. All services would have to have a setCurrentUser() method to synchronize this, so you have to make sure people use this pattern and not constructor injection.
3. You also have to try and make sure people understand what '?@current_user=', so you forget an '=' sign, your implementation is broken. Also - great.
4. You would STILL have the same issue where people should not be storing this on their classes, and if they did, would also break the implementation.
5. You are back to having to inject the whole container into things to change the user
6. The current_user service would also have to be synthetic? So our auth service for example would have to inject the container and set this? would be strange.
I admire the purism from a technical point of view, I don't see how that approach is viable to D8 at all.
Also, I would say fixing this upstream is not going to help us in time (May be worth doing though). Even if we did, the chances of getting this into a Symfony release and into our code base in the timeframe is very optimistic.
So, why has Symfony introduced the RequestStack? Because doing it basically as described above just doesn't work properly! There are too many factors that can break it. So now what is being suggested here is to go with the same model that Symfony was using before recommending the RequestStack. Which we could do, but then I think we would arrive at the same conclusion as they did.
Comment #90
damiankloip CreditAttribution: damiankloip commentedSorry berdir, Think I x posted that. Didn't see your reply first. I am all in favour of asking what @fabpot thinks. It seems like the RequestStack all over again though - for sure.
I can change the name of the service, no problem. Any better ideas?
Comment #91
dawehnerQuoting http://symfony.com/blog/new-in-symfony-2-4-the-request-stack ... it is just drupal who decided that users should be as simple as possible to access. They even throw it away in 3.0
Why are synthetic services a bad idea here: Synthetic services breaks all kind of lazy-execution, so we would have to create the current user as early as the request to really be sure.
And note: All this fancy synchronization/scope handling does not work at all for non services code like plugins are, which also might be persisted during different user scopes.
Comment #92
catchMarked #2102027: Add back the request scope to the current user service as duplicate.
Comment #93
cpj CreditAttribution: cpj commented@sun, #87
I am fairly new to Drupal, so am not aware of the prior work on context stacks that you mention - is this visible somewhere ?
I've used Symfony in several applications with complex user authentication / authorization / switching scenarios, and when necessary I've handled changing / restoring the user "object" and user "session" in a similar way to the Symfony 2.4 request stack, so +1 for that general concept.
I know there are other open issues on session, but I'm a bit surprised session handling doesn't feature more in this discussion. My experience is that switching or "stacking" the user "context" often implies switching or "stacking" the session at the same time. In Drupal, Is it really possible to switch the user object without regard for the state of the session ?
Comment #94
damiankloip CreditAttribution: damiankloip commentedJust a reroll.
Comment #95
damiankloip CreditAttribution: damiankloip commentedSorry, looks like I rebased the wrong branch. Another reroll on its way.
Comment #96
Crell CreditAttribution: Crell commentedWe discussed this at length in today's WSCCI meeting. Here's the conclusion:
Part of the problem is there are really three different issues being discussed here, overlapping.
1) Currently, due to dependencies we are implicitly running the login code twice: Once when creating various services in the container (the current_user service self-instantiates) and once in a request listener.
2) Some services that have a current_user dependency cause the user to be authenticated too early, and because users are entities and entities depend on all kinds of crap it can cause circular dependency issues.
3) The question of whether or not the current user should be a mutable value.
For the first two points, I believe we can address it by removing the authentication listener and JUST authenticating on the fly by the factory specified in the DIC. Then, we add a second service that is a user proxy. Whether that's a transparent proxy (ie, implements AccountInterface) or an explicit wrapper, I don't much care. Then most services that need the current user as a dependency can pass that in their constructor while those that would cause a circular dependency can break it by using the proxied version instead. Note that using the proxied version in the constructor would *still break things*, so don't do that. We'll need to determine how to communicate that properly.
Note: Mark Sonnabaum pointed out that the number of services we have that need the user as a constructor parameter is a problem in and of itself, and most if not all should be accepting a user object as a parameter to the relevant method instead. In general, I agree. However, firstly there are cases where that's simply impossible (eg, the service is behind a COR so the COR object doesn't know it needs to get the user object to pass along in the first place) and secondly I don't want to hold up this issue and those that depend on it while we clean up those cases. Such clean up and can and should happen in parallel. Anywhere that a service needs to use the proxied user I would argue is a design flaw. We cannot fix all of those design flaws in Drupal 8, but we can at least highlight them clearly so we know where they are to fix later (in 8.x or 9.0, depending on the situation).
As for point 3, whether or not the current user should be a mutable value, the answer is simple: Abso-frackin-loutly not. We've poured thousands of hours into eliminating global state and mutable context for a reason: those things make code brittle. We are *not* going to build in a mechanism to encourage code to be brittle. Not happening. Anywhere that functionality is needed it's a sign that we ought to be doing something else instead. This is a design flaw we *cannot* push off to later.
For cases like cron or masquerade, the answer is to use a subrequest. Subrequests are the only way that request-based context can and should change. (The request itself, the user, etc.) If cron wants to run everything as user 0, it can fire a subrequest that does that. It doesn't mess with global state.
For cases like simplenews, if it needs to change the global state of the user then it means it is missing functionality to begin with. That is, it doesn't support using "a user" as a context, it only supports "the user viewing the site" as a context. Faking the latter as a way to support the former is entirely wrong, and is the sort of spaghetti logic we're trying to eliminate from Drupal entirely. The solution is to let simplenews take a passed user object and pass that along as needed, and use *that* as a driver to find other places that are depending on the current user incorrectly. If we run into places where it simply cannot due to a design flaw, then it can fake it using a subrequest, not global mutation. A subrequest is a totally fugly way to do it... because it's a totally fugly thing to do. We shouldn't hide the pain of doing something wrong, because then we forget that it's wrong and keep doing more of it. And that's how we got into the mess we're in to begin with. :-)
In short, out-of-band dependence on the current user is an ugly we can live with, and I don't think we can eliminate entirely. Mutable current user is an ugly we cannot allow, as it undermines everything else we've been trying to do in Drupal for the past 3 years. Berdir, I'm happy to chat with you about how to refactor simplenews so that it doesn't need to do that sort of hackery anymore if you have time at some point (perhaps an upcoming conference).
Comment #98
damiankloip CreditAttribution: damiankloip commentedOK, well if that is the plan (I care little at this point. I would rather just get this issue out of the way), Then we should just do this?
Make the current_user service lazy, remove the auth event (and just let this happen when the current user is needed). Based on what has been said above I see little to no point of adding an additional proxy object now. As if people are going to break shit, they will do it regardless of whether it's current_user or some proxy.
I don't really understand how something like masquerade would work with sub-requests. I also don't see an elegant solution regarding how we then manage which user is returned from authentication from sub-requests?
EDIT: Also @Crell, if we are deciding the user can absolutely not be changed, how do you propose to fix this in tests? Where the current user is set on the container E.g. in drupalLogin().
Comment #99
damiankloip CreditAttribution: damiankloip commentedComment #100
andypostAs I said before - masquerade does not need sub requests! It does logout->login and storing old UID in new session, nothing more
Comment #101
damiankloip CreditAttribution: damiankloip commented@andypost, great, so how do you do that with what the guys are proposing above then? When you cannot change the user during a request?
Comment #102
andypost@damiankloip Currently code is broken/not-ported so I follow core.
Core does
user_logout()
with followinghook_user_logout()
anduser_login_finalize()
withhook_user_login()
so both should care about container state, the masquerade will call them coherently and redirect the visitor back to previous URLComment #103
dawehnerJust to be clear, I don't understand why we assume that drupal is a academial project without real world usecases, even inside core. Yes (INSERT arbitrary point), but reality is different.
Personally I would just love to get the work in comment 94 in.
Comment #104
dawehner94: 2180109-94.patch queued for re-testing.
Comment #106
damiankloip CreditAttribution: damiankloip commentedReroll of patch in #94, just in case we want to use that (and maybe remove the set stuff..).
Comment #109
effulgentsia CreditAttribution: effulgentsia commentedHi folks! Joining the party late here. I read through all of the issue comments, but not through the patch or any IRC conversations.
Thoughts? Would this address everyone's goals?
Comment #110
dawehnerJust playing around with the idea. It is still odd that we consider explicitness as a bad idea in the first place.
Comment #112
dawehnerSome fixes.
Comment #113
joelpittet@dawehner if this helps at all, I've itemized each remaining global $user replacement into it's own patch so you can grab the whole thing or each one you need:
#2213899: Remove global $user wherever possible (outside bootstrap and authentication) Round 2
Comment #115
effulgentsia CreditAttribution: effulgentsia commentedRetitling for the duration that the latest patch on this issue reflects this approach.
Do we need these? There should be no acceptable reason for consuming code to access properties directly, since this is only about implementing AccountInterface, not UserInterface. In fact, I wonder if we should even keep these but have them throw exceptions.
Comment #116
dawehner@effulgentsia
Oh I just assumed we would need it, as maybe some code will check for UserInterface and in that case do something with it.
In general the current failure is somehow caused by a memory problem. Is that a know problem, maybe related AGAIN with serialization?
Comment #117
dawehner112: current_user-2180109-112.patch queued for re-testing.
Comment #119
dawehner.
Comment #120
Berdirnamespace mismatch.
Is this a good name for this?
Should it maybe be AccountProxy instead?
// Reset the current user to ensure that new calls will receive the accordingly authenticated user?
Better suggestions anyone?
Maybe something like this instead:
// If the passed account is already proxied, use the actual account instead to prevent loops.
Yes, can we try to leave this out? Wondering what's going to happen...
Given that we use \Drupal:: anyway, simplify to use currentUser() ?
Comment #121
Crell CreditAttribution: Crell commentedAccountProxy? (Technically the fact that it's lazy is an implementation detail.)
This method makes me nervous. Why are we allowing this? I can't think of a (good) use case.
I agree that these aren't really necessary.
Another advantage of the transparent proxy approach: Code that takes an AccountInterface as a parameter or constructor dependency can, under testing, take a real account object rather than the proxy object, which simplifies the test.
Comment #122
damiankloip CreditAttribution: damiankloip commentedRerolled with most of those changes; Removed the magic functions from AccountProxy. Whether or not we should allow the setting of an account has been going on for the duration of this issue (and a fair bit outside it). WE NEED TO REACH CONSENSUS ON THIS :)
I think we might also want to introduce another interface to cover the setAccount method if we decide to allow this (I am in the yes camp).
Comment #123
dawehnerYeah I am not sure whether the response from crell was meant like this. We discussed usecases for quite a while
Comment #125
catchUntil all the code in core that can possibly be executed during a cron run or queue processing has the user injected, there are use cases for the setAccount() method.
Comment #126
dawehnerPretty wtf. let's see whether it passes.
Comment #128
damiankloip CreditAttribution: damiankloip commentedI think this should fix those fails.
I think we need to consider still adding a new interface for this proxy? As we rely on the setAccount() in places, we could extend AccountInterface? Then we probably need to do something with the Cron::run code based on this patch.
Comment #129
dawehnerNot convinced that we actually have to. Let's talk about that in a follow, ...
Comment #131
catch128: 2180109-128.patch queued for re-testing.
Comment #133
joelpittetFYI if it helps any, #128 fails the same in the CLI for:
\Drupal\file\Tests\UsageTest
\Drupal\file\Tests\DeleteTest
Drupal\system\Tests\Session\SessionTest
Though particular to the SessionTest:
the CLI show's $_SESSION as undefined (expected I assume). But the UI fails with
$_SESSION
contents of:Which is not the case in the UI without the patch. And UI only has that error and not the two "file" test fails.
Comment #134
joelpittetThis fixes the two cron fails locally.
Comment #135
joelpittetRe: #128 this still needs to be addressed but if #134 passes maybe it would work as it should now and we can remove the todos and scrap the remaining
$GLOBALS['user']
from thatCron::run
function?Comment #136
joelpittetWell this does what I suggested in #135, it should be the same result and hopefully a 1 fail positive result for both.
Comment #138
damiankloip CreditAttribution: damiankloip commentedI'm sorry, I just don't see how the patch in #136 will work? afaict, this will end up with the anonymous user again as $original_user.
Comment #140
joelpittet@damiankloip re #138 It likely won't "work". I was only reading through what was there, playing with what I saw and noticed other patches were replacing global $user and that pattern seemed to fix the 2 fails for cron locally. So disclaimer I don't know what is supposed to happen there, just saw an opportunity to push this patch one inch closer...
Though unless #134 is a testbot hiccup it seems I went in the wrong direction and both my patches should be ignored, but I'll retest it to be sure.
Comment #141
joelpittet134: 2180109-user-proxy-134.patch queued for re-testing.
Comment #143
joelpittetTalked to @damiankloip on IRC and now I understand: $original_user should be a referenced variable to the original proxy object and be overwritten on the next call. But I'm confused at why #134 is failing so bad and #136 is doing what I wanted #134 to do...
Comment #144
dawehnerThis are changes you can do to get the session test green.
Comment #146
effulgentsia CreditAttribution: effulgentsia commentedI haven't read the patches since #119, so not sure how much a setAccount() is still needed by core itself. However, I'll reiterate my position from #109.7: if we can make core work without it, then the default 'current_user' service shouldn't have it, but we should provide a MutableAccountProxy class for tests and questionable contrib projects to use if necessary.
Comment #147
dawehnerTo be honest I kinda think that people who still discuss this should have a look at what Drupal is today. This patch itself changes the current user in varios places, from session.inc over cron and simpletest.
Comment #148
cosmicdreams CreditAttribution: cosmicdreams commentedI'm patiently waiting for a good outcome with this issue. I think that #2205295: Objectify session handler may be stalled until we can benefit from the work of this patch.
Comment #149
dawehnerThat one is sadly especially hard. Yes #2205295: Objectify session handler would help a little bit, but the actual problem is probably not in there.
Comment #151
dawehnerOkay here is the thing, all changes since #2180109-136: Change the current_user service to a proxy changed session.inc and how it works internally. This is problematic because
acccessing the current user in session.inc potentially triggers authentication again.
So the newest patch started back from #136 and just fixes the test failure. From there one the bad state of session.inc
needs to be accepted, as this patch does not interfere with it.
The test failure is caused by having $_SESSION initialized lazyly, which means it is not available all the time.
Comment #153
dawehnerAfter some discussion with tim:
Comment #155
dawehnerBetter ...
Comment #157
dawehnerOkay.
Comment #158
ParisLiakos CreditAttribution: ParisLiakos commentedLooks sane to me. thanks dawehner!
does that mean that we can get rid of ContainerAware from this class?
i suppose the use of inheritdoc here is wrong:) where does it inherit from?
Comment #159
damiankloip CreditAttribution: damiankloip commentedNice work, thanks for getting this green again! I think it just needs the current_user_service injected into cron now too. Then we have a satisfied use case in core?
Comment #160
klausiThis should have an @todo that setting the user is evil and Cron should do it's own kernel or service container instead, which does not do any authentication in the first place if it only wants to operate with anonymous users.
does not match the namespace.
This should have more documentation why we need this.
"We need this for legacy code which relies on switching the global user instead of injecting the user or starting a new request." -- could be improved.
This should have a big fat warning "do not use this! Inject the desired user into dependent code instead".
why doesn't this use ->setAccount() on the proxy? Are you switching out the proxy here?
we are not removing the globalness of $_SESSION, since it is global by definition in PHP. And this should have an @todo that we should remove this call once session.inc and related session handling is refactored properly.
Comment #161
znerol CreditAttribution: znerol commentedI'm glad that the on-construction-authentication via the factory method is gone here. I'm sad that a new dependency on the synchronized request service is introduced.
From reading the rest of the patch I infer that the intention behind this dependencies are the following:
So, before the authentication did only take place when entering a master request. This is actually the proper approach because it is save to expect that those authentication parameters the client supplied will not change during the request/response cycle. There is no point in reauthenticating a subrequest, and thus there is no point in invalidating the user session when the request object changes. Let's just keep that behavior.
Let's get rid of that for the reasons stated above.
Comment #162
damiankloip CreditAttribution: damiankloip commentedThat's not strictly true, see the issue summary. Currently that event listener is invoked usually after the current_user factory service has authenticated once anyway. So that is not enforced.
Sorry, but the authentication manager will need the request passed in to authenticate, if it happens to be a sub request, that will be passed in as well. Saying no auth should ever happen on a sub request is not right IMO.
Comment #163
znerol CreditAttribution: znerol commentedThat's right, I consider this as a bug and not as a feature. When accessing the current user (proxy) before the request was authenticated, I'd rather expect an anonymous user to be returned. Authentication should only happen at one (deterministic) point in time in the request/response cycle.
Please give an example.
Comment #164
damiankloip CreditAttribution: damiankloip commented1. I'm sorry but you cannot rely on that, as outlined in this issue. Anything can ask for a current user at any point before the auth event has happened. So we might as well not have it.
2. A few examples outlined in this issue - If something like simplenews decided to use a sub request approach in the future, you would need to pass in a request and authenticate.
Comment #165
znerol CreditAttribution: znerol commentedThis is exactly the reason for the proxy approach. A service consuming the
current_user
proxy on construction time will no longer be stuck with the anonymous user. The authentication fires long before controllers actual will act on the injected object. If we discover that there is legacy code requiring an authenticated user before the event fires, we always can temporarily move the authentication step into a bootstrapping phase. However I doubt that this will be necessary.Nope. Authentication is different to masquerading. If simplenews decides to run under another user, it always can choose to replace the currently active user account (i.e. via the proxy). This is possible independent of the approach taken (with / without subrequests). Authentication (read: validate client supplied credentials) must be exactly once early in the request. The client supplied credentials never change during a request / response cycle, regardless of how many subrequests the main request triggers.
Comment #166
damiankloip CreditAttribution: damiankloip commentedWell, not if we are setting the user ourselves. We don't need to authenticate because we just set what we want.
How do you know, or how can you control what calling code is going to do? You can't.
Comment #167
dawehnerNote: This is just a temporary container, without anything actually booted up. We especially can't create an authentication manager
which is needed for that.
I would like to postpone the cleaning up, this here solves actual problems, while not relying on the request is certainly nice, but
does not solve us multiple criticals.
Comment #168
klausishould be "instead of".
"This proxy object avoids multiple invocations of the authentication manager which can happen if the current user is accessed in constructors. It also allows legacy code to change the current user where the user cannot be directly injected into dependent code." -- is that better?
"Instead, make sure to inject the desired user object into the dependent code directly" -- is that better?
Actually this documentation should be on the interface. Same for setAccount(). Use inheritdoc here.
But otherwise I think we should move forward with this. I don't see the point of having the authentication in a fixed place at the beginning of the request - it only needs to happen if the account is actually used.
And to re-iterate one more time: we need to setAccount() method on the proxy because there will be Drupal 8 code needing to set it, unfortunately. Example: Simplenews needs to built an email for a specific user where a View is invoked, which renders some entity, which invokes the translation system to render some text, which needs access to the user's preferred language. It can be very hard to impossible to pass down that language over 5 levels - we are not there yet in all situations in D8.
Comment #169
dawehnerThis is a little bit embarassing.
Comment #170
dawehnerThis is not necesarry an API change, given that we didn't had an API before.
Comment #171
aheimlichYou're not passing
$exclude_locked_roles
on to$this->getAccount()->getRoles()
.Comment #172
klausiklausi opened a new pull request for this issue.
Comment #173
MartijnBraam CreditAttribution: MartijnBraam commentedI applied the patch and tested issue #2113681: Node author can't be set when posting via HAL. It seems to be working.
Fixed issue in comment #171
Comment #175
klausiCross-post with MartijnBraam, sorry.
Just fixed the $exclude_locked_roles parameter, interdiff https://github.com/klausi/drupal/commit/d7cc212bebe8c522384835046f837e9d...
Otherwise I think this is RTBC. I played around with avoiding the \Drupal::currentUser()->getAccount(); call in SessionTestSubscriber, but the alternative is only to initialize the session at some hardcoded place, which we don't want. So the implication is right now that you have to call \Drupal::currentUser()->getAccount(); before using global $_SESSION when no user account is involved at all. This can/will be fixed by refactoring the session system, which is not part of this issue.
So we only have to wait for the testbot now.
Comment #176
damiankloip CreditAttribution: damiankloip commentedYes, +1 on that. I think this is ready.
Comment #177
ParisLiakos CreditAttribution: ParisLiakos commentedsince everyone ignored me, i will roll a patch instead
Comment #178
damiankloip CreditAttribution: damiankloip commentedOh nice, thanks Paris. I thought that got addressed! Still RTBC.
Comment #180
ParisLiakos CreditAttribution: ParisLiakos commentedit helps if you also remove the setContainer call for services.yml, right?:)
Comment #181
damiankloip CreditAttribution: damiankloip commentedYes, that will help a small amount.
Comment #182
dawehnerOH paris, we don't ignore you. the discussion just got crazy so we accidentally skipped it.
Comment #183
Crell CreditAttribution: Crell commentedI'd also like to see a note either on the setCurrentUser() method or in the Cron class (or both) saying "don't do this, it's for legacy only, cron needs refactoring". Otherwise, I am +1 on the latest patch.
Comment #184
catchCommitted/pushed to 8.x, thanks!
Comment #186
catchCross-posted with Crell, but either way I'd prefer to see an issue about that. It's not cron that needs refactoring. Cron just needs to use whichever the recommended mechanism is to ensure that anything which might get rendered during the cron run (for example nodes during search indexing) is rendered as if it's being viewed by the anonymous user. Once we have that mechanism, cron could be converted to it, but it's just using what we have at the moment.