org.opencastproject.kernel.security.OrganizationDirectoryServiceImpl Maven / Gradle / Ivy
/*
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.kernel.security;
import static org.opencastproject.security.util.SecurityUtil.hostAndPort;
import static org.opencastproject.util.data.Collections.map;
import static org.opencastproject.util.data.Collections.toList;
import static org.opencastproject.util.data.Tuple.tuple;
import org.opencastproject.kernel.security.persistence.OrganizationDatabase;
import org.opencastproject.kernel.security.persistence.OrganizationDatabaseException;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryListener;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.impl.jpa.JpaOrganization;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.data.Tuple;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedServiceFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Implements the organizational directory. As long as no organizations are published in the service registry, the
* directory will contain the default organization as the only instance.
*/
@Component(
property = {
"service.pid=org.opencastproject.organization",
"service.description=Organization Directory Service"
},
immediate = true,
service = { OrganizationDirectoryService.class, ManagedServiceFactory.class }
)
public class OrganizationDirectoryServiceImpl implements OrganizationDirectoryService, ManagedServiceFactory {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(OrganizationDirectoryServiceImpl.class);
/** The organization service PID */
public static final String PID = "org.opencastproject.organization";
/** The prefix for configurations to use for arbitrary organization properties */
public static final String ORG_PROPERTY_PREFIX = "prop.";
/** The managed property that specifies the organization id */
public static final String ORG_ID_KEY = "id";
/** The managed property that specifies the organization name */
public static final String ORG_NAME_KEY = "name";
/** The managed property that specifies the organization server name */
public static final String ORG_SERVER_PREFIX = "prop.org.opencastproject.host.";
/** The default host in case no server is configured */
public static final String DEFAULT_SERVER_HOST = "localhost";
/** The default port in case no server is configured */
public static final int DEFAULT_SERVER_PORT = 8080;
/** The managed property that specifies the organization administrative role */
public static final String ORG_ADMIN_ROLE_KEY = "admin_role";
/** The managed property that specifies the organization anonymous role */
public static final String ORG_ANONYMOUS_ROLE_KEY = "anonymous_role";
/** The configuration admin service */
protected ConfigurationAdmin configAdmin = null;
/** To enable threading when dispatching jobs */
private final ExecutorService executor = Executors.newCachedThreadPool();
/** The organization database */
private OrganizationDatabase persistence = null;
/** The list of directory listeners */
private final List listeners = new ArrayList<>();
private OrgCache cache;
/** OSGi DI */
@Reference
public void setOrgPersistence(OrganizationDatabase setOrgPersistence) {
this.persistence = setOrgPersistence;
this.cache = new OrgCache(60000, persistence);
}
/**
* @param configAdmin
* the configAdmin to set
*/
@Reference
public void setConfigurationAdmin(ConfigurationAdmin configAdmin) {
this.configAdmin = configAdmin;
}
@Override
public Organization getOrganization(final String id) throws NotFoundException {
Organization org = cache.get(id);
if (org == null)
throw new NotFoundException();
return org;
}
@Override
public Organization getOrganization(final URL url) throws NotFoundException {
Organization org = cache.get(url);
if (org == null)
throw new NotFoundException();
return org;
}
@Override
public List getOrganizations() {
return cache.getAll();
}
/**
* Adds the organization to the list of organizations.
*
* @param organization
* the organization
*/
public void addOrganization(Organization organization) {
boolean contains = persistence.containsOrganization(organization.getId());
if (contains)
throw new IllegalStateException("Can not add an organization with id '" + organization.getId()
+ "' since an organization with that identifier has already been registered");
persistence.storeOrganization(organization);
cache.invalidate();
fireOrganizationRegistered(organization);
}
@Override
public String getName() {
return PID;
}
@Override
@SuppressWarnings("rawtypes")
public void updated(String pid, Dictionary properties) throws ConfigurationException {
logger.debug("Updating organization pid='{}'", pid);
// Gather the properties
final String id = (String) properties.get(ORG_ID_KEY);
final String name = (String) properties.get(ORG_NAME_KEY);
// Make sure the configuration meets the minimum requirements
if (StringUtils.isBlank(id))
throw new ConfigurationException(ORG_ID_KEY, ORG_ID_KEY + " must be set");
final String adminRole = (String) properties.get(ORG_ADMIN_ROLE_KEY);
final String anonRole = (String) properties.get(ORG_ANONYMOUS_ROLE_KEY);
// Build the properties map
final Map orgProperties = new HashMap<>();
HashMap servers = new HashMap<>();
for (Enumeration> e = properties.keys(); e.hasMoreElements();) {
final String key = (String) e.nextElement();
if (!key.startsWith(ORG_PROPERTY_PREFIX)) {
continue;
}
if (key.startsWith(ORG_SERVER_PREFIX)) {
String tenantSpecificHost = StringUtils.trimToNull((String) properties.get(key));
if (tenantSpecificHost != null) {
try {
Tuple hostPort = hostAndPort(new URL(tenantSpecificHost));
servers.put(hostPort.getA(), hostPort.getB());
} catch (MalformedURLException malformedURLException) {
logger.error("{} is not a URL", tenantSpecificHost);
}
}
}
orgProperties.put(key.substring(ORG_PROPERTY_PREFIX.length()), (String) properties.get(key));
}
if (servers.isEmpty()) {
logger.debug("No server URL configured for organization {}, setting default {}:{}", name, DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
servers.put(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
}
// Load the existing organization or create a new one
try {
JpaOrganization org;
try {
org = (JpaOrganization) persistence.getOrganization(id);
org.setName(name);
// TODO: should this really be append only?
for (Map.Entry server : servers.entrySet()) {
org.addServer(server.getKey(), server.getValue());
}
org.setAdminRole(adminRole);
org.setAnonymousRole(anonRole);
org.setProperties(orgProperties);
logger.info("Updating organization '{}'", id);
persistence.storeOrganization(org);
fireOrganizationUpdated(org);
} catch (NotFoundException e) {
org = new JpaOrganization(id, name, servers, adminRole, anonRole, orgProperties);
logger.info("Creating organization '{}'", id);
persistence.storeOrganization(org);
fireOrganizationRegistered(org);
}
cache.invalidate();
} catch (OrganizationDatabaseException e) {
logger.error("Unable to register organization '{}': {}", id, e);
}
}
@Override
public void deleted(String pid) {
try {
Organization organization = getOrganization(pid);
persistence.deleteOrganization(pid);
cache.invalidate();
fireOrganizationUnregistered(organization);
} catch (NotFoundException e) {
logger.warn("Can't delete organization with id {}, organization not found.", pid);
}
}
@Override
public void addOrganizationDirectoryListener(OrganizationDirectoryListener listener) {
if (listener == null)
return;
if (!listeners.contains(listener))
listeners.add(listener);
}
@Override
public void removeOrganizationDirectoryListener(OrganizationDirectoryListener listener) {
if (listener == null)
return;
listeners.remove(listener);
}
/**
* Notifies registered listeners about a newly registered organization.
*
* @param organization
* the organization
*/
private void fireOrganizationRegistered(final Organization organization) {
executor.submit(() -> {
for (OrganizationDirectoryListener listener : listeners) {
logger.debug("Notifying {} about newly registered organization '{}'", listener, organization);
listener.organizationRegistered(organization);
}
});
}
/**
* Notifies registered listeners about an unregistered organization.
*
* @param organization
* the organization
*/
private void fireOrganizationUnregistered(final Organization organization) {
executor.submit(() -> {
for (OrganizationDirectoryListener listener : listeners) {
logger.debug("Notifying {} about unregistered organization '{}'", listener, organization);
listener.organizationUnregistered(organization);
}
});
}
/**
* Notifies registered listeners about an updated organization.
*
* @param organization
* the organization
*/
private void fireOrganizationUpdated(final Organization organization) {
executor.submit(() -> {
for (OrganizationDirectoryListener listener : listeners) {
logger.debug("Notifying {} about updated organization '{}'", listener, organization);
listener.organizationUpdated(organization);
}
});
}
/**
* Very simple cache that does a complete refresh after a given interval. This type of cache is only suitable
* for small sets.
*/
private static final class OrgCache {
private final Object lock = new Object();
// A simple hash map is sufficient here.
// No need to deal with soft references or an LRU map since the number of organizations
// will be quite low.
private final Map, Organization> byHost = map();
private final Map byId = map();
private final long refreshInterval;
private long lastRefresh;
private final OrganizationDatabase persistence;
OrgCache(long refreshInterval, OrganizationDatabase persistence) {
this.refreshInterval = refreshInterval;
this.persistence = persistence;
invalidate();
}
public Organization get(URL url) {
synchronized (lock) {
refresh();
return byHost.get(hostAndPort(url));
}
}
public Organization get(String id) {
synchronized (lock) {
refresh();
return byId.get(id);
}
}
public List getAll() {
synchronized (lock) {
refresh();
return toList(byId.values());
}
}
public void invalidate() {
this.lastRefresh = System.currentTimeMillis() - 2 * refreshInterval;
}
private void refresh() {
final long now = System.currentTimeMillis();
if (now - lastRefresh > refreshInterval) {
byId.clear();
byHost.clear();
for (Organization org : persistence.getOrganizations()) {
byId.put(org.getId(), org);
// (host, port)
for (Map.Entry server : org.getServers().entrySet()) {
byHost.put(tuple(server.getKey(), server.getValue()), org);
}
}
lastRefresh = now;
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy