org.dcache.nfs.vfs.PseudoFs.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.nfs.vfs.PseudoFs.java

Source

/*
 * Copyright (c) 2009 - 2015 Deutsches Elektronen-Synchroton,
 * Member of the Helmholtz Association, (DESY), HAMBURG, GERMANY
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program (see the file COPYING.LIB for more
 * details); if not, write to the Free Software Foundation, Inc.,
 * 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.dcache.nfs.vfs;

import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.security.auth.Subject;
import org.dcache.auth.Subjects;
import org.dcache.nfs.ChimeraNFSException;
import org.dcache.nfs.ExportFile;
import org.dcache.nfs.FsExport;
import org.dcache.nfs.nfsstat;
import org.dcache.nfs.status.*;
import org.dcache.nfs.v4.acl.Acls;
import org.dcache.nfs.v4.xdr.acemask4;
import org.dcache.xdr.RpcCall;

import static org.dcache.nfs.v4.xdr.nfs4_prot.*;

import org.dcache.nfs.v4.xdr.nfsace4;
import org.dcache.utils.SubjectHolder;
import org.dcache.xdr.RpcAuth;
import org.dcache.xdr.RpcAuthType;
import org.dcache.xdr.gss.RpcAuthGss;
import org.dcache.xdr.gss.RpcGssService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.dcache.nfs.vfs.AclCheckable.Access;

/**
 * A decorated {@code VirtualFileSystem} that builds a Pseudo file system
 * on top of an other file system based on export rules.
 *
 * In addition, PseudoFS takes the responsibility of permission and access checking.
 */
public class PseudoFs extends ForwardingFileSystem {

    private final static Logger _log = LoggerFactory.getLogger(PseudoFs.class);
    private final Subject _subject;
    private final InetAddress _inetAddress;
    private final VirtualFileSystem _inner;
    private final ExportFile _exportFile;
    private final RpcAuth _auth;

    private final static int ACCESS4_MASK = ACCESS4_DELETE | ACCESS4_EXECUTE | ACCESS4_EXTEND | ACCESS4_LOOKUP
            | ACCESS4_MODIFY | ACCESS4_READ;

    public PseudoFs(VirtualFileSystem inner, RpcCall call, ExportFile exportFile) {
        _inner = inner;
        _subject = call.getCredential().getSubject();
        _auth = call.getCredential();
        _inetAddress = call.getTransport().getRemoteSocketAddress().getAddress();
        _exportFile = exportFile;
    }

    @Override
    protected VirtualFileSystem delegate() {
        return _inner;
    }

    private boolean canAccess(Inode inode, int mode) {
        try {
            checkAccess(inode, mode, false);
            return true;
        } catch (IOException e) {
        }
        return false;
    }

    @Override
    public int access(Inode inode, int mode) throws IOException {
        int accessmask = 0;

        if ((mode & ~ACCESS4_MASK) != 0) {
            throw new InvalException("invalid access mask");
        }

        if ((mode & ACCESS4_READ) != 0) {
            if (canAccess(inode, ACE4_READ_DATA)) {
                accessmask |= ACCESS4_READ;
            }
        }

        if ((mode & ACCESS4_LOOKUP) != 0) {
            if (canAccess(inode, ACE4_EXECUTE)) {
                accessmask |= ACCESS4_LOOKUP;
            }
        }

        if ((mode & ACCESS4_MODIFY) != 0) {
            if (canAccess(inode, ACE4_WRITE_DATA)) {
                accessmask |= ACCESS4_MODIFY;
            }
        }

        if ((mode & ACCESS4_EXECUTE) != 0) {
            if (canAccess(inode, ACE4_EXECUTE)) {
                accessmask |= ACCESS4_EXECUTE;
            }
        }

        if ((mode & ACCESS4_EXTEND) != 0) {
            if (canAccess(inode, ACE4_APPEND_DATA)) {
                accessmask |= ACCESS4_EXTEND;
            }
        }

        if ((mode & ACCESS4_DELETE) != 0) {
            if (canAccess(inode, ACE4_DELETE_CHILD)) {
                accessmask |= ACCESS4_DELETE;
            }
        }

        return accessmask & _inner.access(inode, accessmask);
    }

