/*******************************************************************************
 * Copyright (c) 2001, 2008 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.useradmin;

import java.util.*;
import org.osgi.framework.*;
import org.osgi.service.log.LogService;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.PreferencesService;
import org.osgi.service.useradmin.UserAdminEvent;
import org.osgi.service.useradmin.UserAdminPermission;

/**
 * This interface is used to manage a database of named roles, which can
 * be used for authentication and authorization purposes.
 * <p>
 * This version of UserAdmin defines two types of roles: "User" and
 * "Group". Each type of role is represented by an "int" constant and an
 * interface. The range of positive integers is reserved for new types of
 * roles that may be added in the future. When defining proprietary role
 * types, negative constant values must be used.
 * <p>
 * Every role has a name and a type.
 * <p>
 * A {@link User} role can be configured with credentials (e.g., a password)
 * and properties (e.g., a street address, phone number, etc.).
 * <p>
 * A {@link Group} role represents an aggregation of {@link User} and
 * {@link Group} roles. In
 * other words, the members of a Group role are roles themselves.
 * <p>
 * Every UserAdmin manages and maintains its own
 * namespace of roles, in which each role has a unique name.
 */

public class UserAdmin implements org.osgi.service.useradmin.UserAdmin {

	protected Vector<Role> users;
	protected Vector<Role> roles;
	protected BundleContext context;
	protected UserAdminEventProducer eventProducer;
	protected boolean alive;
	protected UserAdminStore userAdminStore;
	protected UserAdminPermission adminPermission;
	protected ServiceReference reference;
	protected LogTracker log;

	protected UserAdmin(PreferencesService preferencesService, BundleContext context) throws Exception {
		roles = new Vector<>();
		users = new Vector<>();
		this.context = context;

		log = new LogTracker(context, System.out);
		alive = true;
		//This handles user admin persistence
		try {
			userAdminStore = new UserAdminStore(preferencesService, this, log);
			userAdminStore.init();
		} catch (Exception e) {
			log.log(LogService.LOG_ERROR, UserAdminMsg.Backing_Store_Read_Exception, e);
			throw e;
		}
	}

	protected void setServiceReference(ServiceReference reference) {
		if (this.reference == null) {
			this.reference = reference;

			eventProducer = new UserAdminEventProducer(reference, context, log);
		}
	}

	/**
	 * Creates a role with the given name and of the given type.
	 *
	 * <p> If a role was created, a UserAdminEvent of type
	 * {@link UserAdminEvent#ROLE_CREATED} is broadcast to any
	 * UserAdminListener.
	 *
	 * @param name The name of the role to create.
	 * @param type The type of the role to create. Must be either
	 * {@link Role#USER} or {@link Role#GROUP}.
	 *
	 * @return The newly created role, or <code>null</code> if a role with
	 * the given name already exists.
	 *
	 * @throws IllegalArgumentException if <tt>type</tt> is invalid.
	 *
	 * @throws SecurityException If a security manager exists and the caller
	 * does not have the <tt>UserAdminPermission</tt> with name <tt>admin</tt>.
	 */
	@Override
	public org.osgi.service.useradmin.Role createRole(String name, int type) {
		checkAlive();
		checkAdminPermission();
		if (name == null) {
			throw (new IllegalArgumentException(UserAdminMsg.CREATE_NULL_ROLE_EXCEPTION));
		}
		if ((type != org.osgi.service.useradmin.Role.GROUP) && (type != org.osgi.service.useradmin.Role.USER)) {
			throw (new IllegalArgumentException(UserAdminMsg.CREATE_INVALID_TYPE_ROLE_EXCEPTION));
		}
		//if the role already exists, return null
		if (getRole(name) != null) {
			return (null);
		}

		synchronized (this) {
			return createRole(name, type, true);
		}
	}

	protected org.osgi.service.useradmin.Role createRole(String name, int type, boolean store) {
		Role newRole = null;
		if (type == org.osgi.service.useradmin.Role.ROLE) {
			newRole = new Role(name, this);
		} else if (type == org.osgi.service.useradmin.Role.USER) {
			newRole = new User(name, this);
		} else if (type == org.osgi.service.useradmin.Role.GROUP) {
			newRole = new Group(name, this);
		} else //unknown type
		{
			return (null);
		}
		if (store) {
			try {
				userAdminStore.addRole(newRole);
			} catch (BackingStoreException ex) {
				return (null);
			}
			if (eventProducer != null) {
				eventProducer.generateEvent(UserAdminEvent.ROLE_CREATED, newRole);
			}
		}
		if (type == org.osgi.service.useradmin.Role.GROUP || type == org.osgi.service.useradmin.Role.USER) {
			users.addElement(newRole);
		}
		roles.addElement(newRole);
		return (newRole);
	}

	/**
	 * Removes the role with the given name from this UserAdmin.
	 *
	 * <p> If the role was removed, a UserAdminEvent of type
	 * {@link UserAdminEvent#ROLE_REMOVED} is broadcast to any
	 * UserAdminListener.
	 *
	 * @param name The name of the role to remove.
	 *
	 * @return <code>true</code> If a role with the given name is present in this
	 * UserAdmin and could be removed, otherwise <code>false</code>.
	 *
	 * @throws SecurityException If a security manager exists and the caller
	 * does not have the <tt>UserAdminPermission</tt> with name <tt>admin</tt>.
	 */
	@Override
	public boolean removeRole(String name) {
		checkAlive();
		checkAdminPermission();
		if (name.equals(Role.anyoneString)) {
			//silently ignore
			return (true);
		}
		synchronized (this) {
			Role role = (org.eclipse.equinox.internal.useradmin.Role) getRole(name);
			if (role != null) {
				try {
					userAdminStore.removeRole(role);
				} catch (BackingStoreException ex) {
					return (false);
				}
				roles.removeElement(role);
				users.removeElement(role);
				role.destroy();
				eventProducer.generateEvent(UserAdminEvent.ROLE_REMOVED, role);
				role = null;
				return (true);
			}
			return (false);
		}
	}

