PrincipalManagerImpl.java

/*
 * Copyright (c) 2007-2017 MetaSolutions AB
 *
 * Licensed 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.entrystore.impl;

import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.RepositoryResult;
import org.entrystore.AuthorizationException;
import org.entrystore.Entry;
import org.entrystore.EntryType;
import org.entrystore.GraphType;
import org.entrystore.Group;
import org.entrystore.PrincipalManager;
import org.entrystore.User;
import org.entrystore.repository.security.Password;
import org.entrystore.repository.util.URISplit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;


/**
 * Creates a
 * @author Olov Wikberg, IML UmeƄ University
 * @author matthias
 * @author Hannes Ebner
 *
 */
public class PrincipalManagerImpl extends EntryNamesContext implements PrincipalManager {
	private static final Logger log = LoggerFactory.getLogger(PrincipalManagerImpl.class);
	private static final ThreadLocal<URI> authenticatedUserURI = new ThreadLocal<>();
	public User adminUser = null;
	public Group adminGroup = null;
	public User guestUser = null;
	public Group userGroup = null;

	private EntryImpl allPrincipals;

	private static final String ENV_ADMIN_PASSWORD = "ENTRYSTORE_ADMIN_PASSWORD";

	/**
	 * Creates a principal manager
	 * @param entry this principal managers entry
	 * @param uri this principal managers URI
	 * @param cache
	 */
	public PrincipalManagerImpl(EntryImpl entry, String uri, SoftCache cache) {
		super(entry, uri, cache);
	}


	public String getPrincipalName(URI principal) {
		Entry principalEntry = null;
		User u = getUser(principal);
		if (u != null) {
			principalEntry = u.getEntry();
		} else {
			Group g = getGroup(principal);
			if (g != null) {
				principalEntry = g.getEntry();
			}
		}

		if (principalEntry == null) {
			throw new org.entrystore.repository.RepositoryException("Unable to resolve URI into principal: " + principal);
		}

		checkAuthenticatedUserAuthorized(principalEntry, AccessProperty.ReadMetadata);
		return getName(principalEntry.getEntryURI());
	}

	public Entry getPrincipalEntry(String name) {
		Entry principalEntry = getEntryByName(name.toLowerCase());
		if (principalEntry == null) {
			return null;
		} else if (principalEntry.getGraphType() == GraphType.User ||
						principalEntry.getGraphType() == GraphType.Group) {
			return principalEntry;
		}
		throw new org.entrystore.repository.RepositoryException("Found entry for the name is not a principal...\n" +
				"this is either a programming error or someone have been tampering with the RDF directly.");
	}

	public boolean setPrincipalName(URI principal, String newName) {
		if (principal == null || newName == null) {
			throw new IllegalArgumentException("Parameters must not be null");
		}
		String noZeroLengthSpaceName = newName.replaceAll("[\\p{Cf}]", "");
		String trimmedName = org.apache.commons.lang3.StringUtils.trimToNull(noZeroLengthSpaceName);
		if (trimmedName == null) {
			throw new IllegalArgumentException("Principal name must not be null");
		}
		String normalizedName = trimmedName.toLowerCase();
		URISplit us = new URISplit(principal, this.entry.getRepositoryManager().getRepositoryURL());
		Entry principalEntry = getByEntryURI(us.getMetaMetadataURI());
		if (principalEntry == null) {
			throw new org.entrystore.repository.RepositoryException("Cannot find an entry for the specified URI");
		} else if (principalEntry.getGraphType() == GraphType.User || principalEntry.getGraphType() == GraphType.Group) {
			return setEntryName(us.getMetaMetadataURI(), normalizedName);
		}
		throw new org.entrystore.repository.RepositoryException("Given URI does not refer to a Principal.");
	}

    public boolean isUserAdminOrAdminGroup(URI principal) {
        URI currentUserURI = getAuthenticatedUserURI();
        URI adminUserURI = getAdminUser().getURI();
        setAuthenticatedUserURI(adminUserURI);
        if (principal == null) {
            principal = currentUserURI;
        }
        User user = getUser(principal);
        if (adminUserURI.equals(principal)
                || getAdminGroup().isMember(user)) {
            setAuthenticatedUserURI(currentUserURI);
            return true;
        }
        setAuthenticatedUserURI(currentUserURI);
        return false;
    }