    @Override
    public Inode create(Inode parent, Stat.Type type, String path, Subject subject, int mode) throws IOException {
        Subject effectiveSubject = checkAccess(parent, ACE4_ADD_FILE);

        if (inheritUidGid(parent)) {
            Stat s = _inner.getattr(parent);
            effectiveSubject = Subjects.of(s.getUid(), s.getGid());
        }

        return pushExportIndex(parent, _inner.create(parent, type, path, effectiveSubject, mode));
    }

    @Override
    public Inode getRootInode() throws IOException {
        /*
         * reject if there are no exports for this client at all
         */
        if (!_exportFile.exportsFor(_inetAddress).findAny().isPresent()) {
            _log.warn("Access denied: (no export) fs root for client {}", _inetAddress);
            throw new AccessException("no exports");
        }

        Inode inode = _inner.getRootInode();
        FsExport export = _exportFile.getExport("/", _inetAddress);
        return export == null ? realToPseudo(inode) : pushExportIndex(inode, export.getIndex());
    }

    @Override
    public Inode lookup(Inode parent, String path) throws IOException {
        checkAccess(parent, ACE4_EXECUTE);

        if (parent.isPesudoInode()) {
            return lookupInPseudoDirectory(parent, path);
        }

        /*
         * REVISIT: this is not the best place to do it, but the simples one.
         */
        FsExport export = _exportFile.getExport(parent.exportIndex(), _inetAddress);
        if (!export.isWithDcap() && ".(get)(cursor)".equals(path)) {
            throw new NoEntException("the dcap magic file is blocked");
        }

        return pushExportIndex(parent, _inner.lookup(parent, path));
    }

    @Override
    public Inode link(Inode parent, Inode link, String path, Subject subject) throws IOException {
        Subject effectiveSubject = checkAccess(parent, ACE4_ADD_FILE);
        if (inheritUidGid(parent)) {
            Stat s = _inner.getattr(parent);
            effectiveSubject = Subjects.of(s.getUid(), s.getGid());
        }
        return pushExportIndex(parent, _inner.link(parent, link, path, effectiveSubject));
    }

    @Override
    public List<DirectoryEntry> list(Inode inode) throws IOException {
        Subject effectiveSubject = checkAccess(inode, ACE4_LIST_DIRECTORY);
        if (inode.isPesudoInode()) {
            return listPseudoDirectory(inode);
        }
        return Lists.transform(_inner.list(inode), new PushParentIndex(inode));
    }

    @Override
    public Inode mkdir(Inode parent, String path, Subject subject, int mode) throws IOException {
        Subject effectiveSubject = checkAccess(parent, ACE4_ADD_SUBDIRECTORY);
        if (inheritUidGid(parent)) {
            Stat s = _inner.getattr(parent);
            effectiveSubject = Subjects.of(s.getUid(), s.getGid());
        }
        return pushExportIndex(parent, _inner.mkdir(parent, path, effectiveSubject, mode));
    }

    @Override
    public boolean move(Inode src, String oldName, Inode dest, String newName) throws IOException {
        checkAccess(src, ACE4_DELETE_CHILD);
        checkAccess(dest, ACE4_ADD_FILE | ACE4_DELETE_CHILD);
        return _inner.move(src, oldName, dest, newName);
    }

    @Override
    public Inode parentOf(Inode inode) throws IOException {

        Inode parent = _inner.parentOf(inode);
        Inode asPseudo = realToPseudo(parent);
        if (isPseudoDirectory(asPseudo)) {
            /*
             * if parent is a path of export tree
             */
            return asPseudo;
        } else {
            return pushExportIndex(inode, parent);
        }
    }

    @Override
    public int read(Inode inode, byte[] data, long offset, int count) throws IOException {
        checkAccess(inode, ACE4_READ_DATA);
        return _inner.read(inode, data, offset, count);
    }

    @Override
    public String readlink(Inode inode) throws IOException {
        checkAccess(inode, ACE4_READ_DATA);
        return _inner.readlink(inode);
    }

    @Override
    public void remove(Inode parent, String path) throws IOException {
        try {
            checkAccess(parent, ACE4_DELETE_CHILD);
        } catch (ChimeraNFSException e) {
            if (e.getStatus() == nfsstat.NFSERR_ACCESS) {
                Inode inode = pushExportIndex(parent, _inner.lookup(parent, path));
                checkAccess(inode, ACE4_DELETE);
            } else {
                throw e;
            }
        }
        _inner.remove(parent, path);
    }

