/*_############################################################################
  _##
  _##  SNMP4J-UNIX-TRANSPORT - UnixDomainAgentProfilePolicy.java
  _##
  _##  Copyright (C) 2021-2026  Frank Fock (SNMP4J.org)
  _##
  _##  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.snmp4j.transport.unix.audit;

import org.snmp4j.agent.audit.AgentAuditLevel;
import org.snmp4j.agent.audit.AgentPolicyProfile;
import org.snmp4j.agent.audit.DefaultAgentAuditPolicy;
import org.snmp4j.agent.audit.FileAuditEvent;
import org.snmp4j.log.LogAdapter;
import org.snmp4j.log.LogFactory;
import org.snmp4j.smi.Address;
import org.snmp4j.transport.TransportAuditEvent;
import org.snmp4j.transport.unix.UnixDomainAddress;

import java.io.OutputStream;
import java.io.Serial;
import java.nio.file.Path;

/**
 * The {@code UnixDomainAgentProfilePolicy} extends the {@link DefaultAgentAuditPolicy} by support for the
 * {@code "unix"} address domain of {@link UnixDomainAddress}.
 * <p>
 * A Unix domain socket operation is always local, so the {@link AgentPolicyProfile.NetworkAccess}
 * restrictions of the policy profile - which are based on the comparison of {@link java.net.InetAddress}
 * instances and therefore not applicable to a socket file path - are not evaluated for such an operation.
 * The file system operations that are necessary for a Unix domain socket operation are checked instead:
 * the socket's (local) file path is matched against the allowed files and directories of the
 * {@link AgentPolicyProfile} exactly like any other file access audited through
 * {@link #fileAudit(FileAuditEvent)}. The audited transport operation is mapped to the file operation it
 * implies:
 * <ul>
 *     <li>{@link TransportAuditEvent.TransportOperation#LISTEN}:
 *     {@link FileAuditEvent.FileOperation#CREATE} of the local socket file, because binding a Unix domain
 *     server socket creates that file.</li>
 *     <li>{@link TransportAuditEvent.TransportOperation#CLOSE}:
 *     {@link FileAuditEvent.FileOperation#DELETE} of the local socket file, because closing the transport
 *     removes that file.</li>
 *     <li>{@link TransportAuditEvent.TransportOperation#SEND}:
 *     {@link FileAuditEvent.FileOperation#WRITE} of the peer's socket file.</li>
 *     <li>{@link TransportAuditEvent.TransportOperation#RECEIVE}:
 *     {@link FileAuditEvent.FileOperation#READ} of the peer's socket file.</li>
 * </ul>
 * For {@code SEND} and {@code RECEIVE} the local socket file is checked instead of the peer's file if the
 * peer socket is unnamed (auto-bound), which is the regular case for an accepted client connection.
 * Transport operations of any other address domain are audited by {@link DefaultAgentAuditPolicy}
 * unchanged, thus this policy can be used for an agent that serves Unix domain sockets and IP transports
 * at the same time.
 * <p>
 * Like its superclass, this policy denies a violating operation only on audit level
 * {@link AgentAuditLevel#RESTRICT}.
 *
 * @author Frank Fock
 * @version 1.2.2
 * @since 1.2.2
 */
public class UnixDomainAgentProfilePolicy extends DefaultAgentAuditPolicy {

    @Serial
    private static final long serialVersionUID = 6045131093294338553L;

    private static final LogAdapter logger = LogFactory.getLogger(UnixDomainAgentProfilePolicy.class);

    /**
     * Creates a {@code UnixDomainAgentProfilePolicy} that logs audit records through the
     * {@link LogFactory} logging only.
     *
     * @param agentPolicyProfile
     *    the policy profile that defines the access restrictions to enforce, or {@code null} to permit
     *    all operations (audit only).
     * @param auditLevel
     *    the audit level to apply, or {@code null} which is interpreted as {@link AgentAuditLevel#OFF}.
     */
    public UnixDomainAgentProfilePolicy(AgentPolicyProfile agentPolicyProfile, AgentAuditLevel auditLevel) {
        super(agentPolicyProfile, auditLevel);
    }

    /**
     * Creates a {@code UnixDomainAgentProfilePolicy} that logs audit records through the
     * {@link LogFactory} logging and, in addition, writes a copy of every audit record to the supplied
     * {@link OutputStream}.
     *
     * @param agentPolicyProfile
     *    the policy profile that defines the access restrictions to enforce, or {@code null} to permit
     *    all operations (audit only).
     * @param auditLevel
     *    the audit level to apply, or {@code null} which is interpreted as {@link AgentAuditLevel#OFF}.
     * @param auditOutputStream
     *    an optional output stream that receives a copy of every audit record (for example a
     *    {@code FileOutputStream}, {@code System.out} or {@code System.err}), or {@code null} for
     *    logging through {@link LogFactory} only.
     */
    public UnixDomainAgentProfilePolicy(AgentPolicyProfile agentPolicyProfile, AgentAuditLevel auditLevel,
                                        OutputStream auditOutputStream) {
        super(agentPolicyProfile, auditLevel, auditOutputStream);
    }

    /**
     * Evaluates whether the supplied transport operation is permitted by the {@link AgentPolicyProfile}.
     * A Unix domain socket operation is evaluated by checking the socket file it operates on against the
     * allowed files and directories of the policy profile (see {@link #isSocketFileAccessAllowed}), any
     * other operation is evaluated by {@link DefaultAgentAuditPolicy}.
     *
     * @param event
     *    the transport audit event to evaluate.
     * @return
     *    {@code true} if the operation is permitted by the policy profile, {@code false} otherwise.
     */
    @Override
    protected boolean isTransportOperationAllowed(TransportAuditEvent event) {
        UnixDomainAddress socketAddress = getAuditedSocketAddress(event);
        if (socketAddress == null) {
            // Not a Unix domain socket operation: audit it by the address domain independent default policy.
            return super.isTransportOperationAllowed(event);
        }
        return isSocketFileAccessAllowed(event, socketAddress);
    }

