/*
 *  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
 *
 *      https://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.tools.ant.types;

import java.lang.reflect.Constructor;
import java.net.SocketPermission;
import java.security.UnresolvedPermission;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.PropertyPermission;
import java.util.Set;
import java.util.StringTokenizer;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.ExitException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectComponent;
import org.apache.tools.ant.util.SecurityManagerUtil;

/**
 * This class implements a security manager meant for usage by tasks that run inside the
 * Ant VM. An examples are the Java Task and JUnitTask.
 *
 * <p>
 * The basic functionality is that nothing (except for a base set of permissions) is allowed, unless
 * the permission is granted either explicitly or implicitly.
 * If a permission is granted this can be overruled by explicitly revoking the permission.
 *
 * It is not permissible to add permissions (either granted or revoked) while the Security Manager
 * is active (after calling setSecurityManager() but before calling restoreSecurityManager()).
 *
 * <p>
 * Note: This class isn't supported in Java 18 and higher where {@link SecurityManager} has been
 * deprecated for removal.
 *
 * @since Ant 1.6
 */
public class Permissions extends ProjectComponent {

    private final List<Permission> grantedPermissions = new LinkedList<>();
    private final List<Permission> revokedPermissions = new LinkedList<>();
    private java.security.Permissions granted = null;
    private SecurityManager origSm = null;
    private boolean active = false;
    private final boolean delegateToOldSM;

    // Mandatory constructor for permission object.
    private static final Class<?>[] PARAMS = {String.class, String.class};

    /**
     * Create a set of Permissions.  Equivalent to calling
     * <code>new Permissions(false)</code>.
     */
    public Permissions() {
        this(false);
    }

    /**
     * Create a set of permissions.
     * @param delegateToOldSM  if <code>true</code> the old security manager
     * will be used if the permission has not been explicitly granted or revoked
     * in this instance.
     */
    public Permissions(final boolean delegateToOldSM) {
        this.delegateToOldSM = delegateToOldSM;
    }

    /**
     * Adds a permission to be granted.
     * @param perm The Permissions.Permission to be granted.
     */
    public void addConfiguredGrant(final Permissions.Permission perm) {
        grantedPermissions.add(perm);
    }

    /**
     * Adds a permission to be revoked.
     * @param perm The Permissions.Permission to be revoked
     */
    public void addConfiguredRevoke(final Permissions.Permission perm) {
        revokedPermissions.add(perm);
    }

    /**
     * To be used by tasks wishing to use this security model before executing the part to be
     * subject to these Permissions. Note that setting the SecurityManager too early may
     * prevent your part from starting, as for instance changing classloaders may be prohibited.
     * The classloader for the new situation is supposed to be present.
     * <p>
     * This method is no longer supported in Java 18 and higher versions and throws a
     * {@link BuildException}. {@link org.apache.tools.ant.MagicNames#WARN_SECURITY_MANAGER_USAGE}
     * property can be set to {@code true} to log a warning message instead of throwing the exception.
     *
     * @throws BuildException on error
     */
    public synchronized void setSecurityManager() throws BuildException {
        if (!SecurityManagerUtil.isSetSecurityManagerAllowed()) {
            final String msg = "Use of <permissions> or " + Permissions.class.getName()
                    + " is disallowed in current Java runtime version";
            if (SecurityManagerUtil.warnOnSecurityManagerUsage(getProject())) {
                // just log a warning
                log("Security checks are disabled - " + msg, Project.MSG_WARN);
                return;
            } else {
                throw new BuildException(msg);
            }
        }
        origSm = System.getSecurityManager();
        init();
        System.setSecurityManager(new MySM());
        active = true;
    }