    @Override
    public Inode symlink(Inode parent, String path, String link, Subject subject, int mode) throws IOException {
        Subject effectiveSubject = checkAccess(parent, ACE4_ADD_FILE);
        if (inheritUidGid(parent)) {
            Stat s = _inner.getattr(parent);
            effectiveSubject = Subjects.of(s.getUid(), s.getGid());
        }
        return pushExportIndex(parent, _inner.symlink(parent, path, link, effectiveSubject, mode));
    }

    @Override
    public WriteResult write(Inode inode, byte[] data, long offset, int count, StabilityLevel stabilityLevel)
            throws IOException {
        checkAccess(inode, ACE4_WRITE_DATA);
        return _inner.write(inode, data, offset, count, stabilityLevel);
    }

    @Override
    public Stat getattr(Inode inode) throws IOException {
        checkAccess(inode, ACE4_READ_ATTRIBUTES);
        return _inner.getattr(inode);
    }

    @Override
    public void setattr(Inode inode, Stat stat) throws IOException {
        checkAccess(inode, ACE4_WRITE_ATTRIBUTES);
        _inner.setattr(inode, stat);
    }

    @Override
    public nfsace4[] getAcl(Inode inode) throws IOException {
        checkAccess(inode, ACE4_READ_ACL);
        return _inner.getAcl(inode);
    }

    @Override
    public void setAcl(Inode inode, nfsace4[] acl) throws IOException {
        checkAccess(inode, ACE4_WRITE_ACL);
        _inner.setAcl(inode, acl);
    }

    private Subject checkAccess(Inode inode, int requestedMask) throws IOException {
        return checkAccess(inode, requestedMask, true);
    }

    private Subject checkAccess(Inode inode, int requestedMask, boolean shouldLog) throws IOException {

        Subject effectiveSubject = _subject;
        Access aclMatched = Access.UNDEFINED;

        if (inode.isPesudoInode() && Acls.wantModify(requestedMask)) {
            if (shouldLog) {
                _log.warn("Access denied: pseudo Inode {} {} {} {}", inode, _inetAddress,
                        acemask4.toString(requestedMask), new SubjectHolder(effectiveSubject));
            }
            throw new RoFsException("attempt to modify pseudofs");
        }

        if (!inode.isPesudoInode()) {
            int exportIdx = getExportIndex(inode);
            FsExport export = _exportFile.getExport(exportIdx, _inetAddress);
            if (exportIdx != 0 && export == null) {
                if (shouldLog) {
                    _log.warn("Access denied: (no export) to inode {} for client {}", inode, _inetAddress);
                }
                throw new AccessException("permission deny");
            }

            checkSecurityFlavor(_auth, export.getSec());

            if ((export.ioMode() == FsExport.IO.RO) && Acls.wantModify(requestedMask)) {
                if (shouldLog) {
                    _log.warn("Access denied: (RO export) inode {} for client {}", inode, _inetAddress);
                }
                throw new AccessException("read-only export");
            }

            if (export.isAllRoot()) {
                _log.info("permission check to inode {} skipped due to all_root option for client {}", inode,
                        _inetAddress);
                return effectiveSubject;
            }

            if (export.hasAllSquash() || (!export.isTrusted() && Subjects.isRoot(_subject))) {
                effectiveSubject = Subjects.of(export.getAnonUid(), export.getAnonGid());
            }

            if (export.checkAcls()) {
                aclMatched = _inner.getAclCheckable().checkAcl(_subject, inode, requestedMask);
                if (aclMatched == Access.DENY) {
                    if (shouldLog) {
                        _log.warn("Access deny: {} {} {}", _inetAddress, acemask4.toString(requestedMask),
                                new SubjectHolder(_subject));
                    }
                    throw new AccessException();
                }
            }
        }

        /*
         * check for unix permission if ACL did not give us an answer.
         * Skip the check, if we ask for ACE4_READ_ATTRIBUTES as unix
         * always allows it.
         */
        if ((aclMatched == Access.UNDEFINED) && (requestedMask != ACE4_READ_ATTRIBUTES)) {
            Stat stat = _inner.getattr(inode);
            int unixAccessmask = unixToAccessmask(effectiveSubject, stat);
            if ((unixAccessmask & requestedMask) != requestedMask) {
                if (shouldLog) {
                    _log.warn("Access denied: {} {} {} {} {}", inode, _inetAddress,
                            acemask4.toString(requestedMask), acemask4.toString(unixAccessmask),
                            new SubjectHolder(_subject));
                }
                throw new AccessException("permission deny");
            }
        }
        return effectiveSubject;
    }