    /**
     * Checks whether the file access that the audited Unix domain socket operation needs on the socket
     * file of the supplied address is permitted by the {@link AgentPolicyProfile}. The check is delegated
     * to {@link #isFileOperationAllowed(FileAuditEvent)} with the file operation returned by
     * {@link #getImpliedFileOperation(TransportAuditEvent.TransportOperation)}, so the same allowed files
     * and directories apply as for any other file access of the agent.
     *
     * @param event
     *    the audited transport event.
     * @param socketAddress
     *    the Unix domain address whose socket file is accessed by the audited operation.
     * @return
     *    {@code true} if the implied file operation is permitted by the policy profile, {@code false}
     *    otherwise.
     */
    protected boolean isSocketFileAccessAllowed(TransportAuditEvent event, UnixDomainAddress socketAddress) {
        AgentPolicyProfile profile = getAgentPolicyProfile();
        if (profile == null) {
            return true;
        }
        Path socketPath = socketAddress.getPath();
        if ((socketPath == null) || socketPath.toString().isEmpty()) {
            // An unnamed (auto-bound) socket has no representation in the file system, thus there is no
            // path that could be matched against the allowed files and directories. Such an operation is
            // permitted unless the profile forbids any file access at all.
            if (logger.isDebugEnabled()) {
                logger.debug("Auditing Unix domain socket operation without socket file: " + event);
            }
            return profile.getFileAccess() != AgentPolicyProfile.FileAccess.NO_ACCESS;
        }
        FileAuditEvent.FileOperation fileOperation = getImpliedFileOperation(event.getOperation());
        if (logger.isDebugEnabled()) {
            logger.debug("Auditing Unix domain socket operation " + event.getOperation() + " as " +
                    fileOperation + " access on socket file '" + socketPath + "'");
        }
        return isFileOperationAllowed(new FileAuditEvent(event.getSource(), fileOperation, socketPath.toFile()));
    }

    /**
     * Determines the Unix domain address whose socket file is accessed by the audited transport operation.
     *
     * @param event
     *    the audited transport event.
     * @return
     *    the local {@link UnixDomainAddress} for {@link TransportAuditEvent.TransportOperation#LISTEN} and
     *    {@link TransportAuditEvent.TransportOperation#CLOSE}, the peer address for
     *    {@link TransportAuditEvent.TransportOperation#SEND} and
     *    {@link TransportAuditEvent.TransportOperation#RECEIVE} unless its socket is unnamed (then the
     *    local address is returned), or {@code null} if neither the peer nor the local address of the
     *    event is a {@code UnixDomainAddress} (i.e. the event does not refer to a Unix domain socket).
     */
    protected UnixDomainAddress getAuditedSocketAddress(TransportAuditEvent event) {
        UnixDomainAddress peerAddress = toUnixDomainAddress(event.getPeerAddress());
        UnixDomainAddress localAddress = toUnixDomainAddress(event.getLocalAddress());
        if ((peerAddress == null) && (localAddress == null)) {
            return null;
        }
        return switch (event.getOperation()) {
            case LISTEN, CLOSE ->
                // Binding and closing a Unix domain server socket creates and removes the local socket file.
                    (localAddress == null) ? peerAddress : localAddress;
            default -> {
                // Sending to and receiving from a peer accesses the peer's socket file - except for an
                // unnamed peer socket (i.e. an accepted client connection), where the local socket file
                // of this transport is the file that is being accessed.
                if (hasSocketFile(peerAddress)) {
                    yield peerAddress;
                }
                yield (localAddress == null) ? peerAddress : localAddress;
            }
        };
    }

    /**
     * Maps an audited transport operation to the file operation it implies on the socket file of a Unix
     * domain socket.
     *
     * @param transportOperation
     *    the audited transport operation.
     * @return
     *    {@link FileAuditEvent.FileOperation#CREATE} for {@code LISTEN} (binding creates the socket file),
     *    {@link FileAuditEvent.FileOperation#DELETE} for {@code CLOSE} (closing removes the socket file),
     *    {@link FileAuditEvent.FileOperation#WRITE} for {@code SEND}, and
     *    {@link FileAuditEvent.FileOperation#READ} for {@code RECEIVE}.
     */
    protected FileAuditEvent.FileOperation getImpliedFileOperation(
            TransportAuditEvent.TransportOperation transportOperation) {
        if (transportOperation == null) {
            return FileAuditEvent.FileOperation.READ;
        }
        return switch (transportOperation) {
            case LISTEN -> FileAuditEvent.FileOperation.CREATE;
            case CLOSE -> FileAuditEvent.FileOperation.DELETE;
            case SEND -> FileAuditEvent.FileOperation.WRITE;
            default -> FileAuditEvent.FileOperation.READ;
        };
    }

    /**
     * Checks whether the supplied Unix domain address refers to a named socket, i.e. a socket that has a
     * path in the file system.
     *
     * @param address
     *    a Unix domain address or {@code null}.
     * @return
     *    {@code true} if {@code address} has a non-empty socket file path, {@code false} otherwise.
     */
    protected static boolean hasSocketFile(UnixDomainAddress address) {
        if (address == null) {
            return false;
        }
        Path path = address.getPath();
        return (path != null) && (!path.toString().isEmpty());
    }

    private static UnixDomainAddress toUnixDomainAddress(Address address) {
        return (address instanceof UnixDomainAddress) ? (UnixDomainAddress) address : null;
    }
}