    /**
	 * Returns this principal managers all user URIs
	 * @return all user URIs in this principal manager
	 */
	public List<URI> getUsersAsUris() {
		Iterator <URI> entryIterator = getEntries().iterator();
		List<URI> userUris = new ArrayList<>();

		//sort out the users
		while(entryIterator.hasNext()) {
			URI nextURI = entryIterator.next();

			Entry nextEntry = getByEntryURI(nextURI);
			if(nextEntry.getGraphType() == GraphType.User) {
				userUris.add(nextEntry.getResourceURI());
			}
		}

		return userUris;
	}

	/**
	 * Returns this principal managers all user URIs
	 * @return all user URIs in this principal manager
	 */
	public List<User> getUsers() {
		Iterator<URI> entryIterator = getEntries().iterator();
		List<User> userUris = new ArrayList<>();

		//sort out the users
		while(entryIterator.hasNext()) {
			URI nextURI = entryIterator.next();

			Entry nextEntry = getByEntryURI(nextURI);
			if(nextEntry.getGraphType() == GraphType.User) {
				userUris.add((User) nextEntry.getResource());
			}
		}

		return userUris;
	}


	/**
	 * Returns a User object representing a user.
	 * @param userUri The URI to the user.
	 * @return the User object
	 */
	public User getUser(URI userUri) {
		for(Entry user: getByResourceURI(userUri)) {
			if (user.getGraphType() == GraphType.User) {
				return (User) user.getResource();
			}
		}
		return null;
	}

	/**
	 * Returns a Group object representing a group of users.
	 * @param groupUri URI to the group.
	 * @return the Group object
	 */
	public Group getGroup(URI groupUri) {
		for(Entry user: getByResourceURI(groupUri)) {
			if (user.getGraphType() == GraphType.Group) {
				return (Group) user.getResource();
			}
		}
		return null;
	}

	public Set <URI> getGroupUris() {
		Iterator <URI> entryIterator = getEntries().iterator();
		Set <URI> groupUris = new HashSet<>();

		//sort out the groups
		while(entryIterator.hasNext()) {
			URI nextURI = entryIterator.next();

			Entry nextGroup = getByEntryURI(nextURI);
			if (GraphType.Group.equals(nextGroup.getGraphType())) {
				groupUris.add(nextGroup.getResourceURI());
			}
		}

		return groupUris;
	}

	public Set <URI> getGroupUris(URI userUri) {
		Iterator <URI> entryIterator = getEntries().iterator();
		Set <URI> groupUris = new HashSet<>();

		User user = getUser(userUri);
		if (user != null) {
			while(entryIterator.hasNext()) {
				URI nextURI = entryIterator.next();
				Entry nextEntry = getByEntryURI(nextURI);
				if(GraphType.Group.equals(nextEntry.getGraphType())) {
					Group nextGroup = (Group) nextEntry.getResource();
					if(nextGroup != null) {
						if(nextGroup.isMember(user)) {
							groupUris.add(nextGroup.getURI());
						}
					}
				}
			}
		}

		return groupUris;
	}

	public List<URI> getGroupEntryUris() {
		Iterator < URI > entryIterator = getEntries().iterator();
		List<URI> groupUris = new ArrayList<>();

		//sort out the groups
		while(entryIterator.hasNext()) {
			URI nextURI = entryIterator.next();
			Entry e = getByEntryURI(nextURI);
			if(e.getGraphType() == GraphType.Group) {
				groupUris.add(nextURI);
			}
		}

		return groupUris;
	}

	/**
	 * Sets which user that is authenticated in this specific thread.
	 * @param userUri The URI to the user that was authenticated.
	 */
	public void setAuthenticatedUserURI(URI userUri) {
		authenticatedUserURI.set(userUri);
	}

	public URI getAuthenticatedUserURI() {
		return authenticatedUserURI.get();
	}

	@Override
	public boolean currentUserIsGuest() {
		URI currentUserUri = authenticatedUserURI.get();
		return currentUserUri == null || getGuestUser().getURI().equals(currentUserUri);
	}

	@Override
	public boolean currentUserIsAdminOrAdminGroup() {
		URI currentUserUri = authenticatedUserURI.get();
		return isUserAdminOrAdminGroup(currentUserUri);
	}

