-
Notifications
You must be signed in to change notification settings - Fork 12
Do not block access for ALL users when the license user limit is exceeded #192 #207
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
base: master
Are you sure you want to change the base?
Changes from 20 commits
8dd0b11
47e64be
e4ac1ef
52f9e35
7fc507a
c0cbb9f
2de24c6
d3eb916
2d5a325
2ba4b4b
8051097
762ea39
a11d4e5
6b4cc16
46e0b37
4cb3e21
60db9d9
461281a
457509c
fe20bee
8c42396
6ef265a
82100e2
7433457
661e2ea
c28e399
dc64443
6b84ba7
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 |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| /* | ||
| * See the NOTICE file distributed with this work for additional | ||
| * information regarding copyright ownership. | ||
| * | ||
| * This is free software; you can redistribute it and/or modify it | ||
| * under the terms of the GNU Lesser General Public License as | ||
| * published by the Free Software Foundation; either version 2.1 of | ||
| * the License, or (at your option) any later version. | ||
| * | ||
| * This software is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
| * Lesser General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Lesser General Public | ||
| * License along with this software; if not, write to the Free | ||
| * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA | ||
| * 02110-1301 USA, or see the FSF site: http://www.fsf.org. | ||
| */ | ||
| package com.xwiki.licensing.internal; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.xwiki.component.annotation.Role; | ||
| import org.xwiki.model.reference.DocumentReference; | ||
|
|
||
| import com.xpn.xwiki.XWikiContext; | ||
| import com.xpn.xwiki.doc.XWikiDocument; | ||
|
|
||
| /** | ||
| * Interface for extensions which manage users, so the license user limit can only take into account the users managed | ||
| * by those extensions for computing the user limit. | ||
| * <p> | ||
| * Components implementing this interface should be named as extension id, so the user manager is discoverable. | ||
oanalavinia marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * @version $Id$ | ||
| * @since 1.31 | ||
| */ | ||
| @Role | ||
| public interface AuthExtensionUserManager | ||
| { | ||
| /** | ||
| * True if this manager manages the given user. Should return false for invalid licenses. | ||
| * | ||
| * @param user the user to check | ||
| * @return true if this manager manages the given user. | ||
| */ | ||
| boolean managesUser(DocumentReference user); | ||
|
|
||
| /** | ||
| * True if this manager manages the given user. Should return false for invalid licenses. | ||
| * | ||
| * @param user the user to check | ||
| * @return true if this manager manages the given user. | ||
| */ | ||
| boolean managesUser(XWikiDocument user); | ||
|
|
||
| /** | ||
| * Get a list of all users managed by this manager. | ||
abrassat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * | ||
| * @return a list of all users managed by this manager | ||
| */ | ||
| List<XWikiDocument> getManagedUsers(); | ||
|
|
||
| /** | ||
| * Get a list of all active users managed by this manager. | ||
| * | ||
| * @return a list of all active users managed by this auth extension | ||
| */ | ||
| List<XWikiDocument> getActiveManagedUsers(); | ||
|
|
||
| /** | ||
| * Resolve a username (the string used by a user to log in) to the XWiki user page. This method should resolve all | ||
| * valid usernames that a user can use to log into their account. | ||
| * | ||
| * @param username the username used by a user to login | ||
| * @param context XWiki context, to help in querying pages | ||
| * @return a reference to the user page with the XWiki.XWikiUsers object, or null if not existent | ||
| */ | ||
| DocumentReference getUserDocFromUsername(String username, XWikiContext context); | ||
abrassat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,19 +21,27 @@ | |
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.SortedSet; | ||
| import java.util.TreeSet; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import javax.inject.Inject; | ||
| import javax.inject.Named; | ||
| import javax.inject.Singleton; | ||
|
|
||
| import org.apache.commons.lang3.builder.CompareToBuilder; | ||
| import org.slf4j.Logger; | ||
| import org.xwiki.bridge.event.DocumentCreatedEvent; | ||
| import org.xwiki.bridge.event.DocumentDeletedEvent; | ||
| import org.xwiki.bridge.event.DocumentUpdatedEvent; | ||
| import org.xwiki.component.annotation.Component; | ||
| import org.xwiki.model.reference.DocumentReference; | ||
| import org.xwiki.model.reference.LocalDocumentReference; | ||
| import org.xwiki.observation.AbstractEventListener; | ||
| import org.xwiki.observation.event.Event; | ||
| import org.xwiki.observation.event.filter.EventFilter; | ||
| import org.xwiki.observation.event.filter.RegexEventFilter; | ||
| import org.xwiki.query.Query; | ||
| import org.xwiki.query.QueryException; | ||
| import org.xwiki.query.QueryFilter; | ||
|
|
@@ -54,6 +62,11 @@ | |
| @Singleton | ||
| public class UserCounter | ||
| { | ||
| protected static final String BASE_USER_QUERY = ", BaseObject as obj, IntegerProperty as prop " | ||
oanalavinia marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| + "where doc.space = 'XWiki' " | ||
| + "and doc.fullName = obj.name and obj.className = 'XWiki.XWikiUsers' and prop.id.id = obj.id " | ||
| + "and prop.id.name = 'active' and prop.value = '1'"; | ||
|
|
||
| @Inject | ||
| private Logger logger; | ||
|
|
||
|
|
@@ -73,6 +86,12 @@ public class UserCounter | |
|
|
||
| private Long cachedUserCount; | ||
|
|
||
| // A set of users on the instance, sorted by creation date. | ||
| private SortedSet<XWikiDocument> cachedSortedUsers; | ||
|
Contributor
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. Could you see how we could do some performance tests? We would need to know that this doesn't fail on an instance with a loot of users 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'm guessing this depends on the JVM allocated memory. Iirc, this comment mentioned that the cache size should be ok (at least proportionally to the instance size). I'll try to create a test with ~10.000 users on the docker instance, but I'll have to see if the test instance can really support 10k users.
Contributor
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 be nice if we could have it. I know there were some script to create multiple users / wikis |
||
|
|
||
| // Helper to find users in constant time. | ||
|
||
| private Map<DocumentReference, XWikiDocument> cachedSortedUsersLookupTable; | ||
abrassat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Event listener that invalidates the cached user count when an user is added, deleted or the active property's | ||
| * value is changed. | ||
|
|
@@ -94,6 +113,8 @@ public static class UserListener extends AbstractEventListener | |
|
|
||
| protected static final LocalDocumentReference USER_CLASS = new LocalDocumentReference("XWiki", "XWikiUsers"); | ||
|
|
||
| private static final EventFilter XWIKI_SPACE_FILTER = new RegexEventFilter("(.*:)?XWiki\\..*"); | ||
|
|
||
| @Inject | ||
| private UserCounter userCounter; | ||
|
|
||
|
|
@@ -102,8 +123,8 @@ public static class UserListener extends AbstractEventListener | |
| */ | ||
| public UserListener() | ||
| { | ||
| super(HINT, | ||
| Arrays.asList(new DocumentCreatedEvent(), new DocumentUpdatedEvent(), new DocumentDeletedEvent())); | ||
| super(HINT, Arrays.asList(new DocumentCreatedEvent(XWIKI_SPACE_FILTER), | ||
| new DocumentUpdatedEvent(XWIKI_SPACE_FILTER), new DocumentDeletedEvent(XWIKI_SPACE_FILTER))); | ||
abrassat marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -124,9 +145,38 @@ public void onEvent(Event event, Object source, Object data) | |
|
|
||
| if (newDocumentIsUser != oldDocumentIsUser || newActive != oldActive) { | ||
| // The user object is either added/removed or set to active/inactive. Invalidate the cached user count. | ||
| this.userCounter.cachedUserCount = null; | ||
| this.userCounter.flushCache(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Flush the cache of the user counter. | ||
| */ | ||
| public void flushCache() | ||
| { | ||
| this.cachedUserCount = null; | ||
| this.cachedSortedUsers = null; | ||
| this.cachedSortedUsersLookupTable = null; | ||
| } | ||
|
|
||
| /** | ||
| * Get the users sorted by creation date. | ||
| * | ||
| * @return the users, sorted by creation date. | ||
| */ | ||
| public SortedSet<XWikiDocument> getSortedUsers() throws WikiManagerException, QueryException | ||
| { | ||
| if (cachedSortedUsers == null) { | ||
| cachedSortedUsers = new TreeSet<>( | ||
| (e1, e2) -> new CompareToBuilder().append(e1.getCreationDate(), e2.getCreationDate()).build()); | ||
| for (String wikiId : wikiDescriptorManager.getAllIds()) { | ||
| cachedSortedUsers.addAll(getUsersOnWiki(wikiId)); | ||
abrassat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| cachedSortedUsersLookupTable = | ||
| cachedSortedUsers.stream().collect(Collectors.toMap(XWikiDocument::getDocumentReference, e -> e)); | ||
| } | ||
| return cachedSortedUsers; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -155,13 +205,40 @@ public long getUserCount() throws Exception | |
|
|
||
| private long getUserCountOnWiki(String wikiId) throws QueryException | ||
| { | ||
| StringBuilder statement = new StringBuilder(", BaseObject as obj, IntegerProperty as prop "); | ||
| statement.append("where doc.fullName = obj.name and obj.className = 'XWiki.XWikiUsers' and "); | ||
| statement.append("prop.id.id = obj.id and prop.id.name = 'active' and prop.value = '1'"); | ||
|
|
||
| Query query = this.queryManager.createQuery(statement.toString(), Query.HQL); | ||
| Query query = this.queryManager.createQuery(BASE_USER_QUERY, Query.HQL); | ||
| query.addFilter(this.uniqueFilter).addFilter(this.countFilter).setWiki(wikiId); | ||
| List<Long> results = query.execute(); | ||
| return results.get(0); | ||
| } | ||
|
|
||
| /** | ||
| * Return whether the given user is under the specified license user limit. | ||
| * | ||
| * @param user the user to check | ||
| * @param userLimit the license max user limit | ||
| * @return whether the given user is under the specified license user limit | ||
| */ | ||
| public boolean isUserUnderLimit(DocumentReference user, long userLimit) throws Exception | ||
| { | ||
| if (userLimit < 0 || userLimit <= getUserCount()) { | ||
| // Unlimited licenses should always return true. | ||
| // Also, skip the checks for instances with fewer users than the limit. | ||
| return true; | ||
| } | ||
| SortedSet<XWikiDocument> sortedUsers = getSortedUsers(); | ||
| // Lookup table is initialized in getSortedUsers(). | ||
| XWikiDocument userDocument = cachedSortedUsersLookupTable.get(user); | ||
| if (userDocument == null) { | ||
| return false; | ||
| } else { | ||
| return sortedUsers.headSet(userDocument).size() < userLimit; | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private List<XWikiDocument> getUsersOnWiki(String wikiId) throws QueryException | ||
| { | ||
| return this.queryManager.createQuery("select doc from XWikiDocument doc" + BASE_USER_QUERY, Query.HQL) | ||
| .setWiki(wikiId).execute(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.