mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-22 21:55:39 +00:00
Merge branch 'master' into bug5230-old
# Conflicts: # java/com.sap.sse.security.common/src/com/sap/sse/security/shared/PermissionChecker.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/AbstractRoleDefinitionDialog.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionEditDialog.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/SecurityDTOFactory.java # java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java
This commit is contained in:
@@ -6,9 +6,7 @@ Bundle-Version: 1.0.0.qualifier
|
||||
Bundle-Activator: com.sap.sse.security.userstore.mongodb.impl.Activator
|
||||
Bundle-Vendor: SAP
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
|
||||
Import-Package: com.sap.sse,
|
||||
com.sap.sse.util,
|
||||
org.json.simple,
|
||||
Import-Package: org.json.simple,
|
||||
org.osgi.framework;version="1.3.0",
|
||||
org.osgi.util.tracker;version="1.5.1"
|
||||
Bundle-ActivationPolicy: lazy
|
||||
@@ -21,7 +19,8 @@ Require-Bundle: org.apache.shiro.core;bundle-version="1.2.2",
|
||||
org.mongodb.mongo-java-driver;bundle-version="3.6.4",
|
||||
com.sap.sse.replication,
|
||||
com.sap.sse,
|
||||
com.sap.sse.security.interface
|
||||
com.sap.sse.security.interface,
|
||||
com.sap.sse.security
|
||||
Export-Package: com.sap.sse.security.userstore.mongodb,
|
||||
com.sap.sse.security.userstore.mongodb.impl;x-friends:="com.sap.sse.security.storemerging,com.sap.sse.security.test"
|
||||
Automatic-Module-Name: com.sap.sse.security.userstore.mongodb
|
||||
|
||||
+94
-20
@@ -2,12 +2,16 @@ package com.sap.sse.security.userstore.mongodb;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.concurrent.LockUtil;
|
||||
import com.sap.sse.concurrent.LockUtil.RunnableWithResult;
|
||||
import com.sap.sse.concurrent.NamedReentrantReadWriteLock;
|
||||
import com.sap.sse.concurrent.RunnableWithResult;
|
||||
import com.sap.sse.security.interfaces.AccessControlStore;
|
||||
import com.sap.sse.security.interfaces.UserStore;
|
||||
import com.sap.sse.security.shared.AccessControlListAnnotation;
|
||||
@@ -30,6 +34,20 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
* maps from object ID string representations to the access control lists for the respective key object
|
||||
*/
|
||||
private final ConcurrentHashMap<QualifiedObjectIdentifier, AccessControlListAnnotation> accessControlLists;
|
||||
|
||||
/**
|
||||
* For quick lookup of denying ACLs during meta-permission checks (permission to grant a permission) this map
|
||||
* contains a subset of the {@link #accessControlList} map, keyed by the
|
||||
* {@link QualifiedObjectIdentifier#getTypeIdentifier() type identifier} (which is a {@link String}) of the objects
|
||||
* to which those ACLs pertain, and as a nested map, by the {@link UserGroup} that is the key in the ACL that denies
|
||||
* permission to an action (could be the {@code null} group, in that case meaning the anonymous users). Access to
|
||||
* this map, like access to {@link #accessControlLists}, has to be synchronized under the
|
||||
* {@link #lockForManagementMappings}.<p>
|
||||
*
|
||||
* As the inner maps are implemented as non-concurrent maps, {@code null} keys are permissible, so the anonymous
|
||||
* group is actually represented as a {@code null} key.
|
||||
*/
|
||||
private final ConcurrentHashMap<String, Map<UserGroup, Set<QualifiedObjectIdentifier>>> accessControlListsWithDenials;
|
||||
|
||||
/**
|
||||
* maps from object ID string representations to the ownership information for the respective key object
|
||||
@@ -42,7 +60,17 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
* (prevented by the lock)
|
||||
*/
|
||||
private final ConcurrentHashMap<User, Set<OwnershipAnnotation>> userToOwnership;
|
||||
|
||||
/**
|
||||
* The anonymous {@code null} group is represented by {@link #NULL_GROUP} instead as a {@link ConcurrentHashMap}
|
||||
* cannot handle {@code null} keys/values.
|
||||
*/
|
||||
private final ConcurrentHashMap<UserGroup, Set<OwnershipAnnotation>> userGroupToOwnership;
|
||||
|
||||
/**
|
||||
* The anonymous {@code null} group is represented by {@link #NULL_GROUP} instead as a {@link ConcurrentHashMap}
|
||||
* cannot handle {@code null} keys/values.
|
||||
*/
|
||||
private final ConcurrentHashMap<UserGroup, Set<AccessControlListAnnotation>> userGroupToAccessControlListAnnotation;
|
||||
|
||||
private static final UserGroupImpl NULL_GROUP = new UserGroupImpl(null, "<null group>");
|
||||
@@ -75,12 +103,12 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
public AccessControlStoreImpl(final DomainObjectFactory domainObjectFactory,
|
||||
final MongoObjectFactory mongoObjectFactory, final UserStore userStore) {
|
||||
accessControlLists = new ConcurrentHashMap<>();
|
||||
accessControlListsWithDenials = new ConcurrentHashMap<>();
|
||||
ownerships = new ConcurrentHashMap<>();
|
||||
userToOwnership = new ConcurrentHashMap<>();
|
||||
userGroupToOwnership = new ConcurrentHashMap<>();
|
||||
userGroupToAccessControlListAnnotation = new ConcurrentHashMap<>();
|
||||
lockForManagementMappings = new NamedReentrantReadWriteLock("ownershipLock", true);
|
||||
|
||||
this.mongoObjectFactory = mongoObjectFactory;
|
||||
this.domainObjectFactory = domainObjectFactory;
|
||||
this.userStore = userStore;
|
||||
@@ -104,6 +132,7 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
}
|
||||
|
||||
private void internalAddACL(AccessControlListAnnotation acl) {
|
||||
assert lockForManagementMappings.isWriteLockedByCurrentThread();
|
||||
accessControlLists.put(acl.getIdOfAnnotatedObject(), acl);
|
||||
for (UserGroup owner : acl.getAnnotation().getActionsByUserGroup().keySet()) {
|
||||
internalMapUserGroupToACL(owner, acl);
|
||||
@@ -139,9 +168,9 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
final String displayNameOfAccessControlledObject) {
|
||||
return LockUtil.executeWithWriteLockAndResult(lockForManagementMappings,
|
||||
new RunnableWithResult<AccessControlListAnnotation>() {
|
||||
|
||||
@Override
|
||||
public AccessControlListAnnotation run() {
|
||||
removeAccessControlList(idOfAccessControlledObject);
|
||||
AccessControlListAnnotation acl = new AccessControlListAnnotation(new AccessControlList(),
|
||||
idOfAccessControlledObject, displayNameOfAccessControlledObject);
|
||||
accessControlLists.put(idOfAccessControlledObject, acl);
|
||||
@@ -166,6 +195,7 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
}
|
||||
|
||||
private AccessControlListAnnotation getOrCreateAcl(QualifiedObjectIdentifier idOfAccessControlledObject) {
|
||||
assert lockForManagementMappings.isWriteLockedByCurrentThread();
|
||||
return accessControlLists.computeIfAbsent(idOfAccessControlledObject,
|
||||
id->new AccessControlListAnnotation(new AccessControlList(), id, /* display name */ null));
|
||||
}
|
||||
@@ -199,34 +229,56 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
});
|
||||
}
|
||||
|
||||
private void internalMapUserGroupToACL(final UserGroup userGroup2, final AccessControlListAnnotation acl) {
|
||||
private void internalMapUserGroupToACL(final UserGroup userGroup, final AccessControlListAnnotation acl) {
|
||||
if (!lockForManagementMappings.isWriteLockedByCurrentThread()) {
|
||||
throw new IllegalStateException("Current thread has no write lock!");
|
||||
}
|
||||
|
||||
final UserGroup userGroup = userGroup2 == null ? NULL_GROUP : userGroup2;
|
||||
final UserGroup effectiveUserGroup = userGroup == null ? NULL_GROUP : userGroup;
|
||||
Set<AccessControlListAnnotation> currentACLsContainingGroup = userGroupToAccessControlListAnnotation
|
||||
.get(userGroup);
|
||||
.get(effectiveUserGroup);
|
||||
if (currentACLsContainingGroup == null) {
|
||||
currentACLsContainingGroup = Collections
|
||||
.newSetFromMap(new ConcurrentHashMap<AccessControlListAnnotation, Boolean>());
|
||||
userGroupToAccessControlListAnnotation.put(userGroup, currentACLsContainingGroup);
|
||||
userGroupToAccessControlListAnnotation.put(effectiveUserGroup, currentACLsContainingGroup);
|
||||
}
|
||||
currentACLsContainingGroup.add(acl);
|
||||
final String type = acl.getIdOfAnnotatedObject().getTypeIdentifier();
|
||||
// FIXME bug5239: add if an action is denied; remove if no action denied for group
|
||||
Map<UserGroup, Set<QualifiedObjectIdentifier>> aclsByGroupForType = accessControlListsWithDenials.get(type);
|
||||
final Set<String> deniedActions = acl.getAnnotation().getDeniedActions(userGroup);
|
||||
if (deniedActions == null || deniedActions.isEmpty()) {
|
||||
if (aclsByGroupForType != null) {
|
||||
Util.removeFromValueSet(aclsByGroupForType, userGroup, acl.getIdOfAnnotatedObject());
|
||||
}
|
||||
} else {
|
||||
if (aclsByGroupForType == null) {
|
||||
aclsByGroupForType = new HashMap<>();
|
||||
accessControlListsWithDenials.put(type, aclsByGroupForType);
|
||||
Util.addToValueSet(aclsByGroupForType, userGroup, acl.getIdOfAnnotatedObject());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void internalRemoveUserGroupToACLMapping(final UserGroup userGroup2,
|
||||
private void internalRemoveUserGroupToACLMapping(final UserGroup userGroup,
|
||||
final AccessControlListAnnotation acl) {
|
||||
if (!lockForManagementMappings.isWriteLockedByCurrentThread()) {
|
||||
throw new IllegalStateException("Current thread has no write lock!");
|
||||
}
|
||||
final UserGroup userGroup = userGroup2 == null ? NULL_GROUP : userGroup2;
|
||||
final UserGroup effectiveUserGroup = userGroup == null ? NULL_GROUP : userGroup;
|
||||
Set<AccessControlListAnnotation> currentACLsContainingGroup = userGroupToAccessControlListAnnotation
|
||||
.get(userGroup);
|
||||
.get(effectiveUserGroup);
|
||||
if (currentACLsContainingGroup != null) {
|
||||
currentACLsContainingGroup.remove(acl);
|
||||
if (currentACLsContainingGroup.isEmpty()) {
|
||||
userGroupToAccessControlListAnnotation.remove(userGroup);
|
||||
userGroupToAccessControlListAnnotation.remove(effectiveUserGroup);
|
||||
}
|
||||
}
|
||||
final String typeIdentifier = acl.getIdOfAnnotatedObject().getTypeIdentifier();
|
||||
final Map<UserGroup, Set<QualifiedObjectIdentifier>> aclsByGroupForEvent = accessControlListsWithDenials.get(typeIdentifier);
|
||||
if (aclsByGroupForEvent != null) {
|
||||
aclsByGroupForEvent.remove(userGroup);
|
||||
if (aclsByGroupForEvent.isEmpty()) {
|
||||
accessControlListsWithDenials.remove(typeIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,6 +386,12 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<OwnershipAnnotation> getOwnerhipsWithGroupOwner(UserGroup owningUserGroup) {
|
||||
final Set<OwnershipAnnotation> ownerships = userGroupToOwnership.get(owningUserGroup);
|
||||
return ownerships==null ? Collections.emptySet() : Collections.unmodifiableCollection(ownerships);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OwnershipAnnotation getOwnership(final QualifiedObjectIdentifier idOfOwnedObjectAsString) {
|
||||
@@ -371,6 +429,7 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
|
||||
private void removeAll() {
|
||||
accessControlLists.clear();
|
||||
accessControlListsWithDenials.clear();
|
||||
ownerships.clear();
|
||||
userGroupToAccessControlListAnnotation.clear();
|
||||
userGroupToOwnership.clear();
|
||||
@@ -392,14 +451,26 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AccessControlListAnnotation> getAccessControlListsForGroup(UserGroup group) {
|
||||
final Set<AccessControlListAnnotation> aclsForGroup = userGroupToAccessControlListAnnotation.get(group);
|
||||
return aclsForGroup == null ? null : Collections.unmodifiableSet(aclsForGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllOwnershipsFor(final UserGroup userGroup2) {
|
||||
final UserGroup userGroup = userGroup2 == null ? NULL_GROUP : userGroup2;
|
||||
public Map<UserGroup, Set<QualifiedObjectIdentifier>> getAccessControlListsWithDenials(String typeIdentifier) {
|
||||
final Map<UserGroup, Set<QualifiedObjectIdentifier>> aclsForType = accessControlListsWithDenials.get(typeIdentifier);
|
||||
return aclsForType == null ? null : Collections.unmodifiableMap(aclsForType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAllOwnershipsFor(final UserGroup userGroup) {
|
||||
final UserGroup effectiveUserGroup = userGroup == null ? NULL_GROUP : userGroup;
|
||||
LockUtil.executeWithWriteLock(lockForManagementMappings, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Set<OwnershipAnnotation> knownOwnerships = userGroupToOwnership.get(userGroup);
|
||||
Set<OwnershipAnnotation> knownOwnerships = userGroupToOwnership.get(effectiveUserGroup);
|
||||
if (knownOwnerships != null) {
|
||||
// do not use setOwnership, we know the user will not change, and we can use the more effective
|
||||
// remove
|
||||
@@ -411,17 +482,20 @@ public class AccessControlStoreImpl implements AccessControlStore {
|
||||
ownerships.put(ownership.getIdOfAnnotatedObject(), groupLessOwnership);
|
||||
mongoObjectFactory.storeOwnership(groupLessOwnership);
|
||||
}
|
||||
userGroupToOwnership.remove(userGroup);
|
||||
userGroupToOwnership.remove(effectiveUserGroup);
|
||||
}
|
||||
|
||||
Set<AccessControlListAnnotation> knownACLEntries = userGroupToAccessControlListAnnotation
|
||||
.get(userGroup);
|
||||
.get(effectiveUserGroup);
|
||||
if (knownACLEntries != null) {
|
||||
for (AccessControlListAnnotation acl : knownACLEntries) {
|
||||
internalRemoveUserGroupToACLMapping(userGroup, acl);
|
||||
acl.getAnnotation().setPermissions(effectiveUserGroup, Collections.emptySet());
|
||||
internalRemoveUserGroupToACLMapping(effectiveUserGroup, acl);
|
||||
mongoObjectFactory.storeAccessControlList(acl);
|
||||
}
|
||||
userGroupToAccessControlListAnnotation.remove(userGroup);
|
||||
userGroupToAccessControlListAnnotation.remove(effectiveUserGroup);
|
||||
for (final Entry<String, Map<UserGroup, Set<QualifiedObjectIdentifier>>> e : accessControlListsWithDenials.entrySet()) {
|
||||
e.getValue().remove(effectiveUserGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+720
-449
File diff suppressed because it is too large
Load Diff
+23
-7
@@ -4,19 +4,26 @@ import java.util.logging.Logger;
|
||||
|
||||
import org.osgi.framework.BundleActivator;
|
||||
import org.osgi.framework.BundleContext;
|
||||
import org.osgi.framework.ServiceReference;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.util.tracker.ServiceTracker;
|
||||
|
||||
import com.sap.sse.ServerInfo;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.mongodb.MongoDBService;
|
||||
import com.sap.sse.security.interfaces.AccessControlStore;
|
||||
import com.sap.sse.security.interfaces.PreferenceConverterRegistrationManager;
|
||||
import com.sap.sse.security.interfaces.UserStore;
|
||||
import com.sap.sse.security.subscription.SubscriptionApiService;
|
||||
import com.sap.sse.security.subscription.SubscriptionDataHandler;
|
||||
import com.sap.sse.security.userstore.mongodb.AccessControlStoreImpl;
|
||||
import com.sap.sse.security.userstore.mongodb.UserStoreImpl;
|
||||
import com.sap.sse.util.ServiceTrackerFactory;
|
||||
|
||||
public class Activator implements BundleActivator {
|
||||
private static final Logger logger = Logger.getLogger(Activator.class.getName());
|
||||
private static BundleContext context;
|
||||
private static ServiceTracker<SubscriptionApiService, SubscriptionApiService> subscriptionApiServiceTracker;
|
||||
private ServiceRegistration<?> accessControlStoreRegistration;
|
||||
private ServiceRegistration<?> userStoreRegistration;
|
||||
private PreferenceConverterRegistrationManager preferenceConverterRegistrationManager;
|
||||
@@ -24,7 +31,7 @@ public class Activator implements BundleActivator {
|
||||
static BundleContext getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -32,15 +39,13 @@ public class Activator implements BundleActivator {
|
||||
*/
|
||||
public void start(BundleContext bundleContext) throws Exception {
|
||||
Activator.context = bundleContext;
|
||||
final String defaultServerGroupName = System.getProperty(UserStore.DEFAULT_SERVER_GROUP_NAME_PROPERTY_NAME, ServerInfo.getName() + "-server");
|
||||
logger.info("Creating user store");
|
||||
final String defaultServerGroupName = System.getProperty(UserStore.DEFAULT_SERVER_GROUP_NAME_PROPERTY_NAME,
|
||||
ServerInfo.getName() + "-server");
|
||||
subscriptionApiServiceTracker = ServiceTrackerFactory.createAndOpen(context, SubscriptionApiService.class);
|
||||
final UserStoreImpl userStore = new UserStoreImpl(defaultServerGroupName);
|
||||
AccessControlStoreImpl accessControlStore = new AccessControlStoreImpl(userStore);
|
||||
accessControlStoreRegistration = context.registerService(AccessControlStore.class.getName(),
|
||||
accessControlStore, null);
|
||||
userStoreRegistration = context.registerService(UserStore.class.getName(),
|
||||
userStore, null);
|
||||
accessControlStoreRegistration = context.registerService(AccessControlStore.class.getName(), accessControlStore, null);
|
||||
userStoreRegistration = context.registerService(UserStore.class.getName(), userStore, null);
|
||||
preferenceConverterRegistrationManager = new PreferenceConverterRegistrationManager(bundleContext, userStore);
|
||||
logger.info("User store registered.");
|
||||
for (CollectionNames name : CollectionNames.values()) {
|
||||
@@ -60,4 +65,15 @@ public class Activator implements BundleActivator {
|
||||
Activator.context = null;
|
||||
}
|
||||
|
||||
public static SubscriptionDataHandler getSubscriptionDataHandler(String providerName) {
|
||||
if (subscriptionApiServiceTracker != null) {
|
||||
ServiceReference<SubscriptionApiService>[] serviceReferences = subscriptionApiServiceTracker.getServiceReferences();
|
||||
for (final ServiceReference<SubscriptionApiService> serviceReference : serviceReferences) {
|
||||
if (Util.equalsWithNull(serviceReference.getProperty(SubscriptionApiService.PROVIDER_NAME_OSGI_REGISTRY_KEY), providerName)) {
|
||||
return context.getService(serviceReference).getDataHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+80
-48
@@ -41,6 +41,9 @@ import com.sap.sse.security.shared.impl.Role;
|
||||
import com.sap.sse.security.shared.impl.User;
|
||||
import com.sap.sse.security.shared.impl.UserGroup;
|
||||
import com.sap.sse.security.shared.impl.UserGroupImpl;
|
||||
import com.sap.sse.security.shared.subscription.Subscription;
|
||||
import com.sap.sse.security.subscription.SubscriptionData;
|
||||
import com.sap.sse.security.subscription.SubscriptionDataHandler;
|
||||
import com.sap.sse.security.userstore.mongodb.DomainObjectFactory;
|
||||
import com.sap.sse.security.userstore.mongodb.impl.FieldNames.Tenant;
|
||||
|
||||
@@ -52,11 +55,12 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
public DomainObjectFactoryImpl(MongoDatabase db) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterable<AccessControlListAnnotation> loadAllAccessControlLists(UserStore userStore) {
|
||||
ArrayList<AccessControlListAnnotation> result = new ArrayList<>();
|
||||
MongoCollection<org.bson.Document> aclCollection = db.getCollection(CollectionNames.ACCESS_CONTROL_LISTS.name());
|
||||
MongoCollection<org.bson.Document> aclCollection = db
|
||||
.getCollection(CollectionNames.ACCESS_CONTROL_LISTS.name());
|
||||
try {
|
||||
for (Document o : aclCollection.find()) {
|
||||
result.add(loadAccessControlList(o, userStore));
|
||||
@@ -67,20 +71,23 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private AccessControlListAnnotation loadAccessControlList(Document aclDBObject, UserStore userStore) {
|
||||
final QualifiedObjectIdentifier id = QualifiedObjectIdentifierImpl
|
||||
.fromDBWithoutEscaping((String) aclDBObject.get(FieldNames.AccessControlList.OBJECT_ID.name()));
|
||||
final String displayName = (String) aclDBObject.get(FieldNames.AccessControlList.OBJECT_DISPLAY_NAME.name());
|
||||
List<Object> dbPermissionMap = ((List<Object>) aclDBObject.get(FieldNames.AccessControlList.PERMISSION_MAP.name()));
|
||||
List<Object> dbPermissionMap = ((List<Object>) aclDBObject
|
||||
.get(FieldNames.AccessControlList.PERMISSION_MAP.name()));
|
||||
Map<UserGroup, Set<String>> permissionMap = new HashMap<>();
|
||||
for (Object dbPermissionMapEntryO : dbPermissionMap) {
|
||||
Document dbPermissionMapEntry = (Document) dbPermissionMapEntryO;
|
||||
final UUID userGroupKey = (UUID) dbPermissionMapEntry.get(FieldNames.AccessControlList.PERMISSION_MAP_USER_GROUP_ID.name());
|
||||
final UUID userGroupKey = (UUID) dbPermissionMapEntry
|
||||
.get(FieldNames.AccessControlList.PERMISSION_MAP_USER_GROUP_ID.name());
|
||||
final UserGroup userGroup = userStore.getUserGroup(userGroupKey);
|
||||
Set<String> actions = new HashSet<>();
|
||||
for (Object o : (List<Object>) dbPermissionMapEntry.get(FieldNames.AccessControlList.PERMISSION_MAP_ACTIONS.name())) {
|
||||
for (Object o : (List<Object>) dbPermissionMapEntry
|
||||
.get(FieldNames.AccessControlList.PERMISSION_MAP_ACTIONS.name())) {
|
||||
actions.add(o.toString());
|
||||
}
|
||||
permissionMap.put(userGroup, actions);
|
||||
@@ -89,7 +96,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
displayName);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterable<OwnershipAnnotation> loadAllOwnerships(UserStore userStore) {
|
||||
ArrayList<OwnershipAnnotation> result = new ArrayList<>();
|
||||
@@ -104,18 +111,21 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private OwnershipAnnotation loadOwnership(Document ownershipDBObject, UserStore userStore) {
|
||||
String escapedId = (String) ownershipDBObject.get(FieldNames.Ownership.OBJECT_ID.name());
|
||||
final QualifiedObjectIdentifier idOfOwnedObject = QualifiedObjectIdentifierImpl.fromDBWithoutEscaping(escapedId);
|
||||
final String displayNameOfOwnedObject = (String) ownershipDBObject.get(FieldNames.Ownership.OBJECT_DISPLAY_NAME.name());
|
||||
final QualifiedObjectIdentifier idOfOwnedObject = QualifiedObjectIdentifierImpl
|
||||
.fromDBWithoutEscaping(escapedId);
|
||||
final String displayNameOfOwnedObject = (String) ownershipDBObject
|
||||
.get(FieldNames.Ownership.OBJECT_DISPLAY_NAME.name());
|
||||
final String userOwnerName = (String) ownershipDBObject.get(FieldNames.Ownership.OWNER_USERNAME.name());
|
||||
final UUID tenantOwnerId = (UUID) ownershipDBObject.get(FieldNames.Ownership.TENANT_OWNER_ID.name());
|
||||
final User userOwner = userStore.getUserByName(userOwnerName);
|
||||
final UserGroup tenantOwner = userStore.getUserGroup(tenantOwnerId);
|
||||
return new OwnershipAnnotation(new Ownership(userOwner, tenantOwner), idOfOwnedObject, displayNameOfOwnedObject);
|
||||
return new OwnershipAnnotation(new Ownership(userOwner, tenantOwner), idOfOwnedObject,
|
||||
displayNameOfOwnedObject);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterable<RoleDefinition> loadAllRoleDefinitions() {
|
||||
ArrayList<RoleDefinition> result = new ArrayList<>();
|
||||
@@ -160,8 +170,9 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
}
|
||||
return userGroups;
|
||||
}
|
||||
|
||||
private UserGroup loadUserGroupWithProxyUsers(Document groupDBObject, Map<UUID, RoleDefinition> roleDefinitionsById) {
|
||||
|
||||
private UserGroup loadUserGroupWithProxyUsers(Document groupDBObject,
|
||||
Map<UUID, RoleDefinition> roleDefinitionsById) {
|
||||
final UUID id = (UUID) groupDBObject.get(FieldNames.UserGroup.ID.name());
|
||||
final String name = (String) groupDBObject.get(FieldNames.UserGroup.NAME.name());
|
||||
Set<User> users = new HashSet<>();
|
||||
@@ -201,15 +212,16 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
* {@link Role#getQualifiedForTenant() tenant qualification} as defined by this parameter; if this
|
||||
* parameter is {@code null}, role migration will throw an exception.
|
||||
* @param userGroups
|
||||
* the user groups to resolve tenant IDs against for users' default tenants as well as role tenant qualifiers
|
||||
* the user groups to resolve tenant IDs against for users' default tenants as well as role tenant
|
||||
* qualifiers
|
||||
* @return the user objects returned have a fully resolved default tenant as well as fully-resolved role tenant/user
|
||||
* qualifiers; the {@link Tenant} objects passed in the {@code tenants} map may still have an empty user
|
||||
* group that is filled later.
|
||||
*/
|
||||
@Override
|
||||
public Iterable<User> loadAllUsers(
|
||||
Map<UUID, RoleDefinition> roleDefinitionsById, RoleMigrationConverter roleMigrationConverter,
|
||||
Map<UUID, UserGroup> userGroups, UserGroupProvider userGroupProvider) throws UserManagementException {
|
||||
public Iterable<User> loadAllUsers(Map<UUID, RoleDefinition> roleDefinitionsById,
|
||||
RoleMigrationConverter roleMigrationConverter, Map<UUID, UserGroup> userGroups,
|
||||
UserGroupProvider userGroupProvider) throws UserManagementException {
|
||||
Map<String, User> result = new HashMap<>();
|
||||
MongoCollection<org.bson.Document> userCollection = db.getCollection(CollectionNames.USERS.name());
|
||||
try {
|
||||
@@ -225,7 +237,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
resolveRoleUserQualifiers(result);
|
||||
return result.values();
|
||||
}
|
||||
|
||||
|
||||
private void resolveRoleUserQualifiers(Map<String, User> users) throws UserManagementException {
|
||||
for (final User user : users.values()) {
|
||||
final Set<Role> userRoles = new HashSet<>();
|
||||
@@ -234,14 +246,15 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
final User userQualifierProxy = roleWithUserQualifierProxy.getQualifiedForUser();
|
||||
if (userQualifierProxy != null) {
|
||||
final User resolvedUserQualifier = users.get(userQualifierProxy.getName());
|
||||
if (resolvedUserQualifier == null) {
|
||||
throw new UserManagementException("Unable to resolve user named "+userQualifierProxy.getName()+
|
||||
" which serves as a role qualifier for role "+roleWithUserQualifierProxy.getName()+
|
||||
" for user "+user.getName());
|
||||
}
|
||||
user.removeRole(roleWithUserQualifierProxy);
|
||||
user.addRole(new Role(roleWithUserQualifierProxy.getRoleDefinition(),
|
||||
roleWithUserQualifierProxy.getQualifiedForTenant(), resolvedUserQualifier));
|
||||
if (resolvedUserQualifier == null) {
|
||||
logger.severe("Unable to resolve user named "+userQualifierProxy.getName()+
|
||||
" which serves as a role qualifier for role "+roleWithUserQualifierProxy.getName()+
|
||||
" for user "+user.getName()+". Removing role.");
|
||||
} else {
|
||||
user.addRole(new Role(roleWithUserQualifierProxy.getRoleDefinition(),
|
||||
roleWithUserQualifierProxy.getQualifiedForTenant(), resolvedUserQualifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,8 +279,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
final String email = (String) userDBObject.get(FieldNames.User.EMAIL.name());
|
||||
final String fullName = (String) userDBObject.get(FieldNames.User.FULLNAME.name());
|
||||
final String company = (String) userDBObject.get(FieldNames.User.COMPANY.name());
|
||||
final String localeRaw = (String) userDBObject.get(FieldNames.User.LOCALE.name());
|
||||
final Locale locale = localeRaw != null ? Locale.forLanguageTag(localeRaw) : null;
|
||||
final String localeRaw = (String) userDBObject.get(FieldNames.User.LOCALE.name());
|
||||
final Locale locale = localeRaw != null ? Locale.forLanguageTag(localeRaw) : null;
|
||||
Boolean emailValidated = (Boolean) userDBObject.get(FieldNames.User.EMAIL_VALIDATED.name());
|
||||
String passwordResetSecret = (String) userDBObject.get(FieldNames.User.PASSWORD_RESET_SECRET.name());
|
||||
String validationSecret = (String) userDBObject.get(FieldNames.User.VALIDATION_SECRET.name());
|
||||
@@ -281,7 +294,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
if (role != null) {
|
||||
roles.add(role);
|
||||
} else {
|
||||
logger.warning("Role with ID "+o+" that used to be assigned to user "+username+" not found");
|
||||
logger.warning(
|
||||
"Role with ID " + o + " that used to be assigned to user " + username + " not found");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -289,20 +303,21 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
// try to find an equal-named role in the set of role definitions and create a role
|
||||
// that is qualified by the default tenant; for this a default tenant must exist because
|
||||
// otherwise a user would obtain global rights by means of migration which must not happen.
|
||||
logger.info("Migrating roles of user "+username);
|
||||
logger.info("Migrating roles of user " + username);
|
||||
List<?> roleNames = (List<?>) userDBObject.get("ROLES");
|
||||
if (roleNames != null) {
|
||||
logger.info("Found old roles "+roleNames+" for user "+username);
|
||||
logger.info("Found old roles " + roleNames + " for user " + username);
|
||||
for (Object o : roleNames) {
|
||||
final Role convertedRole = roleMigrationConverter.convert(o.toString(), username);
|
||||
if (convertedRole != null) {
|
||||
logger.info("Found role "+convertedRole.getRoleDefinition()+" for old role "+o.toString()+" for user "+username);
|
||||
logger.info("Found role " + convertedRole.getRoleDefinition() + " for old role " + o.toString()
|
||||
+ " for user " + username);
|
||||
// we do not do role associations, to stay similar as before, meaning that all admins can
|
||||
// edit the roles. Without this we would need to determine which admin (if
|
||||
// multiple present) should own this association.
|
||||
roles.add(convertedRole);
|
||||
rolesMigrated = true;
|
||||
}else {
|
||||
} else {
|
||||
logger.warning("Role " + o.toString() + " for user " + username
|
||||
+ " not found during migration. User will no longer be in this role.");
|
||||
}
|
||||
@@ -315,7 +330,6 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
permissions.add((String) o);
|
||||
}
|
||||
}
|
||||
|
||||
final Map<String, UserGroup> defaultTenant = new ConcurrentHashMap<>();
|
||||
final List<?> defaultTenantIds = (List<?>) userDBObject.get(FieldNames.User.DEFAULT_TENANT_IDS.name());
|
||||
if (defaultTenantIds != null) {
|
||||
@@ -350,6 +364,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
// is also only interested in the object's ID
|
||||
new MongoObjectFactoryImpl(db).storeUser(result);
|
||||
}
|
||||
final List<?> subscriptionDocs = (List<?>) userDBObject.get(FieldNames.User.SUBSCRIPTIONS.name());
|
||||
result.setSubscriptions(loadSubscriptions(subscriptionDocs));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -371,7 +387,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
|
||||
private Map<AccountType, Account> createAccountMapFromdDBObject(Document accountsMap) {
|
||||
Map<AccountType, Account> accounts = new HashMap<>();
|
||||
for (Entry<?, ?> e : accountsMap.entrySet()){
|
||||
for (Entry<?, ?> e : accountsMap.entrySet()) {
|
||||
AccountType type = AccountType.valueOf((String) e.getKey());
|
||||
Account account = createAccountFromDBObject((Document) e.getValue(), type);
|
||||
accounts.put(type, account);
|
||||
@@ -386,10 +402,10 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
String saltedPassword = (String) dbAccount.get(FieldNames.UsernamePassword.SALTED_PW.name());
|
||||
Binary salt = (Binary) dbAccount.get(FieldNames.UsernamePassword.SALT.name());
|
||||
return new UsernamePasswordAccount(name, saltedPassword, salt.getData());
|
||||
//TODO [D056866] add other Account-types
|
||||
// TODO [D056866] add other Account-types
|
||||
case SOCIAL_USER:
|
||||
SocialUserAccount socialUserAccount = new SocialUserAccount();
|
||||
for (Social s : Social.values()){
|
||||
for (Social s : Social.values()) {
|
||||
socialUserAccount.setProperty(s.name(), (String) dbAccount.get(s.name()));
|
||||
}
|
||||
return socialUserAccount;
|
||||
@@ -402,22 +418,19 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
public Map<String, Object> loadSettings() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
MongoCollection<org.bson.Document> settingsCollection = db.getCollection(CollectionNames.SETTINGS.name());
|
||||
|
||||
try {
|
||||
Document query = new Document();
|
||||
query.put(FieldNames.Settings.NAME.name(), FieldNames.Settings.VALUES.name());
|
||||
Document settingDBObject = settingsCollection.find(query).first();
|
||||
if (settingDBObject != null) {
|
||||
result = loadSettingMap(settingDBObject);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.info("No stored settings found!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load settings.");
|
||||
logger.log(Level.SEVERE, "loadSettings", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -428,7 +441,8 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
try {
|
||||
for (Object o : settingsCollection.find()) {
|
||||
Document usernameAndPreferencesMap = (Document) o;
|
||||
Map<String, String> userMap = loadPreferencesMap((Iterable<?>) usernameAndPreferencesMap.get(FieldNames.Preferences.KEYS_AND_VALUES.name()));
|
||||
Map<String, String> userMap = loadPreferencesMap(
|
||||
(Iterable<?>) usernameAndPreferencesMap.get(FieldNames.Preferences.KEYS_AND_VALUES.name()));
|
||||
String username = (String) usernameAndPreferencesMap.get(FieldNames.Preferences.USERNAME.name());
|
||||
result.put(username, userMap);
|
||||
}
|
||||
@@ -453,7 +467,7 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
private Map<String, Object> loadSettingMap(Document settingDBObject) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
Map<?, ?> map = ((Document) settingDBObject.get(FieldNames.Settings.MAP.name()));
|
||||
for (Entry<?, ?> e : map.entrySet()){
|
||||
for (Entry<?, ?> e : map.entrySet()) {
|
||||
String key = (String) e.getKey();
|
||||
Object value = e.getValue();
|
||||
result.put(key, value);
|
||||
@@ -465,29 +479,26 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
public Map<String, Class<?>> loadSettingTypes() {
|
||||
Map<String, Class<?>> result = new HashMap<String, Class<?>>();
|
||||
MongoCollection<Document> settingsCollection = db.getCollection(CollectionNames.SETTINGS.name());
|
||||
|
||||
try {
|
||||
Document query = new Document();
|
||||
query.put(FieldNames.Settings.NAME.name(), FieldNames.Settings.TYPES.name());
|
||||
Document settingTypesDBObject = settingsCollection.find(query).first();
|
||||
if (settingTypesDBObject != null) {
|
||||
result = loadSettingTypesMap(settingTypesDBObject);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
logger.info("No stored setting types found!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.SEVERE, "Error connecting to MongoDB, unable to load setting types.");
|
||||
logger.log(Level.SEVERE, "loadSettingTypes", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Class<?>> loadSettingTypesMap(Document settingTypesDBObject) {
|
||||
Map<String, Class<?>> result = new HashMap<>();
|
||||
Map<?, ?> map = (Document) settingTypesDBObject.get(FieldNames.Settings.MAP.name());
|
||||
for (Entry<?, ?> e : map.entrySet()){
|
||||
for (Entry<?, ?> e : map.entrySet()) {
|
||||
String key = (String) e.getKey();
|
||||
Class<?> value = null;
|
||||
try {
|
||||
@@ -499,4 +510,25 @@ public class DomainObjectFactoryImpl implements DomainObjectFactory {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Subscription[] loadSubscriptions(List<?> subscriptionsDoc) {
|
||||
final Subscription[] subscriptions;
|
||||
if (subscriptionsDoc != null) {
|
||||
subscriptions = new Subscription[subscriptionsDoc.size()];
|
||||
int i = 0;
|
||||
for (Object o : subscriptionsDoc) {
|
||||
final Document doc = (Document) o;
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
for (Entry<String, Object> entry : doc.entrySet()) {
|
||||
data.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
SubscriptionData subscriptionData = new SubscriptionData(data);
|
||||
final SubscriptionDataHandler subscriptionDataHandler = Activator.getSubscriptionDataHandler(subscriptionData.getProviderName());
|
||||
subscriptions[i++] = subscriptionDataHandler.toSubscription(subscriptionData);
|
||||
}
|
||||
} else {
|
||||
subscriptions = null;
|
||||
}
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -39,8 +39,8 @@ public class FieldNames {
|
||||
NAME,
|
||||
USERNAMES,
|
||||
ROLE_DEFINITION_MAP, // a list of objects with two components each:
|
||||
ROLE_DEFINITION_MAP_ROLE_ID,
|
||||
ROLE_DEFINITION_MAP_FOR_ALL
|
||||
ROLE_DEFINITION_MAP_ROLE_ID,
|
||||
ROLE_DEFINITION_MAP_FOR_ALL
|
||||
}
|
||||
|
||||
public static enum User {
|
||||
@@ -56,7 +56,8 @@ public class FieldNames {
|
||||
PASSWORD_RESET_SECRET,
|
||||
VALIDATION_SECRET,
|
||||
DEFAULT_TENANT_SERVER,
|
||||
DEFAULT_TENANT_GROUP;
|
||||
DEFAULT_TENANT_GROUP,
|
||||
SUBSCRIPTIONS;
|
||||
}
|
||||
|
||||
public static enum Settings {
|
||||
@@ -82,6 +83,4 @@ public class FieldNames {
|
||||
SALTED_PW,
|
||||
SALT;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+18
-1
@@ -31,6 +31,8 @@ import com.sap.sse.security.shared.impl.Ownership;
|
||||
import com.sap.sse.security.shared.impl.Role;
|
||||
import com.sap.sse.security.shared.impl.User;
|
||||
import com.sap.sse.security.shared.impl.UserGroup;
|
||||
import com.sap.sse.security.shared.subscription.Subscription;
|
||||
import com.sap.sse.security.subscription.SubscriptionDataHandler;
|
||||
import com.sap.sse.security.userstore.mongodb.MongoObjectFactory;
|
||||
|
||||
public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
@@ -38,7 +40,6 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
private final MongoDatabase db;
|
||||
final MongoCollection<org.bson.Document> settingCollection;
|
||||
|
||||
|
||||
public MongoObjectFactoryImpl(MongoDatabase db) {
|
||||
this.db = db;
|
||||
settingCollection = db.getCollection(CollectionNames.PREFERENCES.name());
|
||||
@@ -230,6 +231,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
defaultTennants.add(tenant);
|
||||
}
|
||||
dbUser.put(FieldNames.User.DEFAULT_TENANT_IDS.name(), defaultTennants);
|
||||
dbUser.put(FieldNames.User.SUBSCRIPTIONS.name(), createSubscriptions(user.getSubscriptions()));
|
||||
usersCollection.withWriteConcern(WriteConcern.ACKNOWLEDGED).replaceOne(query, dbUser, new UpdateOptions().upsert(true));
|
||||
}
|
||||
|
||||
@@ -333,4 +335,19 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory {
|
||||
return dbSettingTypes;
|
||||
}
|
||||
|
||||
private BasicDBList createSubscriptions(Iterable<Subscription> subscriptions) {
|
||||
final BasicDBList result;
|
||||
if (subscriptions != null) {
|
||||
result = new BasicDBList();
|
||||
for (final Subscription subscription : subscriptions) {
|
||||
final Document doc = new Document();
|
||||
final SubscriptionDataHandler subscriptionDataHandler = Activator.getSubscriptionDataHandler(subscription.getProviderName());
|
||||
doc.putAll(subscriptionDataHandler.toMap(subscription));
|
||||
result.add(doc);
|
||||
}
|
||||
} else {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -14,6 +14,7 @@ import com.sap.sse.security.shared.impl.Role;
|
||||
import com.sap.sse.security.shared.impl.User;
|
||||
import com.sap.sse.security.shared.impl.UserGroup;
|
||||
import com.sap.sse.security.shared.impl.UserGroupImpl;
|
||||
import com.sap.sse.security.shared.subscription.Subscription;
|
||||
|
||||
public class UserProxy implements User {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -207,4 +208,29 @@ public class UserProxy implements User {
|
||||
public String createRandomSecret() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Subscription> getSubscriptions() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSubscriptions(Subscription[] subscriptions) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription getSubscriptionByPlan(String planId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Subscription getSubscriptionById(String subscriptionId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasActiveSubscription() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user