	/**
	 * Checks if the authenticated user it authorized to perform a specific task on a Entry. The task is defined by an access property.
	 * @param entry the entry on which to check the specified accessProperty.
	 * @param accessProperty the access property to check the entry for.
	 * @throws AuthorizationException if not allowed.
	 */
	public void checkAuthenticatedUserAuthorized(Entry entry, AccessProperty accessProperty) throws AuthorizationException {

		//is check authorization on?
		if(!entry.getRepositoryManager().isCheckForAuthorization()) {
			return;
		}

		URI currentUserURI = getAuthenticatedUserURI();

		//is anyone logged in on this thread?
		if (currentUserURI == null) {
			currentUserURI = getGuestUser().getURI();
			log.warn("Authenticated user not set, assuming guest user");
		}

		//is admin?
		if(currentUserURI.equals(getAdminUser().getURI())) {
			return;
		}

		if (currentUserURI.equals(entry.getResourceURI()) &&
				(accessProperty == AccessProperty.ReadMetadata || accessProperty == AccessProperty.ReadResource)) {
			return;
		}
		//Switch to admin so that the PrincipalManager can perform all
		//neccessary checks without being hindered by itself (results in loops).
		setAuthenticatedUserURI(getAdminUser().getURI());

		try {

			//Fetch the current user from thread local.
			User currentUser = getUser(currentUserURI);

			//Check if user is in admingroup.
			if (getAdminGroup().isMember(currentUser)) {
				return;
			}

			Entry contextEntry = entry.getContext().getEntry();
			//Check if user is owner of surrounding context
			if (hasAccess(currentUser, contextEntry, AccessProperty.Administer)) {
				return;
			} else {
				//If entry overrides Context ACL (only relevant if the user is not an owner of the context)
				if(entry.hasAllowedPrincipals()) {
					if (hasAccess(currentUser, entry, AccessProperty.Administer)
                    || hasAccess(currentUser, entry, accessProperty)) {
						return;
					} else if (accessProperty == AccessProperty.ReadMetadata
                            && hasAccess(currentUser, entry, AccessProperty.WriteMetadata)) {
                        return; //WriteMetadata implies ReadMetadata
                    } else if (accessProperty == AccessProperty.ReadResource
                        && hasAccess(currentUser, entry, AccessProperty.WriteResource)) {
                        return; //WriteResource implies ReadResource
                    }
				} else {
					//Check if user has access to the surrounding context of the entry.
					if (accessProperty == AccessProperty.ReadMetadata || accessProperty == AccessProperty.ReadResource) {
						if (hasAccess(currentUser, contextEntry, AccessProperty.ReadResource)
                                || hasAccess(currentUser, contextEntry, AccessProperty.WriteResource)) {
							return; //Both read and write on the context resource implies read on all entries for both the metadata and the resource.
						}
					} else {
						if (hasAccess(currentUser, contextEntry, AccessProperty.WriteResource)) {
							return;
						}
					}
				}
			}

			throw new AuthorizationException(currentUser, entry, accessProperty);
		} finally {
			//Switch back to the current user.
			setAuthenticatedUserURI(currentUserURI);
		}
	}

	protected boolean hasAccess(User currentUser, Entry entry, AccessProperty prop) {
		Set<URI> principals = entry.getAllowedPrincipalsFor(prop);
		if (!principals.isEmpty()) {

			//Check if guest is in principals.
			if (principals.contains(getGuestUser().getURI())) {
				return true;
			}

			//Check if the special "user" group is in principals and user is not guest.
			if (currentUser != getGuestUser() && principals.contains(getUserGroup().getURI())) {
				return true;
			}

			//Check if user is in principals.
			if (principals.contains(currentUser.getURI())) {
				return true;
			}

			//Check if any of the groups the user belongs to is in principals
			Set<URI> groups = getGroupUris(currentUser.getURI());
			groups.retainAll(principals);
			if (!groups.isEmpty()) {
				return true;
			}
		}

		if (prop != AccessProperty.Administer) {
			principals = entry.getAllowedPrincipalsFor(AccessProperty.Administer);
			if (!principals.isEmpty()) {

				//Check if user is in principals.
				if (principals.contains(currentUser.getURI())) {
					return true;
				}

				//Check if any of the groups the user belongs to is in principals
				Set<URI> groups = getGroupUris(currentUser.getURI());
				groups.retainAll(principals);
				return !groups.isEmpty();
			}
		}
		return false;
	}

