UserImpl.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.BNode;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryResult;
import org.entrystore.Context;
import org.entrystore.Entry;
import org.entrystore.PrincipalManager;
import org.entrystore.PrincipalManager.AccessProperty;
import org.entrystore.User;
import org.entrystore.repository.RepositoryEvent;
import org.entrystore.repository.RepositoryEventObject;
import org.entrystore.repository.RepositoryException;
import org.entrystore.repository.RepositoryManager;
import org.entrystore.repository.config.Settings;
import org.entrystore.repository.security.Password;
import org.entrystore.repository.test.TestSuite;
import org.entrystore.repository.util.NS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;


public class UserImpl extends RDFResource implements User {
	
	/** Logger */
	static Logger log = LoggerFactory.getLogger(UserImpl.class);

	private String saltedHashedSecret;

	private String language;

	private URI homeContext;
	
	private String externalID;
	
	private RepositoryManager rm;

	public static final IRI customProperty;

	public static final IRI customPropertyKey;

	public static final IRI customPropertyValue;

	static {
		ValueFactory vf = SimpleValueFactory.getInstance();
		customProperty = vf.createIRI(NS.entrystore, "customProperty");
		customPropertyKey = vf.createIRI(NS.entrystore, "customPropertyKey");
		customPropertyValue = vf.createIRI(NS.entrystore, "customPropertyValue");
	}

	/**
	 * Creates a new user
	 * @param entry
	 * @param resourceURI
	 * @param cache
	 */
	//What to do with the cache?
	protected UserImpl(EntryImpl entry, IRI resourceURI, SoftCache cache) {
		super(entry, resourceURI);
		rm = entry.getRepositoryManager();
	}

	/**
	 * Returns the name of the user
	 * @return the name of the user
	 */
	public String getName() {
		//No access control check since everyone should have access to this information.
		return ((PrincipalManager) this.entry.getContext()).getPrincipalName(this.getURI());
	}

	/**
	 * Tries to sets the users name.
	 * @param newName the requested name
	 * @return true if the name was approved, false otherwise
	 */
	public boolean setName(String newName) {
		return ((PrincipalManager) this.entry.getContext()).setPrincipalName(this.getURI(), newName);
	}