    /**
     * Initializes the list of granted permissions, checks the list of revoked permissions.
     */
    private void init() throws BuildException {
        granted = new java.security.Permissions();
        for (final Permissions.Permission p : revokedPermissions) {
            if (p.getClassName() == null) {
                throw new BuildException("Revoked permission " + p + " does not contain a class.");
            }
        }
        for (final Permissions.Permission p : grantedPermissions) {
            if (p.getClassName() == null) {
                throw new BuildException("Granted permission " + p
                        + " does not contain a class.");
            } else {
                final java.security.Permission perm = createPermission(p);
                granted.add(perm);
            }
        }
        // Add base set of permissions
        granted.add(new SocketPermission("localhost:1024-", "listen"));
        granted.add(new PropertyPermission("java.version", "read"));
        granted.add(new PropertyPermission("java.vendor", "read"));
        granted.add(new PropertyPermission("java.vendor.url", "read"));
        granted.add(new PropertyPermission("java.class.version", "read"));
        granted.add(new PropertyPermission("os.name", "read"));
        granted.add(new PropertyPermission("os.version", "read"));
        granted.add(new PropertyPermission("os.arch", "read"));
        granted.add(new PropertyPermission("file.encoding", "read"));
        granted.add(new PropertyPermission("file.separator", "read"));
        granted.add(new PropertyPermission("path.separator", "read"));
        granted.add(new PropertyPermission("line.separator", "read"));
        granted.add(new PropertyPermission("java.specification.version", "read"));
        granted.add(new PropertyPermission("java.specification.vendor", "read"));
        granted.add(new PropertyPermission("java.specification.name", "read"));
        granted.add(new PropertyPermission("java.vm.specification.version", "read"));
        granted.add(new PropertyPermission("java.vm.specification.vendor", "read"));
        granted.add(new PropertyPermission("java.vm.specification.name", "read"));
        granted.add(new PropertyPermission("java.vm.version", "read"));
        granted.add(new PropertyPermission("java.vm.vendor", "read"));
        granted.add(new PropertyPermission("java.vm.name", "read"));
    }

    private java.security.Permission createPermission(
            final Permissions.Permission permission) {
        try {
            // First add explicitly already resolved permissions will not be
            // resolved when added as unresolved permission.
            final Class<? extends java.security.Permission> clazz = Class.forName(
                    permission.getClassName()).asSubclass(java.security.Permission.class);
            final String name = permission.getName();
            final String actions = permission.getActions();
            final Constructor<? extends java.security.Permission> ctr = clazz.getConstructor(PARAMS);
            return ctr.newInstance(name, actions);
        } catch (final Exception e) {
            // Let the UnresolvedPermission handle it.
            return new UnresolvedPermission(permission.getClassName(),
                    permission.getName(), permission.getActions(), null);
        }
    }

    /**
     * To be used by tasks that just finished executing the parts subject to these permissions.
     * <p>
     * This method is no longer supported in Java 18 and higher versions and throws a
     * {@link BuildException}. {@link org.apache.tools.ant.MagicNames#WARN_SECURITY_MANAGER_USAGE}
     * property can be set to {@code true} to log a warning message instead of throwing the exception.
     */
    public synchronized void restoreSecurityManager() throws BuildException {
        if (!SecurityManagerUtil.isSetSecurityManagerAllowed()) {
            final String msg = "Use of <permissions> or " + Permissions.class.getName()
                    + " is disallowed in current Java runtime version";
            if (SecurityManagerUtil.warnOnSecurityManagerUsage(getProject())) {
                // just log a warning
                log("Security checks are disabled - " + msg, Project.MSG_WARN);
                return;
            } else {
                throw new BuildException(msg);
            }
        }
        active = false;
        System.setSecurityManager(origSm);
    }

    /**
     * This inner class implements the actual SecurityManager that can be used by tasks
     * supporting Permissions.
     */
    private class MySM extends SecurityManager {