	public Set<AccessProperty> getRights(Entry entry) {
		Set<AccessProperty> set = new HashSet<>();
		//is check authorization on?
		if(!entry.getRepositoryManager().isCheckForAuthorization()) {
			set.add(AccessProperty.Administer);
			return set;
		}

		URI currentUserURI = getAuthenticatedUserURI();

		//is anyone logged in on this thread?
		if (currentUserURI == null) {
			//TODO, should we perhaps assume guest if none set?
			log.error("Authenticated user not set, should at least be guest.");
			throw new AuthorizationException(null, entry, null);
		}

		//is admin?
		if(currentUserURI.equals(getAdminUser().getURI())) {
			set.add(AccessProperty.Administer);
			return set;
		}

		//Switch to admin so that the PrincipalManager can perform all
		//neccessary checks without being hindered by itself (results in loops).
		setAuthenticatedUserURI(getAdminUser().getURI());

		try {

			//Fetch the current user from thread local.
			User currentUser = getUser(currentUserURI);

			//Check if user is in admingroup.
			if (getAdminGroup().isMember(currentUser)) {
				set.add(AccessProperty.Administer);
				return set;
			}

			Entry contextEntry = entry.getContext().getEntry();
			//Check if user is owner of surrounding context
			if (hasAccess(currentUser, contextEntry, AccessProperty.Administer)) {
				set.add(AccessProperty.Administer);
			} else {
				//If entry overrides Context ACL (only relevant if the user is not an owner of the context)
				if(entry.hasAllowedPrincipals()) {
					if (hasAccess(currentUser, entry, AccessProperty.Administer)) {
						set.add(AccessProperty.Administer);
						return set;
					} else {
						if (hasAccess(currentUser, entry, AccessProperty.WriteMetadata)) {
							set.add(AccessProperty.WriteMetadata);
						} else if (hasAccess(currentUser, entry, AccessProperty.ReadMetadata)) {
							set.add(AccessProperty.ReadMetadata);
						}
						if (hasAccess(currentUser, entry, AccessProperty.WriteResource)) {
							set.add(AccessProperty.WriteResource);
						} else if (hasAccess(currentUser, entry, AccessProperty.ReadResource)) {
							set.add(AccessProperty.ReadResource);
						}
					}
				} else {
					if (hasAccess(currentUser, contextEntry, AccessProperty.WriteResource)) {
						set.add(AccessProperty.Administer);
					} else if (hasAccess(currentUser, contextEntry, AccessProperty.ReadResource)) {
						set.add(AccessProperty.ReadMetadata);
						set.add(AccessProperty.ReadResource);
					}
				}
			}
		} finally {
			//Switch back to the current user.
			setAuthenticatedUserURI(currentUserURI);
		}
		return set;
	}


	/**
	 * Checks if a secret is valid.
	 * @param secret Secret to be checked.
	 * @return true If the secret fullfils minimum requirements, currently a minimum length of 8 characters.
	 */
	public boolean isValidSecret(String secret) {
		return Password.conformsToRules(secret);
	}

	public User getAdminUser() {
		return adminUser;
	}

	public Group getAdminGroup() {
		return adminGroup;
	}

	public User getGuestUser() {
		return guestUser;
	}

	public Group getUserGroup() {
		return userGroup;
	}

	@Override
	public void initResource(EntryImpl newEntry) throws RepositoryException {
		if (newEntry.getEntryType() != EntryType.Local) {
			return;
		}
		switch (newEntry.getGraphType()) {
		case User:
			newEntry.setResource(new UserImpl(newEntry, newEntry.getSesameResourceURI(), cache));
			break;
		case Group:
			newEntry.setResource(new GroupImpl(newEntry, newEntry.getSesameResourceURI(), cache));
			break;
		default:
			super.initResource(newEntry);
		}
	}