    /*
     * unix permission bits offset as defined in POSIX
     * for st_mode filed of the stat  structure.
     */
    private static final int BIT_MASK_OWNER_OFFSET = 6;
    private static final int BIT_MASK_GROUP_OFFSET = 3;
    private static final int BIT_MASK_OTHER_OFFSET = 0;

    @SuppressWarnings("PointlessBitwiseExpression")
    private int unixToAccessmask(Subject subject, Stat stat) {
        int mode = stat.getMode();
        boolean isDir = (mode & Stat.S_IFDIR) == Stat.S_IFDIR;
        int fromUnixMask;

        if (Subjects.isRoot(subject)) {
            fromUnixMask = Acls.toAccessMask(Acls.RBIT | Acls.WBIT | Acls.XBIT, isDir, true);
        } else if (Subjects.hasUid(subject, stat.getUid())) {
            fromUnixMask = Acls.toAccessMask(mode >> BIT_MASK_OWNER_OFFSET, isDir, true);
        } else if (Subjects.hasGid(subject, stat.getGid())) {
            fromUnixMask = Acls.toAccessMask(mode >> BIT_MASK_GROUP_OFFSET, isDir, false);
        } else {
            fromUnixMask = Acls.toAccessMask(mode >> BIT_MASK_OTHER_OFFSET, isDir, false);
        }
        return fromUnixMask;
    }

    private Inode lookupInPseudoDirectory(Inode parent, String name) throws IOException {
        Set<PseudoFsNode> nodes = prepareExportTree();

        for (PseudoFsNode node : nodes) {
            if (node.id().equals(parent)) {
                PseudoFsNode n = node.getChild(name);
                if (n != null) {
                    return n.isMountPoint() ? pseudoIdToReal(n.id(), getIndexId(n)) : n.id();
                }
            }
        }
        throw new NoEntException();
    }

    private boolean isPseudoDirectory(Inode dir) throws IOException {
        return prepareExportTree().stream().anyMatch(n -> n.id().equals(dir));
    }

    public static Inode pseudoIdToReal(Inode inode, int index) {

        FileHandle fh = new FileHandle.FileHandleBuilder().setExportIdx(index).setType(0).build(inode.getFileId());
        return new Inode(fh);
    }

    private int getIndexId(PseudoFsNode node) {
        List<FsExport> exports = node.getExports();
        return exports.get(0).getIndex();
    }

    private class ConvertToRealInode implements Function<DirectoryEntry, DirectoryEntry> {

        private final PseudoFsNode _node;

        ConvertToRealInode(PseudoFsNode node) {
            _node = node;
        }

        @Override
        public DirectoryEntry apply(DirectoryEntry input) {
            return new DirectoryEntry(input.getName(), pseudoIdToReal(input.getInode(), getIndexId(_node)),
                    input.getStat());
        }
    }

    private class PushParentIndex implements Function<DirectoryEntry, DirectoryEntry> {

        private final Inode _inode;

        PushParentIndex(Inode parent) {
            _inode = parent;
        }

        @Override
        public DirectoryEntry apply(DirectoryEntry input) {
            return new DirectoryEntry(input.getName(), pushExportIndex(_inode, input.getInode()), input.getStat());
        }
    }

    private List<DirectoryEntry> listPseudoDirectory(Inode parent) throws ChimeraNFSException, IOException {
        Set<PseudoFsNode> nodes = prepareExportTree();
        for (PseudoFsNode node : nodes) {
            if (node.id().equals(parent)) {
                if (node.isMountPoint()) {
                    return Lists.transform(_inner.list(parent), new ConvertToRealInode(node));
                } else {
                    List<DirectoryEntry> pseudoLs = new ArrayList<>();
                    for (String s : node.getChildren()) {
                        PseudoFsNode subNode = node.getChild(s);
                        Inode inode = subNode.id();
                        Stat stat = _inner.getattr(inode);
                        DirectoryEntry e = new DirectoryEntry(s,
                                subNode.isMountPoint() ? pseudoIdToReal(inode, getIndexId(subNode)) : inode, stat);
                        pseudoLs.add(e);
                    }
                    return pseudoLs;
                }
            }
        }
        throw new NoEntException();
    }