	/**
	 * Returns the secret of the user
	 * @return the secret of the user
	 */
	@Deprecated
	public String getSecret() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);

		String secret = null;
		RepositoryConnection rc = null;
		try {
			rc = this.entry.repository.getConnection();
			List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.secret, null, false, resourceURI).asList();
			if (!matches.isEmpty()) {
				secret = matches.get(0).getObject().stringValue();
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to repository", e);
		} finally {
			try {
				rc.close();
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage(), e);
			}
		}

		return secret;
	}
	
	public String getSaltedHashedSecret() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);

		if (this.saltedHashedSecret == null) {
			// we allow an override of the admin password in the config file
			if (rm.getPrincipalManager().getAdminUser().getEntry().getId().equals(entry.getId())) {
				if (rm.getConfiguration().containsKey(Settings.AUTH_ADMIN_SECRET)) {
					log.warn("Admin secret override in config file");
					try {
						this.saltedHashedSecret = Password.getSaltedHash(rm.getConfiguration().getString(Settings.AUTH_ADMIN_SECRET));
						return this.saltedHashedSecret;
					} catch (IllegalArgumentException iae) {
						log.error("Admin secret override was not successful due to password rule violation");
					}
				}
			}

			RepositoryConnection rc = null;
			try {
				rc = this.entry.repository.getConnection();
				List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.saltedHashedSecret, null, false, resourceURI).asList();
				if (!matches.isEmpty()) {
					this.saltedHashedSecret = matches.get(0).getObject().stringValue();
				}
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage(), e);
				throw new RepositoryException("Failed to connect to repository", e);
			} finally {
				try {
					rc.close();
				} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
					log.error(e.getMessage(), e);
				}
			}
		}

		return this.saltedHashedSecret;
	}
	
	/**
	 * Sets the user's password. Does not store the password in clear-text, it is salted and hashed.
	 * 
	 * @param secret The new password
	 * @return True if the password was approved and successfully set, false otherwise
	 */
	public boolean setSecret(String secret) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);
		if (!entry.getRepositoryManager().getPrincipalManager().isValidSecret(secret)) {
			return false;
		}

		String shSecret = null;
		try {
			shSecret = Password.getSaltedHash(secret);
		} catch (IllegalArgumentException iae) {
			log.info(iae.getMessage());
			return false;
		}

		return setSaltedHashedSecret(shSecret);
	}

	/**
	 * Sets the user's hashed password.
	 *
	 * @param shSecret The new salted and hashed password
	 * @return True if the was successfully set, false otherwise
	 */
	public boolean setSaltedHashedSecret(String shSecret) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);

		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				rc.begin();
				try {
					// remove an eventually existing plaintext password and store only a salted hash
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.secret, null, false, resourceURI), resourceURI);
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.saltedHashedSecret, null, false, resourceURI), resourceURI);
					rc.add(resourceURI, RepositoryProperties.saltedHashedSecret, vf.createLiteral(shSecret), resourceURI);
					this.entry.updateModifiedDateSynchronized(rc, vf);
					rc.commit();
					this.saltedHashedSecret = shSecret;
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
					return true;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					rc.close();
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to repository", e);
		}

		return false;
	}

	public void setMetadata(Entry entry, String title, String desc) {
		try {
			Model graph = entry.getLocalMetadata().getGraph();
			ValueFactory vf = SimpleValueFactory.getInstance();
			IRI root = vf.createIRI(entry.getResourceURI().toString());
			graph.add(root, TestSuite.dc_title, vf.createLiteral(title, "en"));
			if (desc != null) {
				graph.add(root, TestSuite.dc_description, vf.createLiteral(desc, "en"));
			}
			entry.getLocalMetadata().setGraph(graph);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
		}
	}
	
	public Context getHomeContext() {
		//No access control check since everyone should have access to this information.
		if (this.homeContext == null) {
			RepositoryConnection rc = null;
			
			try {
				rc = this.entry.repository.getConnection();
                List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.homeContext, null,false, entry.getSesameEntryURI()).asList();
                if (!matches.isEmpty()) {
                    this.homeContext = URI.create(matches.get(0).getObject().stringValue());
                } else { //TODO else case is backwards compatible code, remove in future.
                    matches = rc.getStatements(resourceURI, RepositoryProperties.homeContext, null, false, resourceURI).asList();
                    if (!matches.isEmpty()) {
                        this.homeContext = URI.create(matches.get(0).getObject().stringValue());
                    }
                }
				
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage(), e);
				throw new RepositoryException("Failed to connect to Repository.", e);
			} finally {
				try {
					rc.close();
				} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
					log.error(e.getMessage(), e);
				}
			}
		}
		
		if (this.homeContext != null) {
			Entry eContext = this.entry.getRepositoryManager().getContextManager().getByEntryURI(this.homeContext);
			if (eContext != null) {
				return (Context) eContext.getResource();
			}
		}

		return null;
	}

	public boolean setHomeContext(Context context) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);
		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				rc.begin();
				try {
					//Remove homecontext and remove inverse relation cache.
					RepositoryResult<Statement> iter = rc.getStatements(resourceURI, RepositoryProperties.homeContext, null, false, entry.getSesameEntryURI());
					while (iter.hasNext()) {
						Statement statement = iter.next();
						URI sourceEntryURI = URI.create(statement.getObject().stringValue());
						EntryImpl sourceEntry = (EntryImpl) this.entry.getRepositoryManager().getContextManager().getEntry(sourceEntryURI);
						if (sourceEntry != null) {
							sourceEntry.removeRelationSynchronized(statement, rc, vf);
						}
						rc.remove(statement, entry.getSesameEntryURI());
					}
					iter.close();

					//TODO Remove the following line in future as it corresponds to backward compatability where homecontext where saved in resource graph instead of entry graph.
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.homeContext, null, false, resourceURI), resourceURI);

					//Add new homecontext and add inverse relational cache
					if (context != null) {
						Statement newStatement = vf.createStatement(resourceURI, RepositoryProperties.homeContext, ((EntryImpl) context.getEntry()).getSesameEntryURI(), entry.getSesameEntryURI());
						rc.add(newStatement);
						((EntryImpl) context.getEntry()).addRelationSynchronized(newStatement, rc, this.entry.repository.getValueFactory());
					}
					this.entry.updateModifiedDateSynchronized(rc, vf);
					rc.commit();
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
					return true;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					rc.close();
					//We poke in the internals of entryImpl, to notify that it has relations for later setGraph calls to work
					entry.invRelations = true;
					this.homeContext = context.getEntry().getEntryURI();
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to repository.", e);
		}
		return false;
	}

	public String getLanguage() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);
		if (this.language == null) {
			RepositoryConnection rc = null;
			try {
				rc = this.entry.repository.getConnection();
				List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.language, null, false, resourceURI).asList();
				if (!matches.isEmpty()) {
					this.language = matches.get(0).getObject().stringValue();
				}
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage(), e);
				throw new RepositoryException("Failed to connect to Repository.", e);
			} finally {
				try {
					rc.close();
				} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
					log.error(e.getMessage(), e);
				}
			}
		}
		return this.language;
	}

	public boolean setLanguage(String language) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);

		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				rc.begin();
				try {
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.language, null, false, resourceURI), resourceURI);
					if (language != null) {
						rc.add(resourceURI, RepositoryProperties.language, vf.createLiteral(language), resourceURI);
					}
					this.entry.updateModifiedDateSynchronized(rc, vf);
					rc.commit();
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
					return true;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					rc.close();
					this.language = language;
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to Repository.", e);
		}
		return false;
	}
	
	/**
	 * @return An E-Mail address that can be mapped to an external authentication service, e.g. OpenID.
	 * 
	 * @see org.entrystore.User#getExternalID()
	 */
	public String getExternalID() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);
		if (externalID == null) {
			RepositoryConnection rc = null;
			try {
				rc = this.entry.repository.getConnection();
				List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.externalID, null, false, resourceURI).asList();
				if (!matches.isEmpty()) {
					externalID = matches.get(0).getObject().stringValue();
					String prefix = "mailto:";
					if (externalID.contains(prefix)) {
						externalID = externalID.substring(externalID.lastIndexOf(prefix) + prefix.length());
					}
				}
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage(), e);
				throw new RepositoryException("Failed to connect to repository", e);
			} finally {
				try {
					rc.close();
				} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
					log.error(e.getMessage(), e);
				}
			}
		}
		return this.externalID;
	}

	/**
	 * @param eid External ID, expects an E-Mail address.
	 * 
	 * @see org.entrystore.User#setExternalID(java.lang.String)
	 */
	public boolean setExternalID(String eid) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);

		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				rc.begin();
				try {
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.externalID, null, false, resourceURI), resourceURI);
					if (eid != null) {
						rc.add(resourceURI, RepositoryProperties.externalID, vf.createIRI("mailto:", eid), resourceURI);
						this.entry.updateModifiedDateSynchronized(rc, vf);
					}
					rc.commit();
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
					return true;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					rc.close();
					this.externalID = eid;
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to repository", e);
		}
		return false;
	}

	public Set<URI> getGroups() {
		HashSet<URI> set = new HashSet<URI>();
		List<Statement> relations = this.entry.getRelations();
		for (Statement statement : relations) {
			if (statement.getPredicate().equals(RepositoryProperties.hasGroupMember)) {
				set.add(URI.create(statement.getSubject().toString()));
			}
		}
		return set;
	}

	public Map<String, String> getCustomProperties() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);

		Map<String, String> result = new HashMap<>();
		Model userResourceGraph = getGraph();
		for (Statement s : userResourceGraph.filter(resourceURI, customProperty, null)) {
			if (s.getObject() instanceof BNode) {
				String keyStr = null;
				String valueStr = null;
				Iterator<Statement> argKeyIt = userResourceGraph.filter((BNode) s.getObject(), customPropertyKey, null).iterator();
				if (argKeyIt.hasNext()) {
					Value argKey = argKeyIt.next().getObject();
					if (argKey instanceof Literal) {
						keyStr = ((Literal) argKey).stringValue();
						Iterator<Statement> argValueIt = userResourceGraph.filter((BNode) s.getObject(), customPropertyValue, null).iterator();
						if (argValueIt.hasNext()) {
							Value argValue = argValueIt.next().getObject();
							if (argValue instanceof Literal) {
								valueStr = ((Literal) argValue).stringValue();
							}
						}
					}
				}
				if (keyStr != null && valueStr != null) {
					result.put(keyStr.toLowerCase(), valueStr);
				}
			}
		}

		return result;
	}

	public boolean setCustomProperties(Map<String, String> properties) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);

		if (properties == null) {
			throw new IllegalArgumentException("Parameter must not be null");
		}

		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				rc.begin();
				try {
					rc.remove(rc.getStatements(null, customProperty, null, false, resourceURI), resourceURI);
					rc.remove(rc.getStatements(null, customPropertyKey, null, false, resourceURI), resourceURI);
					rc.remove(rc.getStatements(null, customPropertyValue, null, false, resourceURI), resourceURI);

					for (java.util.Map.Entry<String, String> e : properties.entrySet()) {
						BNode bnode = vf.createBNode();
						rc.add(resourceURI, customProperty, bnode, resourceURI);
						rc.add(bnode, customPropertyKey, vf.createLiteral(e.getKey()), resourceURI);
						rc.add(bnode, customPropertyValue, vf.createLiteral(e.getValue()), resourceURI);
					}

					this.entry.updateModifiedDateSynchronized(rc, vf);

					rc.commit();
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
					return true;
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					rc.close();
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage(), e);
			throw new RepositoryException("Failed to connect to repository", e);
		}
		return false;
	}

	public boolean isDisabled() {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.ReadResource);
		RepositoryConnection rc = null;
		try {
			rc = this.entry.repository.getConnection();
			List<Statement> matches = rc.getStatements(resourceURI, RepositoryProperties.disabled, null, false, resourceURI).asList();
			if (!matches.isEmpty()) {
				Literal l = (Literal) matches.get(0).getObject();
				return l.booleanValue();
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage());
			throw new RepositoryException("Failed to connect to repository", e);
		} finally {
			try {
				if (rc != null) {
					rc.close();
				}
			} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
				log.error(e.getMessage());
			}
		}
		return false;
	}

	public void setDisabled(boolean disabled) {
		rm.getPrincipalManager().checkAuthenticatedUserAuthorized(entry, AccessProperty.WriteResource);
		try {
			synchronized (this.entry.repository) {
				RepositoryConnection rc = this.entry.repository.getConnection();
				ValueFactory vf = this.entry.repository.getValueFactory();
				try {
					rc.begin();
					rc.remove(rc.getStatements(resourceURI, RepositoryProperties.disabled, null, false, resourceURI), resourceURI);
					if (disabled) {
						rc.add(resourceURI, RepositoryProperties.disabled, vf.createLiteral(disabled), resourceURI);
					}
					this.entry.updateModifiedDateSynchronized(rc, vf);
					rc.commit();
					entry.getRepositoryManager().fireRepositoryEvent(new RepositoryEventObject(entry, RepositoryEvent.ResourceUpdated));
				} catch (Exception e) {
					log.error(e.getMessage(), e);
					rc.rollback();
				} finally {
					if (rc != null) {
						rc.close();
					}
				}
			}
		} catch (org.eclipse.rdf4j.repository.RepositoryException e) {
			log.error(e.getMessage());
			throw new RepositoryException("Failed to connect to repository", e);
		}
	}

}