/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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.apache.karaf.service.guard.impl;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.regex.Pattern;
import org.apache.aries.proxy.InvocationListener;
import org.apache.aries.proxy.ProxyManager;
import org.apache.aries.proxy.UnableToProxyException;
import org.apache.karaf.service.guard.tools.ACLConfigurationParser;
import org.apache.karaf.service.guard.tools.ACLConfigurationParser.Specificity;
import org.apache.karaf.util.jaas.JaasHelper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GuardProxyCatalog implements ServiceListener {
public static final String KARAF_SECURED_SERVICES_SYSPROP = "karaf.secured.services";
public static final String SERVICE_GUARD_ROLES_PROPERTY = "org.apache.karaf.service.guard.roles";
public static final String KARAF_SECURED_COMMAND_COMPULSORY_ROLES_PROPERTY = "karaf.secured.command.compulsory.roles";
static final String PROXY_CREATOR_THREAD_NAME = "Secure OSGi Service Proxy Creator";
static final String PROXY_SERVICE_KEY = "." + GuardProxyCatalog.class.getName(); // The only currently used value is Boolean.TRUE
static final String SERVICE_ACL_PREFIX = "org.apache.karaf.service.acl.";
static final String SERVICE_GUARD_KEY = "service.guard";
static final Logger LOG = LoggerFactory.getLogger(GuardProxyCatalog.class);
private static final Pattern JAVA_METHOD_NAME_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*");
private static final String ROLE_WILDCARD = "*";
private final BundleContext myBundleContext;
private final Map<String, Filter> filters = new ConcurrentHashMap<String, Filter>();
final ServiceTracker<ConfigurationAdmin, ConfigurationAdmin> configAdminTracker;
final ServiceTracker<ProxyManager, ProxyManager> proxyManagerTracker;
final ConcurrentMap<Long, ServiceRegistrationHolder> proxyMap = new ConcurrentHashMap<Long, ServiceRegistrationHolder>();
final BlockingQueue<CreateProxyRunnable> createProxyQueue = new LinkedBlockingQueue<CreateProxyRunnable>();
final String compulsoryRoles;
// These two variables control the proxy creator thread, which is started as soon as a ProxyManager Service
// becomes available.
volatile boolean runProxyCreator = true;
volatile Thread proxyCreatorThread = null;
GuardProxyCatalog(BundleContext bc) throws Exception {
LOG.trace("Starting GuardProxyCatalog");
myBundleContext = bc;
compulsoryRoles = System.getProperty(GuardProxyCatalog.KARAF_SECURED_COMMAND_COMPULSORY_ROLES_PROPERTY);
if (compulsoryRoles == null) {
//default behavior as before, no compulsory roles for a karaf command without the ACL
LOG.info("No compulsory roles for a karaf command without the ACL as its system property is not set: {}", GuardProxyCatalog.KARAF_SECURED_COMMAND_COMPULSORY_ROLES_PROPERTY);
}
// The service listener is used to update/unregister proxies if the backing service changes/goes away
bc.addServiceListener(this);
Filter caFilter = getNonProxyFilter(bc, ConfigurationAdmin.class);
LOG.trace("Creating Config Admin Tracker using filter {}", caFilter);
configAdminTracker = new ServiceTracker<ConfigurationAdmin, ConfigurationAdmin>(bc, caFilter, null);
configAdminTracker.open();
Filter pmFilter = getNonProxyFilter(bc, ProxyManager.class);
LOG.trace("Creating Proxy Manager Tracker using filter {}", pmFilter);
proxyManagerTracker = new ServiceTracker<ProxyManager, ProxyManager>(bc, pmFilter, new ServiceProxyCreatorCustomizer());
proxyManagerTracker.open();
}
static Filter getNonProxyFilter(BundleContext bc, Class<?> clazz) throws InvalidSyntaxException {
Filter caFilter = bc.createFilter(
"(&(" + Constants.OBJECTCLASS + "=" + clazz.getName() +
")(!(" + PROXY_SERVICE_KEY + "=*)))");
return caFilter;
}
void close() {
LOG.trace("Stopping GuardProxyCatalog");
stopProxyCreator();
proxyManagerTracker.close();
configAdminTracker.close();
myBundleContext.removeServiceListener(this);
// Remove all proxy registrations
for (ServiceRegistrationHolder holder : proxyMap.values()) {
ServiceRegistration<?> reg = holder.registration;
if (reg != null) {
LOG.info("Unregistering proxy service of {} with properties {}",
reg.getReference().getProperty(Constants.OBJECTCLASS), copyProperties(reg.getReference()));
reg.unregister();
}
}
proxyMap.clear();
}
@Override
public void serviceChanged(ServiceEvent event) {
// This method is to ensure that proxied services follow the original service. I.e. if the original service
// goes away the proxies should go away too. If the original service is modified, the proxies should be
// modified accordingly
ServiceReference<?> sr = event.getServiceReference();
if (event.getType() == ServiceEvent.REGISTERED) {
// Nothing to do for new services
return;
}
if (isProxy(sr)) {
// Ignore proxies, we only react to real service changes
return;
}
Long orgServiceID = (Long) sr.getProperty(Constants.SERVICE_ID);
if (event.getType() == ServiceEvent.UNREGISTERING) {
handleOriginalServiceUnregistering(orgServiceID);
}
if ((event.getType() & (ServiceEvent.MODIFIED | ServiceEvent.MODIFIED_ENDMATCH)) > 0) {
handleOriginalServiceModifed(orgServiceID, sr);
}
}
private void handleOriginalServiceUnregistering(Long orgServiceID) {
// If the service queued up to be proxied, remove it.
for (Iterator<CreateProxyRunnable> i = createProxyQueue.iterator(); i.hasNext(); ) {
CreateProxyRunnable cpr = i.next();
if (orgServiceID.equals(cpr.getOriginalServiceID())) {
i.remove();
}
}
ServiceRegistrationHolder holder = proxyMap.remove(orgServiceID);
if (holder != null) {
if (holder.registration != null) {
holder.registration.unregister();
}
}
}
private void handleOriginalServiceModifed(Long orgServiceID, ServiceReference<?> orgServiceRef) {
// We don't need to do anything for services that are queued up to be proxied, as the
// properties are only taken at the point of proxyfication...
ServiceRegistrationHolder holder = proxyMap.get(orgServiceID);
if (holder != null) {
ServiceRegistration<?> reg = holder.registration;
if (reg != null) {
// Preserve the roles as they are expensive to compute
Object roles = reg.getReference().getProperty(SERVICE_GUARD_ROLES_PROPERTY);
Dictionary<String, Object> newProxyProps = proxyProperties(orgServiceRef);
if (roles != null) {
newProxyProps.put(SERVICE_GUARD_ROLES_PROPERTY, roles);
} else {
newProxyProps.remove(SERVICE_GUARD_ROLES_PROPERTY);
}
reg.setProperties(newProxyProps);
}
}
}
boolean isProxy(ServiceReference<?> sr) {
return sr.getProperty(PROXY_SERVICE_KEY) != null;
}
// Called by hooks to find out whether the service should be hidden.
// Also handles the proxy creation of services if applicable.
// Return true if the hook should hide the service for the bundle
boolean handleProxificationForHook(ServiceReference<?> sr) {
// Note that when running under an OSGi R6 framework the number of proxies created
// can be limited by looking at the new 'service.scope' property. If the value is
// 'singleton' then the same proxy can be shared across all clients.
// Pre OSGi R6 it is not possible to find out whether a service is backed by a
// Service Factory, so we assume that every service is.
if (isProxy(sr)) {
return false;
}
proxyIfNotAlreadyProxied(sr); // Note does most of the work async
return true;
}
void proxyIfNotAlreadyProxied(final ServiceReference<?> originalRef) {
final long orgServiceID = (Long) originalRef.getProperty(Constants.SERVICE_ID);
// make sure it's on the map before the proxy is registered, as that can trigger
// another call into this method, and we need to make sure that it doesn't proxy
// the service again.
final ServiceRegistrationHolder registrationHolder = new ServiceRegistrationHolder();
ServiceRegistrationHolder previousHolder = proxyMap.putIfAbsent(orgServiceID, registrationHolder);
if (previousHolder != null) {
// There is already a proxy for this service
return;
}
LOG.trace("Will create proxy of service {}({})", originalRef.getProperty(Constants.OBJECTCLASS), orgServiceID);
// Instead of immediately creating the proxy, we add the code that creates the proxy to the proxyQueue.
// This means that we can better react to the fact that the ProxyManager service might arrive
// later. As soon as the Proxy Manager is available, the queue is emptied and the proxies created.
CreateProxyRunnable cpr = new CreateProxyRunnable() {
@Override
public long getOriginalServiceID() {
return orgServiceID;
}
@Override
public void run(final ProxyManager pm) throws Exception {
String[] objectClassProperty = (String[]) originalRef.getProperty(Constants.OBJECTCLASS);
ServiceFactory<Object> sf = new ProxyServiceFactory(pm, originalRef);
registrationHolder.registration = originalRef.getBundle().getBundleContext().registerService(
objectClassProperty, sf, proxyPropertiesRoles());
Dictionary<String, Object> actualProxyProps = copyProperties(registrationHolder.registration.getReference());
LOG.debug("Created proxy of service {} under {} with properties {}",
orgServiceID, actualProxyProps.get(Constants.OBJECTCLASS), actualProxyProps);
}
private Dictionary<String, Object> proxyPropertiesRoles() throws Exception {
Dictionary<String, Object> p = proxyProperties(originalRef);
Set<String> roles = getServiceInvocationRoles(originalRef);
if (roles != null) {
roles.remove(ROLE_WILDCARD); // we don't expose that on the service property
p.put(SERVICE_GUARD_ROLES_PROPERTY, roles);
} else {
// In this case there are no roles defined for the service so anyone can invoke it
p.remove(SERVICE_GUARD_ROLES_PROPERTY);
}
return p;
}
};
try {
createProxyQueue.put(cpr);
} catch (InterruptedException e) {
LOG.warn("Problem scheduling a proxy creator for service {}({})",
originalRef.getProperty(Constants.OBJECTCLASS), orgServiceID, e);
e.printStackTrace();
}
}
private static Dictionary<String, Object> proxyProperties(ServiceReference<?> sr) {
Dictionary<String, Object> p = copyProperties(sr);
p.put(PROXY_SERVICE_KEY, Boolean.TRUE);
return p;
}
private static Dictionary<String, Object> copyProperties(ServiceReference<?> sr) {
Dictionary<String, Object> p = new Hashtable<String, Object>();
for (String key : sr.getPropertyKeys()) {
p.put(key, sr.getProperty(key));
}
return p;
}
// Returns what roles can possibly ever invoke this service. Note that not every invocation may be successful
// as there can be different roles for different methods and also roles based on arguments passed in.
Set<String> getServiceInvocationRoles(ServiceReference<?> serviceReference) throws Exception {
boolean definitionFound = false;
Set<String> allRoles = new HashSet<String>();
// This can probably be optimized. Maybe we can cache the config object relevant instead of
// walking through all of the ones that have 'service.guard'.
for (Configuration config : getServiceGuardConfigs()) {
Dictionary<String, Object> properties = config.getProperties();
Object guardFilter = properties.get(SERVICE_GUARD_KEY);
if (guardFilter instanceof String) {
Filter filter = getFilter((String) guardFilter);
if (filter.match(serviceReference)) {
definitionFound = true;
for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
String key = e.nextElement();
String bareKey = key;
int idx = bareKey.indexOf('(');
if (idx >= 0) {
bareKey = bareKey.substring(0, idx);
}
int idx1 = bareKey.indexOf('[');
if (idx1 >= 0) {
bareKey = bareKey.substring(0, idx1);
}
int idx2 = bareKey.indexOf('*');
if (idx2 >= 0) {
bareKey = bareKey.substring(0, idx2);
}
if (!isValidMethodName(bareKey)) {
continue;
}
Object value = properties.get(key);
if (value instanceof String) {
allRoles.addAll(ACLConfigurationParser.parseRoles((String) value));
}
}
}
}
}
return definitionFound ? allRoles : null;
}
private Filter getFilter(String string) throws InvalidSyntaxException {
Filter filter = filters.get(string);
if (filter == null) {
filter = myBundleContext.createFilter(string);
filters.put(string, filter);
}
return filter;
}
// Ensures that it never returns null
private Configuration[] getServiceGuardConfigs() throws IOException, InvalidSyntaxException {
ConfigurationAdmin ca = null;
try {
ca = configAdminTracker.waitForService(5000);
} catch (InterruptedException e) {
}
if (ca == null) {
throw new IllegalStateException("Role based access for services requires the OSGi Configuration Admin Service to be present");
}
Configuration[] configs = ca.listConfigurations(
"(&(" + Constants.SERVICE_PID + "=" + SERVICE_ACL_PREFIX + "*)(" + SERVICE_GUARD_KEY + "=*))");
if (configs == null) {
return new Configuration [] {};
}
return configs;
}
private boolean isValidMethodName(String name) {
return JAVA_METHOD_NAME_PATTERN.matcher(name).matches();
}
void stopProxyCreator() {
runProxyCreator = false; // Will end the proxy creation thread
if (proxyCreatorThread != null) {
proxyCreatorThread.interrupt();
}
}
static boolean currentUserHasRole(String reqRole) {
if (ROLE_WILDCARD.equals(reqRole)) {
return true;
}
return JaasHelper.currentUserHasRole(reqRole);
}
static class ServiceRegistrationHolder {
volatile ServiceRegistration<?> registration;
}
class ProxyServiceFactory implements ServiceFactory<Object> {
private final ProxyManager pm;
private final ServiceReference<?> originalRef;
ProxyServiceFactory(ProxyManager pm, ServiceReference<?> originalRef) {
this.pm = pm;
this.originalRef = originalRef;
}
@Override
public Object getService(Bundle bundle, ServiceRegistration<Object> registration) {
Set<Class<?>> allClasses = new HashSet<Class<?>>();
// This needs to be done on the Client BundleContext since the bundle might be backed by a Service Factory
// in which case it needs to be given a chance to produce the right service for this client.
Object svc = bundle.getBundleContext().getService(originalRef);
Class<?> curClass = svc.getClass();
while (!Object.class.equals(curClass)) {
allClasses.add(curClass);
allClasses.addAll(Arrays.asList(curClass.getInterfaces()));
curClass = curClass.getSuperclass(); // Collect super types too
}
for (Iterator<Class<?>> i = allClasses.iterator(); i.hasNext(); ) {
Class<?> cls = i.next();
if (((cls.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) == 0) ||
((cls.getModifiers() & Modifier.FINAL) > 0) ||
cls.isAnonymousClass() || cls.isLocalClass()) {
// Do not attempt to proxy private, package-default, final, anonymous or local classes
i.remove();
} else {
for (Method m : cls.getDeclaredMethods()) {
if ((m.getModifiers() & Modifier.FINAL) > 0) {
// If the class contains final methods, don't attempt to proxy it
i.remove();
break;
}
}
}
}
InvocationListener il = new ProxyInvocationListener(originalRef);
try {
return pm.createInterceptingProxy(originalRef.getBundle(), allClasses, svc, il);
} catch (UnableToProxyException e) {
throw new RuntimeException(e);
}
}
@Override
public void ungetService(Bundle bundle, ServiceRegistration<Object> registration, Object service) {
bundle.getBundleContext().ungetService(originalRef);
}
}
class ProxyInvocationListener implements InvocationListener {
private final ServiceReference<?> serviceReference;
ProxyInvocationListener(ServiceReference<?> sr) {
this.serviceReference = sr;
}
@Override
public Object preInvoke(Object proxy, Method m, Object[] args) throws Throwable {
String[] sig = new String[m.getParameterTypes().length];
for (int i = 0; i < m.getParameterTypes().length; i++) {
sig[i] = m.getParameterTypes()[i].getName();
}
// The ordering of the keys is important because the first value when iterating has the highest specificity
TreeMap<Specificity, List<String>> roleMappings = new TreeMap<ACLConfigurationParser.Specificity, List<String>>();
boolean foundMatchingConfig = false;
// This can probably be optimized. Maybe we can cache the config object relevant instead of
// walking through all of the ones that have 'service.guard'.
Object guardFilter = null;
for (Configuration config : getServiceGuardConfigs()) {
guardFilter = config.getProperties().get(SERVICE_GUARD_KEY);
if (guardFilter instanceof String) {
Filter filter = myBundleContext.createFilter((String) guardFilter);
if (filter.match(serviceReference)) {
foundMatchingConfig = true;
List<String> roles = new ArrayList<String>();
Specificity s = ACLConfigurationParser.
getRolesForInvocation(m.getName(), args, sig, config.getProperties(), roles);
if (s != Specificity.NO_MATCH) {
roleMappings.put(s, roles);
if (s == Specificity.ARGUMENT_MATCH) {
// No more specific mapping can be found
break;
}
}
}
}
}
if (!foundMatchingConfig) {
if (compulsoryRoles != null && (guardFilter instanceof String)
&& ((String)guardFilter).indexOf("osgi.command.scope") > 0
&& ((String)guardFilter).indexOf("osgi.command.functio") > 0) {
//use compulsoryRoles roles for those karaf command without any ACL
roleMappings.put(Specificity.NAME_MATCH, ACLConfigurationParser.parseRoles(compulsoryRoles));
} else {
// No mappings for this service, anyone can invoke
return null;
}
}
if (roleMappings.size() == 0) {
LOG.info("Service {} has role mapping, but assigned no roles to method {}", serviceReference, m);
throw new SecurityException("Insufficient credentials.");
}
// The first entry on the map has the highest significance because the keys are sorted in the order of
// the Specificity enum.
List<String> allowedRoles = roleMappings.values().iterator().next();
for (String role : allowedRoles) {
if (currentUserHasRole(role)) {
LOG.trace("Allow user with role {} to invoke service {} method {}", role, serviceReference, m);
return null;
}
}
// The current user does not have the required roles to invoke the service.
LOG.info("Current user does not have required roles ({}) for service {} method {} and/or arguments",
allowedRoles, serviceReference, m);
throw new SecurityException("Insufficient credentials.");
}
@Override
public void postInvokeExceptionalReturn(Object token, Object proxy, Method m, Throwable exception) throws Throwable {
}
@Override
public void postInvoke(Object token, Object proxy, Method m, Object returnValue) throws Throwable {
}
}
// This customizer comes into action as the ProxyManager service arrives.
class ServiceProxyCreatorCustomizer implements ServiceTrackerCustomizer<ProxyManager, ProxyManager> {
@Override
public ProxyManager addingService(ServiceReference<ProxyManager> reference) {
runProxyCreator = true;
final ProxyManager svc = myBundleContext.getService(reference);
if (proxyCreatorThread == null && svc != null) {
proxyCreatorThread = newProxyProducingThread(svc);
}
return svc;
}
private Thread newProxyProducingThread(final ProxyManager proxyManager) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (runProxyCreator) {
CreateProxyRunnable proxyCreator = null;
try {
proxyCreator = createProxyQueue.take(); // take waits until there is something on the queue
} catch (InterruptedException ie) {
// part of normal behaviour
}
if (proxyCreator != null) {
try {
proxyCreator.run(proxyManager);
} catch (Exception e) {
LOG.warn("Problem creating secured service proxy", e);
}
}
}
// finished running
proxyCreatorThread = null;
}
});
t.setName(PROXY_CREATOR_THREAD_NAME);
t.setDaemon(true);
t.start();
return t;
}
@Override
public void modifiedService(ServiceReference<ProxyManager> reference, ProxyManager service) {
// no need to react
}
@Override
public void removedService(ServiceReference<ProxyManager> reference, ProxyManager service) {
stopProxyCreator();
}
}
interface CreateProxyRunnable {
long getOriginalServiceID();
void run(ProxyManager pm) throws Exception;
}
}