	public void initializeSystemEntries() {
		super.initializeSystemEntries();
		Entry adminUserEntry;
		Entry adminGroupEntry;
		Entry userGroupEntry;
		Entry guestUserEntry;

		guestUserEntry = get("_guest");
		if (guestUserEntry != null) {
			guestUser = (User) guestUserEntry.getResource();
		} else {
			guestUserEntry = this.createNewMinimalItem(null, null, EntryType.Local, GraphType.User, null, "_guest");
			setMetadata(guestUserEntry, "Guest user", "All non logged in users will automatically appear as this user.");
			guestUser = (User) guestUserEntry.getResource();
			guestUser.setName("guest");
			guestUserEntry.addAllowedPrincipalsFor(AccessProperty.ReadMetadata, guestUser.getURI());
			log.info("Successfully added the guest user");
		}
		addSystemEntryToSystemEntries(guestUserEntry.getEntryURI());


		adminUserEntry = get("_admin");
		if (adminUserEntry != null) {
			adminUser = (User) adminUserEntry.getResource();
		} else {
			adminUserEntry = this.createNewMinimalItem(null, null, EntryType.Local, GraphType.User, null, "_admin");
			setMetadata(adminUserEntry, "Admin user", "Default super user, has all rights.");
			adminUser = (User) adminUserEntry.getResource();
			adminUser.setName("admin");
			String adminSecret = System.getenv(ENV_ADMIN_PASSWORD);
			if (adminSecret != null) {
				log.info("Setting admin password based on environment variable {}", ENV_ADMIN_PASSWORD);
				if (!adminUser.setSecret(adminSecret)) {
					log.warn("Password in environment variable does not conform to configured rules, initializing admin user without password");
				}
			} else {
				log.warn("Environment variable {} not found, initializing admin user without password", ENV_ADMIN_PASSWORD);
			}
			adminUserEntry.addAllowedPrincipalsFor(AccessProperty.ReadMetadata, guestUser.getURI());
			log.info("Successfully added the admin user");
		}
		addSystemEntryToSystemEntries(adminUserEntry.getEntryURI());

		adminGroupEntry = get("_admins");
		if(adminGroupEntry != null) {
			adminGroup = (Group) adminGroupEntry.getResource();
		} else {
			adminGroupEntry = this.createNewMinimalItem(null, null, EntryType.Local, GraphType.Group, null, "_admins");
			setMetadata(adminGroupEntry, "Admin group", "All members of this group have super user rights.");
			adminGroup = (Group) adminGroupEntry.getResource();
			adminGroup.setName("admins");
			adminGroupEntry.addAllowedPrincipalsFor(AccessProperty.ReadMetadata, guestUser.getURI());
			log.info("Successfully added the admin group");
		}
		addSystemEntryToSystemEntries(adminGroupEntry.getEntryURI());

		userGroupEntry = get("_users");
		if(userGroupEntry == null) {
			userGroupEntry = this.createNewMinimalItem(null, null, EntryType.Local, GraphType.Group, null, "_users");
			setMetadata(userGroupEntry, "Users group", "All regular users are part of this group.");
			setPrincipalName(userGroupEntry.getResourceURI(), "users");
			userGroupEntry.addAllowedPrincipalsFor(AccessProperty.ReadMetadata, guestUser.getURI());
			log.info("Successfully added the user group");
		}
		EntryImpl e = (EntryImpl) userGroupEntry;
		e.setResource(new SystemGroup(e, e.getSesameResourceURI()) {
			@Override
			public boolean isMember(User user) {
				return (user != null &&
						PrincipalManagerImpl.this.guestUser != null &&
						!user.getURI().equals(PrincipalManagerImpl.this.guestUser.getURI()));
				// return true;
			}
			@Override
			public List<User> members() {
                this.entry.getRepositoryManager().getPrincipalManager().checkAuthenticatedUserAuthorized(this.entry, AccessProperty.ReadResource);
                return getUsers();
			}
			@Override
			public List<URI> memberUris() {
                this.entry.getRepositoryManager().getPrincipalManager().checkAuthenticatedUserAuthorized(this.entry, AccessProperty.ReadResource);
                return getUsersAsUris();
			}
		});

		userGroup = (Group) userGroupEntry.getResource();
		addSystemEntryToSystemEntries(userGroupEntry.getEntryURI());
	}

	/**
	 * @param externalID
	 *            An E-Mail address
	 * @return A user that can be mapped to the external E-Mail address (that
	 *         e.g. originates from an OpenID service)
	 * @see org.entrystore.PrincipalManager#getUserByExternalID(java.lang.String)
	 */
	public User getUserByExternalID(String externalID) {
		RepositoryConnection rc = null;
		Resource userResourceURI = null;
		try {
			rc = entry.getRepository().getConnection();
			ValueFactory vf = rc.getValueFactory();
			RepositoryResult<Statement> rr = rc.getStatements(null, RepositoryProperties.externalID, vf.createIRI("mailto:", externalID), false);
			if (rr.hasNext()) {
				userResourceURI = rr.next().getSubject();
			}
			rr.close();
		} catch (RepositoryException re) {
			log.error(re.getMessage(), re);
		} finally {
			if (rc != null) {
				try {
					rc.close();
				} catch (RepositoryException ignore) {}
			}
		}
		if (userResourceURI == null) {
			return null;
		}
		return getUser(URI.create(userResourceURI.stringValue()));
	}

}