        /**
         * Exit is treated in a special way in order to be able to return the exit code
         * towards tasks.
         * An ExitException is thrown instead of a simple SecurityException to indicate the exit
         * code.
         * Overridden from java.lang.SecurityManager
         * @param status The exit status requested.
         */
        @Override
        public void checkExit(final int status) {
            final java.security.Permission perm = new RuntimePermission("exitVM", null);
            try {
                checkPermission(perm);
            } catch (final SecurityException e) {
                throw new ExitException(e.getMessage(), status);
            }
        }

        /**
         * The central point in checking permissions.
         * Overridden from java.lang.SecurityManager
         *
         * @param perm The permission requested.
         */
        @Override
        public void checkPermission(final java.security.Permission perm) {
            if (active) {
                if (delegateToOldSM && !perm.getName().equals("exitVM")) {
                    boolean permOK = granted.implies(perm);
                    checkRevoked(perm);
                    /*
                     if the permission was not explicitly granted or revoked
                     the original security manager will do its work
                    */
                    if (!permOK && origSm != null) {
                        origSm.checkPermission(perm);
                    }
                }  else {
                    if (!granted.implies(perm)) {
                        throw new SecurityException("Permission " + perm + " was not granted.");
                    }
                    checkRevoked(perm);
                }
            }
        }

        /**
         * throws an exception if this permission is revoked
         * @param perm the permission being checked
         */
        private void checkRevoked(final java.security.Permission perm) {
            for (final Permissions.Permission revoked : revokedPermissions) {
                if (revoked.matches(perm)) {
                    throw new SecurityException("Permission " + perm + " was revoked.");
                }
            }
        }
    }

    /** Represents a permission. */
    public static class Permission {
        private String className;
        private String name;
        private String actionString;
        private Set<String> actions;

        /**
         * Set the class, mandatory.
         * @param aClass The class name of the permission.
         */
        public void setClass(final String aClass) {
            className = aClass.trim();
        }

        /**
         * Get the class of the permission.
         * @return The class name of the permission.
         */
        public String getClassName() {
            return className;
        }

        /**
         * Set the name of the permission.
         * @param aName The name of the permission.
         */
        public void setName(final String aName) {
            name = aName.trim();
        }

        /**
         * Get the name of the permission.
         * @return The name of the permission.
         */
        public String getName() {
            return name;
        }

        /**
         * Set the actions.
         * @param actions The actions of the permission.
         */
        public void setActions(final String actions) {
            actionString = actions;
            if (!actions.isEmpty()) {
                this.actions = parseActions(actions);
            }
        }

        /**
         * Get the actions.
         * @return The actions of the permission.
         */
        public String getActions() {
            return actionString;
        }

        /**
         * Learn whether the permission matches in case of a revoked permission.
         * @param perm The permission to check against.
         */
        boolean matches(final java.security.Permission perm) {
            if (!className.equals(perm.getClass().getName())) { //NOSONAR
                return false;
            }
            if (name != null) {
                if (name.endsWith("*")) {
                    if (!perm.getName().startsWith(name.substring(0, name.length() - 1))) {
                        return false;
                    }
                } else if (!name.equals(perm.getName())) {
                    return false;
                }
            }
            if (actions != null) {
                final Set<String> as = parseActions(perm.getActions());
                final int size = as.size();
                as.removeAll(actions);
                // If no actions removed, then all allowed
                return as.size() != size;
            }
            return true;
        }

        /**
         * Parses the actions into a set of separate strings.
         * @param actions The actions to be parsed.
         */
        private Set<String> parseActions(final String actions) {
            final Set<String> result = new HashSet<>();
            final StringTokenizer tk = new StringTokenizer(actions, ",");
            while (tk.hasMoreTokens()) {
                final String item = tk.nextToken().trim();
                if (!item.isEmpty()) {
                    result.add(item);
                }
            }
            return result;
        }

        /**
         * Get a string description of the permissions.
         * @return string description of the permissions.
         */
        @Override
        public String toString() {
            return ("Permission: " + className + " (\"" + name + "\", \"" + actions + "\")");
        }
    }
}