    private Inode pushExportIndex(Inode inode, int index) {

        FileHandle fh = new FileHandle.FileHandleBuilder().setExportIdx(index).setType(0).build(inode.getFileId());
        return new Inode(fh);
    }

    private Inode pushExportIndex(Inode parent, Inode inode) {
        return pushExportIndex(inode, getExportIndex(parent));
    }

    private int getExportIndex(Inode inode) {
        /*
         * NOTE, we take first export entry allowed for this client.
         * This can be wrong, e.g. RO vs. RW.
         */
        if (inode.handleVersion() == 0) {
            FsExport export = _exportFile.exportsFor(_inetAddress).findFirst().orElse(null);
            return export == null ? -1 : export.getIndex();
        }
        return inode.exportIndex();
    }

    private Inode realToPseudo(Inode inode) {
        return realToPseudo(inode, 0);
    }

    private Inode realToPseudo(Inode inode, int idx) {

        FileHandle fh = new FileHandle.FileHandleBuilder().setExportIdx(idx).setType(1).build(inode.getFileId());
        return new Inode(fh);
    }

    private void pathToPseudoFs(final PseudoFsNode root, Set<PseudoFsNode> all, FsExport e) {

        PseudoFsNode parent = root;
        String path = e.getPath();

        if (e.getPath().equals("/")) {
            root.addExport(e);
            return;
        }

        Splitter splitter = Splitter.on('/').omitEmptyStrings();
        Set<PseudoFsNode> pathNodes = new HashSet<>();

        for (String s : splitter.split(path)) {
            try {
                PseudoFsNode node = parent.getChild(s);
                if (node == null) {
                    node = new PseudoFsNode(realToPseudo(_inner.lookup(parent.id(), s)));
                    parent.addChild(s, node);
                    pathNodes.add(node);
                }
                parent = node;
            } catch (IOException ef) {
                return;
            }
        }

        all.addAll(pathNodes);
        parent.setId(pseudoIdToReal(parent.id(), e.getIndex()));
        parent.addExport(e);
    }

    private Set<PseudoFsNode> prepareExportTree() throws ChimeraNFSException, IOException {

        Set<PseudoFsNode> nodes = new HashSet<>();
        Inode rootInode = realToPseudo(_inner.getRootInode());
        PseudoFsNode root = new PseudoFsNode(rootInode);

        _exportFile.exportsFor(_inetAddress).forEach(e -> pathToPseudoFs(root, nodes, e));

        if (nodes.isEmpty()) {
            _log.warn("No exports found for: {}", _inetAddress);
            throw new AccessException();
        }

        nodes.add(root);
        return nodes;
    }

    private static void checkSecurityFlavor(RpcAuth auth, FsExport.Sec minFlavor) throws ChimeraNFSException {

        FsExport.Sec usedFlavor;
        switch (auth.type()) {
        case RpcAuthType.NONE:
            usedFlavor = FsExport.Sec.NONE;
            break;
        case RpcAuthType.UNIX:
            usedFlavor = FsExport.Sec.SYS;
            break;
        case RpcAuthType.RPCGSS_SEC:
            RpcAuthGss authGss = (RpcAuthGss) auth;
            switch (authGss.getService()) {
            case RpcGssService.RPC_GSS_SVC_NONE:
                usedFlavor = FsExport.Sec.KRB5;
                break;
            case RpcGssService.RPC_GSS_SVC_INTEGRITY:
                usedFlavor = FsExport.Sec.KRB5I;
                break;
            case RpcGssService.RPC_GSS_SVC_PRIVACY:
                usedFlavor = FsExport.Sec.KRB5P;
                break;
            default:
                throw new PermException("Unsupported Authentication GSS service: " + authGss.getService());
            }
            break;
        default:
            throw new PermException("Unsupported Authentication flavor: " + auth.type());
        }

        if (usedFlavor.compareTo(minFlavor) < 0) {
            throw new PermException("Authentication flavor too weak: " + "allowed <" + minFlavor + "> provided <"
                    + usedFlavor + ">");
        }
    }

    private boolean inheritUidGid(Inode inode) {
        return _exportFile.getExport(inode.exportIndex(), _inetAddress).isAllRoot();
    }
}