blob: 9b287cd724693a46a9357f71236d6ba5d7f39835 [file] [log] [blame]
/*
* 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.ace.useradmin.repository;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.ace.repository.Repository;
import org.apache.ace.useradmin.repository.xstream.GroupDTO;
import org.apache.ace.useradmin.repository.xstream.RoleDTO;
import org.apache.ace.useradmin.repository.xstream.UserDTO;
import org.apache.ace.useradmin.repository.xstream.XStreamFactory;
import org.apache.felix.useradmin.RoleFactory;
import org.apache.felix.useradmin.RoleRepositoryStore;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.service.log.LogService;
import org.osgi.service.useradmin.Group;
import org.osgi.service.useradmin.Role;
import org.osgi.service.useradmin.User;
import org.osgi.service.useradmin.UserAdminEvent;
import org.osgi.service.useradmin.UserAdminListener;
import com.thoughtworks.xstream.XStream;
/**
* Felix UserAdmin RoleRepositoryStore implementation that's backed by an ACE Repository
*/
public class UserAdminRepository implements RoleRepositoryStore, UserAdminListener, RepoCurrentChecker {
private final ConcurrentMap<String, Role> m_roleMap;
private final AtomicLong m_version;
private final ReadWriteLock m_rw;
private volatile Repository m_repository;
private volatile LogService m_log;
public UserAdminRepository() {
m_roleMap = new ConcurrentHashMap<>();
m_version = new AtomicLong(-1L);
m_rw = new ReentrantReadWriteLock(true /* fair */);
}
UserAdminRepository(Repository repo, LogService log) {
this();
m_repository = repo;
m_log = log;
}
@Override
public Role addRole(String name, int type) throws Exception {
ensureRoleMapIsCurrent();
m_rw.readLock().lock();
try {
if (m_roleMap.containsKey(name)) {
return null;
}
}
finally {
m_rw.readLock().unlock();
}
Role role;
switch (type) {
case Role.USER:
role = wrapRole(RoleFactory.createUser(name));
break;
case Role.GROUP:
role = wrapRole(RoleFactory.createGroup(name));
break;
default:
throw new IllegalArgumentException("Invalid group type " + type);
}
m_rw.writeLock().lock();
try {
m_roleMap.put(name, role);
// Make sure the repository is correctly synchronized...
m_log.log(LogService.LOG_DEBUG, "Writing role map due to adding of " + ((type == Role.USER) ? "user" : "group") + " role: " + name);
writeRoleMap();
}
finally {
m_rw.writeLock().unlock();
}
return role;
}
public void checkRepoUpToDate(Role context, AtomicLong version) throws IllegalStateException {
long currentVersion = getMostRecentVersion();
// NOTE: do not use local variable for `version.get()` as it appears that javac otherwise replaces it with a
// constant...
if ((version.get() > 0) && currentVersion != version.get()) {
m_rw.writeLock().lock();
try {
m_roleMap.clear();
}
finally {
m_rw.writeLock().unlock();
}
throw new IllegalStateException(context + " out of sync. Please refresh first!");
}
}
@Override
public Role getRoleByName(String name) throws Exception {
if (name == null) {
return null;
}
ensureRoleMapIsCurrent();
m_rw.readLock().lock();
try {
return m_roleMap.get(name);
}
finally {
m_rw.readLock().unlock();
}
}
@Override
public Role[] getRoles(String filterString) throws Exception {
Filter filter = null;
if (filterString != null) {
filter = FrameworkUtil.createFilter(filterString);
}
ensureRoleMapIsCurrent();
List<Role> allRoles;
m_rw.readLock().lock();
try {
allRoles = new ArrayList<>(m_roleMap.values());
}
finally {
m_rw.readLock().unlock();
}
List<Role> matchingRoles = new ArrayList<>();
for (Role role : allRoles) {
if (filter == null || filter.match(role.getProperties())) {
matchingRoles.add(role);
}
}
return matchingRoles.toArray(new Role[matchingRoles.size()]);
}
@Override
public Role removeRole(String name) throws Exception {
m_rw.writeLock().lock();
try {
Role role = m_roleMap.remove(name);
if (role != null) {
// Make sure the repository is correctly synchronized...
m_log.log(LogService.LOG_DEBUG, "Writing role map due to removal of " + ((role.getType() == Role.USER) ? "user" : "group") + " role: " + role.getName());
writeRoleMap();
}
return role;
}
finally {
m_rw.writeLock().unlock();
}
}
@Override
public void roleChanged(UserAdminEvent event) {
m_rw.writeLock().lock();
try {
// Make sure the repository is correctly synchronized...
Role role = event.getRole();
m_log.log(LogService.LOG_DEBUG, "Writing role map due to change of " + ((role.getType() == Role.USER) ? "user" : "group") + " role: " + role.getName());
writeRoleMap();
}
finally {
m_rw.writeLock().unlock();
}
}
final void readRoleMap(long version) throws Exception {
XStream instance = XStreamFactory.getInstance();
Map<String, Role> newRoles = new HashMap<>();
try (Reader r = new InputStreamReader(m_repository.checkout(version));
ObjectInputStream objectInputStream = instance.createObjectInputStream(r)) {
RoleDTO roleDto;
List<RoleDTO> rolesWithMemberships = new ArrayList<>();
try {
while ((roleDto = (RoleDTO) objectInputStream.readObject()) != null) {
User role;
if (roleDto.type == Role.USER) {
role = RoleFactory.createUser(roleDto.name);
}
else if (roleDto.type == Role.GROUP) {
role = RoleFactory.createGroup(roleDto.name);
}
else {
throw new IllegalStateException("");
}
if (roleDto.properties != null) {
for (Entry<Object, Object> entry : roleDto.properties.entrySet()) {
role.getProperties().put(entry.getKey(), entry.getValue());
}
}
if (roleDto.credentials != null) {
for (Entry<Object, Object> entry : roleDto.credentials.entrySet()) {
role.getCredentials().put(entry.getKey(), entry.getValue());
}
}
if (roleDto.memberOf != null && !roleDto.memberOf.isEmpty()) {
rolesWithMemberships.add(roleDto);
}
newRoles.put(role.getName(), role);
}
}
catch (EOFException e) {
// Ignore, this is the way XStream let's us know we're done reading
}
for (RoleDTO role : rolesWithMemberships) {
Role memberRole = newRoles.get(role.name);
for (String memberOf : role.memberOf) {
Role groupRole = newRoles.get(memberOf);
if (groupRole == null) {
throw new IllegalStateException("Target group not found");
}
if (groupRole.getType() != Role.GROUP) {
throw new IllegalStateException("Target is not a group");
}
((Group) groupRole).addMember(memberRole);
}
}
}
m_rw.writeLock().lock();
try {
// "Commit" everything...
m_roleMap.clear();
for (Map.Entry<String, Role> entry : newRoles.entrySet()) {
m_roleMap.put(entry.getKey(), wrapRole(entry.getValue()));
}
m_version.set(version);
}
finally {
m_rw.writeLock().unlock();
}
}
final void writeRoleMap() {
StringWriter writer = new StringWriter();
List<Role> roles = new ArrayList<>(m_roleMap.values());
XStream instance = XStreamFactory.getInstance();
try (ObjectOutputStream stream = instance.createObjectOutputStream(writer, "roles")) {
for (Role role : roles) {
List<String> memberOf = memberOf(roles, role);
if (role.getType() == Role.USER) {
stream.writeObject(new UserDTO((User) role, memberOf));
}
else if (role.getType() == Role.GROUP) {
GroupDTO obj = new GroupDTO((Group) role, memberOf);
stream.writeObject(obj);
}
else {
throw new IllegalStateException("Unsupported role type");
}
}
}
catch (IOException e) {
m_log.log(LogService.LOG_ERROR, "Failed to write role changes to the main role repository", e);
return;
}
try (InputStream inputStream = new ByteArrayInputStream(writer.toString().getBytes())) {
long fromVersion = m_version.longValue();
if (m_repository.commit(inputStream, fromVersion)) {
m_version.set(getMostRecentVersion());
}
}
catch (Exception e) {
m_log.log(LogService.LOG_ERROR, "Failed to commit role changes to the main role repository", e);
}
}
/**
* Add a wrapper around a Role that prevents changes to Users / Groups when the repository is out of sync
*
* @param role
* User or Group role to be wrapped
* @return a wrapped Role
*/
protected Role wrapRole(Role role) {
if (role.getType() == Role.USER) {
return new RepositoryUser((User) role, m_version, this);
}
else if (role.getType() == Role.GROUP) {
return new RepositoryGroup((Group) role, m_version, this);
}
else {
throw new IllegalStateException("Invalid role type: " + role.getType());
}
}
private boolean contains(Role needle, Role[] haystack) {
for (Role role : haystack) {
if (role.getType() == needle.getType() && role.getName().equals(needle.getName())) {
return true;
}
}
return false;
}
private boolean ensureRoleMapIsCurrent() throws Exception {
long actualVersion;
long localVersion;
boolean isCurrent;
m_rw.readLock().lock();
try {
actualVersion = getMostRecentVersion();
localVersion = m_version.longValue();
isCurrent = !m_roleMap.isEmpty() && localVersion == actualVersion;
}
finally {
m_rw.readLock().unlock();
}
if (!isCurrent) {
m_log.log(LogService.LOG_DEBUG, "Reading role map as we're no longer current (" + localVersion + " <=> " + actualVersion + " )...");
readRoleMap(actualVersion);
}
return isCurrent;
}
private long getMostRecentVersion() {
m_rw.readLock().lock();
try {
return m_repository.getRange().getHigh();
}
catch (IOException exception) {
m_log.log(LogService.LOG_WARNING, "Unable to query repository for most recent version!", exception);
return -1L;
}
finally {
m_rw.readLock().unlock();
}
}
private List<String> memberOf(List<Role> roles, Role role) {
List<String> memberOf = new ArrayList<>();
for (Role r : roles) {
if (r instanceof Group) {
Group group = (Group) r;
Role[] members = group.getMembers();
if (members != null) {
if (contains(role, members)) {
memberOf.add(group.getName());
}
}
}
}
return memberOf;
}
}