-
-
Notifications
You must be signed in to change notification settings - Fork 550
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
XWIKI-22430: Logging out does not unlock pages that were being edited #3380
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,7 +64,6 @@ | |
import org.hibernate.query.Query; | ||
import org.slf4j.Logger; | ||
import org.suigeneris.jrcs.rcs.Version; | ||
import org.xwiki.bridge.event.ActionExecutingEvent; | ||
import org.xwiki.component.annotation.Component; | ||
import org.xwiki.component.manager.ComponentLookupException; | ||
import org.xwiki.component.manager.ComponentManager; | ||
|
@@ -86,7 +85,9 @@ | |
import org.xwiki.observation.event.Event; | ||
import org.xwiki.query.QueryException; | ||
import org.xwiki.query.QueryManager; | ||
import org.xwiki.security.authentication.UserUnauthenticatedEvent; | ||
import org.xwiki.store.UnexpectedException; | ||
import org.xwiki.user.UserReferenceSerializer; | ||
import org.xwiki.wiki.descriptor.WikiDescriptorManager; | ||
import org.xwiki.wiki.manager.WikiManagerException; | ||
|
||
|
@@ -176,6 +177,10 @@ public class XWikiHibernateStore extends XWikiHibernateBaseStore implements XWik | |
@Named("local") | ||
private EntityReferenceSerializer<String> localEntityReferenceSerializer; | ||
|
||
@Inject | ||
@Named("document") | ||
private UserReferenceSerializer<DocumentReference> userReferenceSerializer; | ||
|
||
@Inject | ||
private ComponentManager componentManager; | ||
|
||
|
@@ -2055,7 +2060,7 @@ private void registerLogoutListener() | |
{ | ||
this.observationManager.addListener(new EventListener() | ||
{ | ||
private final Event ev = new ActionExecutingEvent(); | ||
private static final List<Event> EVENT_LIST = List.of(new UserUnauthenticatedEvent()); | ||
|
||
@Override | ||
public String getName() | ||
|
@@ -2066,59 +2071,60 @@ public String getName() | |
@Override | ||
public List<Event> getEvents() | ||
{ | ||
return Collections.<Event>singletonList(this.ev); | ||
return EVENT_LIST; | ||
} | ||
|
||
@Override | ||
public void onEvent(Event event, Object source, Object data) | ||
{ | ||
if ("logout".equals(((ActionExecutingEvent) event).getActionName())) { | ||
final XWikiContext ctx = (XWikiContext) data; | ||
if (ctx.getUserReference() != null) { | ||
releaseAllLocksForCurrentUser(ctx); | ||
} | ||
UserUnauthenticatedEvent userUnauthenticatedEvent = (UserUnauthenticatedEvent) event; | ||
if (userUnauthenticatedEvent.getUserReference() != null) { | ||
DocumentReference userDoc = XWikiHibernateStore.this.userReferenceSerializer.serialize( | ||
userUnauthenticatedEvent.getUserReference()); | ||
releaseAllLocksForUser(userDoc, (XWikiContext) data); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Release all of the locks held by the currently logged in user. | ||
* | ||
* @param ctx the XWikiContext, used to start the connection and get the user name. | ||
*/ | ||
private void releaseAllLocksForCurrentUser(final XWikiContext ctx) | ||
private void releaseAllLocksForUser(final DocumentReference userDoc, final XWikiContext context) | ||
{ | ||
try { | ||
executeWrite(ctx, session -> { | ||
executeWrite(context, session -> { | ||
final Query query = session.createQuery("delete from XWikiLock as lock where lock.userName=:userName"); | ||
// Using deprecated getUser() because this is how locks are created. | ||
// It would be a maintainibility disaster to use different code paths | ||
// for calculating names when creating and removing. | ||
query.setParameter("userName", ctx.getUser()); | ||
query.setParameter("userName", this.compactWikiEntityReferenceSerializer.serialize(userDoc)); | ||
query.executeUpdate(); | ||
|
||
return null; | ||
}); | ||
} catch (Exception e) { | ||
String msg = "Error while deleting active locks held by user."; | ||
try { | ||
this.endTransaction(ctx, false); | ||
this.endTransaction(context, false); | ||
} catch (Exception utoh) { | ||
msg += " Failed to commit OR rollback [" + utoh.getMessage() + "]"; | ||
} | ||
throw new UnexpectedException(msg, e); | ||
} | ||
|
||
// If we're in a non-main wiki & the user is global, | ||
// switch to the global wiki and delete locks held there. | ||
if (!ctx.isMainWiki() && ctx.isMainWiki(ctx.getUserReference().getWikiReference().getName())) { | ||
final String cdb = ctx.getWikiId(); | ||
if (this.wikiDescriptorManager.isMainWiki(userDoc.getWikiReference().getName())) { | ||
try { | ||
ctx.setWikiId(ctx.getMainXWiki()); | ||
this.releaseAllLocksForCurrentUser(ctx); | ||
} finally { | ||
ctx.setWikiId(cdb); | ||
String currentWiki = context.getWikiId(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would probably make more sense to use getWikiReference here, it avoids creating a new WikiReference just to restore it. |
||
for (String wikiId : this.wikiDescriptorManager.getAllIds()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels too big a change for a LTS (I'm afraid logout on myxwiki.org is going to be quite long now, for example). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly I wasn't really sure about this loop: it feels a bit overkill but on the other side that's the only way to guarantee that a user properly has release all locks to my knowledge. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not overkill, the user could have lock anywhere in the farm (there are many use cases where you have only main wiki users and various subwikis, xwiki.org for example). It's just very expensive in a farm like myxwiki.org (I guess we might want to move the locks to Solr eventually, or just stop with the locks since that's another thing we talked about). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so it will impact perf, you think it's enough to prevent backporting?
Interesting idea. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I updated the description to not backport. |
||
if (!currentWiki.equals(wikiId)) { | ||
context.setWikiReference(new WikiReference(wikiId)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels simpler to use setWikiId here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought setWikiId was deprecated. Will check. |
||
this.releaseAllLocksForUser(userDoc, context); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this fail, the context wiki will be broken. Would be better to restore the context wiki only once in a try/finally around the whole |
||
context.setWikiReference(new WikiReference(currentWiki)); | ||
} | ||
} | ||
} catch (WikiManagerException e) { | ||
this.logger.error("Error for getting list of wikis to release locks for user [{}]", userDoc, e); | ||
tmortagne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,21 +38,21 @@ | |
|
||
import com.xpn.xwiki.XWikiContext; | ||
import com.xpn.xwiki.XWikiException; | ||
import com.xpn.xwiki.internal.user.UserAuthenticatedEventNotifier; | ||
import com.xpn.xwiki.internal.user.UserAuthenticationEventNotifier; | ||
import com.xpn.xwiki.web.Utils; | ||
|
||
public class MyFormAuthenticator extends FormAuthenticator implements XWikiAuthenticator | ||
{ | ||
private static final Logger LOGGER = LoggerFactory.getLogger(MyFormAuthenticator.class); | ||
|
||
private UserAuthenticatedEventNotifier userAuthenticatedEventNotifier; | ||
private UserAuthenticationEventNotifier userAuthenticationEventNotifier; | ||
|
||
private UserAuthenticatedEventNotifier getUserAuthenticatedEventNotifier() | ||
private UserAuthenticationEventNotifier getUserAuthenticatedEventNotifier() | ||
{ | ||
if ( this.userAuthenticatedEventNotifier == null ) { | ||
this.userAuthenticatedEventNotifier = Utils.getComponent(UserAuthenticatedEventNotifier.class); | ||
if ( this.userAuthenticationEventNotifier == null ) { | ||
this.userAuthenticationEventNotifier = Utils.getComponent(UserAuthenticationEventNotifier.class); | ||
} | ||
return this.userAuthenticatedEventNotifier; | ||
return this.userAuthenticationEventNotifier; | ||
} | ||
|
||
/** | ||
|
@@ -168,7 +168,7 @@ public boolean processLogin(SecurityRequestWrapper request, HttpServletResponse | |
|
||
request.setUserPrincipal(principal); | ||
|
||
this.getUserAuthenticatedEventNotifier().notify(principal.getName()); | ||
this.getUserAuthenticatedEventNotifier().notifyUserAuthenticated(principal.getName()); | ||
|
||
} else { | ||
// Failed to authenticate, better cleanup the user stored in the session | ||
|
@@ -240,7 +240,7 @@ public boolean processLogin(String username, String password, String rememberme, | |
|
||
request.setUserPrincipal(principal); | ||
|
||
this.getUserAuthenticatedEventNotifier().notify(principal.getName()); | ||
this.getUserAuthenticatedEventNotifier().notifyUserAuthenticated(principal.getName()); | ||
|
||
Boolean bAjax = (Boolean) context.get("ajax"); | ||
if ((bAjax == null) || (!bAjax.booleanValue())) { | ||
|
@@ -292,7 +292,8 @@ public boolean processLogout(SecurityRequestWrapper securityRequestWrapper, | |
HttpServletResponse httpServletResponse, URLPatternMatcher urlPatternMatcher) throws Exception | ||
{ | ||
boolean result = super.processLogout(securityRequestWrapper, httpServletResponse, urlPatternMatcher); | ||
if (result == true) { | ||
if (result) { | ||
this.getUserAuthenticatedEventNotifier().notifyUserUnauthenticated(securityRequestWrapper.getRemoteUser()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, this should probably be called after the forgetLogin. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add a comment about that one: I thought the same at first but then what I'm doing doesn't work anymore because forgetLogin reset the wrapped request user to null. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, that's why the event should be sent after. You just need to remember the user reference before it's reseted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok |
||
if (this.persistentLoginManager != null) { | ||
this.persistentLoginManager.forgetLogin(securityRequestWrapper, httpServletResponse); | ||
} | ||
|
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.
Why does this release all locks? Shouldn't this only release locks of the current session of the user? I mean when I log out on my phone, this shouldn't release locks that I'm holding on my desktop browser, should it?
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.
We don't really have the concept of session right now in the locks, but yes ideally they should.
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.
That would be an improvment to add in the future I think, the expectation of current mechanism is that it does release all locks
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.
Could be done at the same time as moving the locks to solr for better perf as Thomas suggested.
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.
Storing a reference to all active locks of the user in the session would solve both the session and the performance problem.
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.
It would probably be enough for the performance problem, yes (there is very little chance that the user has locks on many wikis at the same time in practice).
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'd do that as part of another improvment ticket: IMO it's out of the scope of current PR.
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.
It's kind of the same scope as the
for
you added, the session references was another way to fix this problem (but yes, it's more complex than adding a for :)). It would also make the new event useless for this need, as the trigger would be more the session invalidation (which is also triggered by the logout) and not the logout (as a bonus this would work with all authenticators right away).