	/**
	 * Gets the role with the given name from this UserAdmin.
	 *
	 * @param name The name of the role to get.
	 *
	 * @return The requested role, or <code>null</code> if this UserAdmin does
	 * not have a role with the given name.
	 */
	@Override
	public org.osgi.service.useradmin.Role getRole(String name) {
		checkAlive();
		if (name == null) {
			return (null);
		}
		synchronized (this) {
			Enumeration<Role> e = roles.elements();
			while (e.hasMoreElements()) {
				Role role = e.nextElement();
				if (role.getName().equals(name)) {
					return (role);
				}
			}
			return (null);
		}
	}

	/**
	 * Gets the roles managed by this UserAdmin that have properties matching
	 * the specified LDAP filter criteria. See
	 * <code>org.osgi.framework.Filter</code> or IETF RFC 2254 for a
	 * description of the filter syntax. If a <code>null</code> filter is
	 * specified, all roles managed by this UserAdmin are returned.
	 *
	 * @param filterString The filter criteria to match.
	 *
	 * @return The roles managed by this UserAdmin whose properties
	 * match the specified filter criteria, or all roles if a
	 * <code>null</code> filter is specified.
	 *
	 */
	@Override
	public org.osgi.service.useradmin.Role[] getRoles(String filterString) throws InvalidSyntaxException {
		checkAlive();
		Vector<Role> returnedRoles;
		synchronized (this) {
			if (filterString == null) {
				returnedRoles = roles;
			} else {
				Filter filter = context.createFilter(filterString); //We do this first so an
				//InvalidSyntaxException will be
				//thrown even if there are no roles
				//present.
				returnedRoles = new Vector<>();
				for (int i = 0; i < roles.size(); i++) {
					Role role = roles.elementAt(i);
					if (filter.match(role.getProperties())) {
						returnedRoles.addElement(role);
					}
				}
			}
			int size = returnedRoles.size();
			if (size == 0) {
				return (null);
			}
			Role[] roleArray = new Role[size];
			returnedRoles.copyInto(roleArray);
			return (roleArray);
		}
	}

	/**
	 * Gets the user with the given property key-value pair from the UserAdmin
	 * database. This is a convenience method for retrieving a user based on
	 * a property for which every user is supposed to have a unique value
	 * (within the scope of this UserAdmin), such as a user's
	 * X.500 distinguished name.
	 *
	 * @param key The property key to look for.
	 * @param value The property value to compare with.
	 *
	 * @return A matching user, if <em>exactly</em> one is found. If zero or
	 * more than one matching users are found, <code>null</code> is returned.
	 */
	@Override
	public org.osgi.service.useradmin.User getUser(String key, String value) {
		checkAlive();
		if (key == null) {
			return (null);
		}
		User user;
		User foundUser = null;
		Dictionary<String, Object> props;
		String keyValue;
		synchronized (this) {
			Enumeration<Role> e = users.elements();
			while (e.hasMoreElements()) {
				user = (User) e.nextElement();
				props = user.getProperties();
				keyValue = (String) props.get(key);
				if (keyValue != null && keyValue.equals(value)) {
					if (foundUser != null) {
						return (null); //we found more than one match	
					}
					foundUser = user;
				}
			}
			return (foundUser);
		}
	}

	/**
	 * Creates an Authorization object that encapsulates the specified user
	 * and the roles it possesses. The <code>null</code> user is interpreted
	 * as the anonymous user.
	 *
	 * @param user The user to create an Authorization object for, or
	 * <code>null</code> for the anonymous user.
	 *
	 * @return the Authorization object for the specified user.
	 */
	@Override
	public org.osgi.service.useradmin.Authorization getAuthorization(org.osgi.service.useradmin.User user) {
		checkAlive();
		return (new Authorization((User) user, this));
	}

	protected synchronized void destroy() {
		alive = false;
		eventProducer.close();
		userAdminStore.destroy();

		log.close();
	}

	public void checkAdminPermission() {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			if (adminPermission == null) {
				adminPermission = new UserAdminPermission(UserAdminPermission.ADMIN, null);
			}
			sm.checkPermission(adminPermission);
		}
	}

	public void checkGetCredentialPermission(String credential) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(new org.osgi.service.useradmin.UserAdminPermission(credential, org.osgi.service.useradmin.UserAdminPermission.GET_CREDENTIAL));
		}
	}

	public void checkChangeCredentialPermission(String credential) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(new org.osgi.service.useradmin.UserAdminPermission(credential, org.osgi.service.useradmin.UserAdminPermission.CHANGE_CREDENTIAL));
		}
	}

	public void checkChangePropertyPermission(String property) {
		SecurityManager sm = System.getSecurityManager();
		if (sm != null) {
			sm.checkPermission(new org.osgi.service.useradmin.UserAdminPermission(property, org.osgi.service.useradmin.UserAdminPermission.CHANGE_PROPERTY));
		}
	}

	public void checkAlive() {
		if (!alive) {
			throw (new IllegalStateException(UserAdminMsg.USERADMIN_UNREGISTERED_EXCEPTION));
		}
	}

}
