/*
* 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.chimera.nfsv41.door;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.primitives.Longs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.Subject;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.dcache.acl.ACE;
import org.dcache.acl.enums.AceFlags;
import org.dcache.acl.enums.AceType;
import org.dcache.acl.enums.Who;
import org.dcache.auth.Subjects;
import org.dcache.chimera.ChimeraFsException;
import org.dcache.chimera.DirNotEmptyHimeraFsException;
import org.dcache.chimera.DirectoryStreamHelper;
import org.dcache.chimera.FileExistsChimeraFsException;
import org.dcache.chimera.FileNotFoundHimeraFsException;
import org.dcache.chimera.FsInode;
import org.dcache.chimera.FsInode_CONST;
import org.dcache.chimera.FsInode_ID;
import org.dcache.chimera.FsInode_NAMEOF;
import org.dcache.chimera.FsInode_PARENT;
import org.dcache.chimera.FsInode_PATHOF;
import org.dcache.chimera.FsInode_PCRC;
import org.dcache.chimera.FsInode_PCUR;
import org.dcache.chimera.FsInode_PLOC;
import org.dcache.chimera.FsInode_PSET;
import org.dcache.chimera.FsInode_TAG;
import org.dcache.chimera.FsInode_TAGS;
import org.dcache.chimera.FsInodeType;
import org.dcache.chimera.HimeraDirectoryEntry;
import org.dcache.chimera.InvalidArgumentChimeraException;
import org.dcache.chimera.IsDirChimeraException;
import org.dcache.chimera.JdbcFs;
import org.dcache.chimera.NotDirChimeraException;
import org.dcache.chimera.PermissionDeniedChimeraFsException;
import org.dcache.chimera.StorageGenericLocation;
import org.dcache.nfs.status.BadHandleException;
import org.dcache.nfs.status.BadOwnerException;
import org.dcache.nfs.status.ExistException;
import org.dcache.nfs.status.InvalException;
import org.dcache.nfs.status.IsDirException;
import org.dcache.nfs.status.NfsIoException;
import org.dcache.nfs.status.NoEntException;
import org.dcache.nfs.status.NotDirException;
import org.dcache.nfs.status.NotEmptyException;
import org.dcache.nfs.status.PermException;
import org.dcache.nfs.status.StaleException;
import org.dcache.nfs.v4.NfsIdMapping;
import org.dcache.nfs.v4.acl.Acls;
import org.dcache.nfs.v4.xdr.aceflag4;
import org.dcache.nfs.v4.xdr.acemask4;
import org.dcache.nfs.v4.xdr.acetype4;
import org.dcache.nfs.v4.xdr.nfsace4;
import org.dcache.nfs.v4.xdr.uint32_t;
import org.dcache.nfs.v4.xdr.utf8str_mixed;
import org.dcache.nfs.vfs.AclCheckable;
import org.dcache.nfs.vfs.DirectoryEntry;
import org.dcache.nfs.vfs.FsStat;
import org.dcache.nfs.vfs.Inode;
import org.dcache.nfs.vfs.Stat;
import org.dcache.nfs.vfs.VirtualFileSystem;
import static org.dcache.chimera.FileSystemProvider.StatCacheOption.NO_STAT;
import static org.dcache.chimera.FileSystemProvider.StatCacheOption.STAT;
import static org.dcache.nfs.v4.xdr.nfs4_prot.*;
/**
* Interface to a virtual file system.
*/
public class ChimeraVfs implements VirtualFileSystem, AclCheckable {
private static final Logger _log = LoggerFactory.getLogger(ChimeraVfs.class);
private final JdbcFs _fs;
private final NfsIdMapping _idMapping;
/**
* minimal binary handle size which can be processed.
*/
private static final int MIN_HANDLE_LEN = 4;
public ChimeraVfs(JdbcFs fs, NfsIdMapping idMapping) {
_fs = fs;
_idMapping = idMapping;
}
@Override
public Inode getRootInode() throws IOException {
return toInode(FsInode.getRoot(_fs));
}
@Override
public Inode lookup(Inode parent, String path) throws IOException {
try {
FsInode parentFsInode = toFsInode(parent);
FsInode fsInode = parentFsInode.inodeOf(path, NO_STAT);
return toInode(fsInode);
}catch (FileNotFoundHimeraFsException e) {
throw new NoEntException("Path Do not exist.");
}
}
@Override
public Inode create(Inode parent, Stat.Type type, String path, Subject subject, int mode) throws IOException {
int uid = (int)Subjects.getUid(subject);
int gid = (int)Subjects.getPrimaryGid(subject);
try {
FsInode parentFsInode = toFsInode(parent);
FsInode fsInode = _fs.createFile(parentFsInode, path, uid, gid, mode | typeToChimera(type), typeToChimera(type));
return toInode(fsInode);
} catch (FileExistsChimeraFsException e) {
throw new ExistException("path already exists");
}
}
@Override
public Inode mkdir(Inode parent, String path, Subject subject, int mode) throws IOException {
int uid = (int) Subjects.getUid(subject);
int gid = (int) Subjects.getPrimaryGid(subject);
try {
FsInode parentFsInode = toFsInode(parent);
FsInode fsInode = parentFsInode.mkdir(path, uid, gid, mode);
return toInode(fsInode);
} catch (FileExistsChimeraFsException e) {
throw new ExistException("path already exists");
}
}
@Override
public Inode link(Inode parent, Inode link, String path, Subject subject) throws IOException {
FsInode parentFsInode = toFsInode(parent);
FsInode linkInode = toFsInode(link);
try {
FsInode fsInode = _fs.createHLink(parentFsInode, linkInode, path);
return toInode(fsInode);
}catch (NotDirChimeraException e) {
throw new NotDirException("parent not a directory");
} catch (FileExistsChimeraFsException e) {
throw new ExistException("path already exists");
}
}
@Override
public Inode symlink(Inode parent, String path, String link, Subject subject, int mode) throws IOException {
int uid = (int) Subjects.getUid(subject);
int gid = (int) Subjects.getPrimaryGid(subject);
try {
FsInode parentFsInode = toFsInode(parent);
FsInode fsInode = _fs.createLink(parentFsInode, path, uid, gid, mode, link.getBytes(StandardCharsets.UTF_8));
return toInode(fsInode);
} catch (FileExistsChimeraFsException e) {
throw new ExistException("path already exists");
}
}
@Override
public int read(Inode inode, byte[] data, long offset, int count) throws IOException {
FsInode fsInode = toFsInode(inode);
return fsInode.read(offset, data, 0, count);
}
@Override
public boolean move(Inode src, String oldName, Inode dest, String newName) throws IOException {
FsInode from = toFsInode(src);
FsInode to = toFsInode(dest);
try {
return _fs.rename(_fs.inodeOf(from, oldName, NO_STAT), from, oldName, to, newName);
} catch (NotDirChimeraException e) {
throw new NotDirException("not a directory");
} catch (FileExistsChimeraFsException e) {
throw new ExistException("destination exists");
} catch (DirNotEmptyHimeraFsException e) {
throw new NotEmptyException("directory exist and not empty");
} catch (FileNotFoundHimeraFsException e) {
throw new NoEntException("file not found");
} catch (PermissionDeniedChimeraFsException e) {
throw new PermException(e.getMessage());
}
}
@Override
public String readlink(Inode inode) throws IOException {
FsInode fsInode = toFsInode(inode);
int count = (int) fsInode.statCache().getSize();
byte[] data = new byte[count];
int n = _fs.read(fsInode, 0, data, 0, count);
if (n < 0) {
throw new NfsIoException("Can't read symlink");
}
return new String(data, 0, n, StandardCharsets.UTF_8);
}
@Override
public void remove(Inode parent, String path) throws IOException {
FsInode parentFsInode = toFsInode(parent);
try {
_fs.remove(parentFsInode, path, _fs.inodeOf(parentFsInode, path, STAT));
} catch (FileNotFoundHimeraFsException e) {
throw new NoEntException("path not found");
} catch (DirNotEmptyHimeraFsException e) {
throw new NotEmptyException("directory not empty");
}
}
@Override
public WriteResult write(Inode inode, byte[] data, long offset, int count, StabilityLevel stabilityLevel) throws IOException {
FsInode fsInode = toFsInode(inode);
int bytesWritten = fsInode.write(offset, data, 0, count);
return new WriteResult(StabilityLevel.FILE_SYNC, bytesWritten);
}
@Override
public void commit(Inode inode, long offset, int count) throws IOException {
//nop (all IO is FILE_SYNC so no commits expected)
}
@Override
public List<DirectoryEntry> list(Inode inode) throws IOException {
FsInode parentFsInode = toFsInode(inode);
List<HimeraDirectoryEntry> list = DirectoryStreamHelper.listOf(parentFsInode);
return Lists.transform(list, new ChimeraDirectoryEntryToVfs());
}
@Override
public Inode parentOf(Inode inode) throws IOException {
FsInode parent = toFsInode(inode).getParent();
if (parent == null) {
throw new NoEntException("no parent");
}
return toInode(parent);
}
@Override
public FsStat getFsStat() throws IOException {
org.dcache.chimera.FsStat fsStat = _fs.getFsStat();
return new FsStat(fsStat.getTotalSpace(),
fsStat.getTotalFiles(),
fsStat.getUsedSpace(),
fsStat.getUsedFiles());
}
private FsInode toFsInode(Inode inode) throws IOException {
return inodeFromBytes(inode.getFileId());
}
private Inode toInode(final FsInode inode) {
return Inode.forFile(inodeToBytes(inode));
}
@Override
public Stat getattr(Inode inode) throws IOException {
FsInode fsInode = toFsInode(inode);
try {
return fromChimeraStat(fsInode.stat(), fsInode.ino());
} catch (FileNotFoundHimeraFsException e) {
throw new NoEntException("Path Do not exist.");
}
}
@Override
public void setattr(Inode inode, Stat stat) throws IOException {
FsInode fsInode = toFsInode(inode);
try {
fsInode.setStat(toChimeraStat(stat));
} catch (InvalidArgumentChimeraException e) {
throw new InvalException(e.getMessage());
} catch (IsDirChimeraException e) {
throw new IsDirException(e.getMessage());
} catch (FileNotFoundHimeraFsException e) {
throw new StaleException(e.getMessage());
}
}
@Override
public nfsace4[] getAcl(Inode inode) throws IOException {
FsInode fsInode = toFsInode(inode);
try {
nfsace4[] aces;
List<ACE> dacl = _fs.getACL(fsInode);
org.dcache.chimera.posix.Stat stat = fsInode.statCache();
nfsace4[] unixAcl = Acls.of(stat.getMode(), fsInode.isDirectory());
aces = new nfsace4[dacl.size() + unixAcl.length];
int i = 0;
for (ACE ace : dacl) {
aces[i] = valueOf(ace, _idMapping);
i++;
}
System.arraycopy(unixAcl, 0, aces, i, unixAcl.length);
return Acls.compact(aces);
} catch (FileNotFoundHimeraFsException e) {
throw new StaleException(e.getMessage());
}
}
@Override
public void setAcl(Inode inode, nfsace4[] acl) throws IOException {
FsInode fsInode = toFsInode(inode);
List<ACE> dacl = new ArrayList<>();
for (nfsace4 ace : acl) {
dacl.add(valueOf(ace, _idMapping));
}
try {
_fs.setACL(fsInode, dacl);
} catch (FileNotFoundHimeraFsException e) {
throw new StaleException(e.getMessage());
}
}
private static Stat fromChimeraStat(org.dcache.chimera.posix.Stat pStat, long fileid) {
Stat stat = new Stat();
stat.setATime(pStat.getATime());
stat.setCTime(pStat.getCTime());
stat.setMTime(pStat.getMTime());
stat.setGid(pStat.getGid());
stat.setUid(pStat.getUid());
stat.setDev(pStat.getDev());
stat.setIno(Longs.hashCode(pStat.getIno()));
stat.setMode(pStat.getMode());
stat.setNlink(pStat.getNlink());
stat.setRdev(pStat.getRdev());
stat.setSize(pStat.getSize());
stat.setFileid(fileid);
stat.setGeneration(pStat.getGeneration());
return stat;
}
private static org.dcache.chimera.posix.Stat toChimeraStat(Stat stat) {
org.dcache.chimera.posix.Stat pStat = new org.dcache.chimera.posix.Stat();
if (stat.isDefined(Stat.StatAttribute.ATIME)) {
pStat.setATime(stat.getATime());
/*
* update ctime on atime update
*/
if (!stat.isDefined(Stat.StatAttribute.CTIME)) {
pStat.setCTime(System.currentTimeMillis());
}
}
if (stat.isDefined(Stat.StatAttribute.CTIME)) {
pStat.setCTime(stat.getCTime());
}
if (stat.isDefined(Stat.StatAttribute.MTIME)) {
pStat.setMTime(stat.getMTime());
}
if (stat.isDefined(Stat.StatAttribute.GROUP)) {
pStat.setGid(stat.getGid());
}
if (stat.isDefined(Stat.StatAttribute.OWNER)) {
pStat.setUid(stat.getUid());
}
if (stat.isDefined(Stat.StatAttribute.DEV)) {
pStat.setDev(stat.getDev());
}
if (stat.isDefined(Stat.StatAttribute.MODE)) {
pStat.setMode(stat.getMode());
}
if (stat.isDefined(Stat.StatAttribute.NLINK)) {
pStat.setNlink(stat.getNlink());
}
if (stat.isDefined(Stat.StatAttribute.RDEV)) {
pStat.setRdev(stat.getRdev());
}
if (stat.isDefined(Stat.StatAttribute.SIZE)) {
pStat.setSize(stat.getSize());
}
if (stat.isDefined(Stat.StatAttribute.GENERATION)) {
pStat.setGeneration(stat.getGeneration());
}
return pStat;
}
@Override
public int access(Inode inode, int mode) throws IOException {
int accessmask = mode;
if ((mode & (ACCESS4_MODIFY | ACCESS4_EXTEND)) != 0) {
FsInode fsInode = toFsInode(inode);
if (shouldRejectUpdates(fsInode)) {
accessmask ^= (ACCESS4_MODIFY | ACCESS4_EXTEND);
}
}
return accessmask;
}
private boolean shouldRejectUpdates(FsInode fsInode) throws ChimeraFsException {
return fsInode.type() == FsInodeType.INODE
&& fsInode.getLevel() == 0
&& !fsInode.isDirectory()
&& (!_fs.getInodeLocations(fsInode, StorageGenericLocation.TAPE).isEmpty()
|| !_fs.getInodeLocations(fsInode, StorageGenericLocation.DISK).isEmpty());
}
@Override
public boolean hasIOLayout(Inode inode) throws IOException {
FsInode fsInode = toFsInode(inode);
return fsInode.type() == FsInodeType.INODE && fsInode.getLevel() == 0;
}
@Override
public AclCheckable getAclCheckable() {
return this;
}
@Override
public NfsIdMapping getIdMapper() {
return _idMapping;
}
private class ChimeraDirectoryEntryToVfs implements Function<HimeraDirectoryEntry, DirectoryEntry> {
@Override
public DirectoryEntry apply(HimeraDirectoryEntry e) {
return new DirectoryEntry(e.getName(), toInode(e.getInode()), fromChimeraStat(e.getStat(), e.getInode().ino()));
}
}
private int typeToChimera(Stat.Type type) {
switch (type) {
case SYMLINK:
return Stat.S_IFLNK;
case DIRECTORY:
return Stat.S_IFDIR;
case SOCK:
return Stat.S_IFSOCK;
case FIFO:
return Stat.S_IFIFO;
case BLOCK:
return Stat.S_IFBLK;
case CHAR:
return Stat.S_IFCHR;
case REGULAR:
default:
return Stat.S_IFREG;
}
}
private static nfsace4 valueOf(ACE ace, NfsIdMapping idMapping) {
String principal;
switch (ace.getWho()) {
case USER:
principal = idMapping.uidToPrincipal(ace.getWhoID());
break;
case GROUP:
principal = idMapping.gidToPrincipal(ace.getWhoID());
break;
default:
principal = ace.getWho().getAbbreviation();
}
nfsace4 nfsace = new nfsace4();
nfsace.access_mask = new acemask4(new uint32_t(ace.getAccessMsk()));
nfsace.flag = new aceflag4(new uint32_t(ace.getFlags()));
nfsace.type = new acetype4(new uint32_t(ace.getType().getValue()));
nfsace.who = new utf8str_mixed(principal);
return nfsace;
}
private static ACE valueOf(nfsace4 ace, NfsIdMapping idMapping) throws BadOwnerException {
String principal = ace.who.toString();
int type = ace.type.value.value;
int flags = ace.flag.value.value;
int mask = ace.access_mask.value.value;
int id = -1;
Who who = Who.fromAbbreviation(principal);
if (who == null) {
// not a special pricipal
boolean isGroup = AceFlags.IDENTIFIER_GROUP.matches(flags);
if (isGroup) {
who = Who.GROUP;
id = idMapping.principalToGid(principal);
} else {
who = Who.USER;
id = idMapping.principalToUid(principal);
}
}
return new ACE(AceType.valueOf(type), flags, mask, who, id);
}
@Override
public Access checkAcl(Subject subject, Inode inode, int access) throws IOException {
FsInode fsInode = toFsInode(inode);
List<ACE> acl = _fs.getACL(fsInode);
org.dcache.chimera.posix.Stat stat = _fs.stat(fsInode);
return checkAcl(subject, acl, stat.getUid(), stat.getGid(), access);
}
private Access checkAcl(Subject subject, List<ACE> acl, int owner, int group, int access) {
for (ACE ace : acl) {
int flag = ace.getFlags();
if ((flag & ACE4_INHERIT_ONLY_ACE) != 0) {
continue;
}
if ((ace.getType() != AceType.ACCESS_ALLOWED_ACE_TYPE) && (ace.getType() != AceType.ACCESS_DENIED_ACE_TYPE)) {
continue;
}
int ace_mask = ace.getAccessMsk();
if ((ace_mask & access) == 0) {
continue;
}
Who who = ace.getWho();
if ((who == Who.EVERYONE)
|| (who == Who.OWNER && Subjects.hasUid(subject, owner))
|| (who == Who.OWNER_GROUP && Subjects.hasGid(subject, group))
|| (who == Who.GROUP && Subjects.hasGid(subject, ace.getWhoID()))
|| (who == Who.USER && Subjects.hasUid(subject, ace.getWhoID()))) {
if (ace.getType() == AceType.ACCESS_DENIED_ACE_TYPE) {
return Access.DENY;
} else {
return Access.ALLOW;
}
}
}
return Access.UNDEFINED;
}
/**
* Get a bytes corresponding to provided {code FsInode} into.
*
* @param inode to process.
* @return bytes array representing inode.
*/
private byte[] inodeToBytes(FsInode inode) {
return inode.getIdentifier();
}
/**
* Get a {code FsInode} corresponding to provided bytes.
*
* @param bytes to construct inode from.
* @return object inode.
* @throws ChimeraFsException
*/
public FsInode inodeFromBytes(byte[] handle) throws BadHandleException {
FsInode inode;
if (handle.length < MIN_HANDLE_LEN) {
throw new BadHandleException("Bad file handle");
}
ByteBuffer b = ByteBuffer.wrap(handle);
int fsid = b.get();
int type = b.get();
int len = b.get(); // eat the file id size.
long ino = b.getLong();
int opaqueLen = b.get();
if (opaqueLen > b.remaining()) {
throw new BadHandleException("Bad/old file handle");
}
FsInodeType inodeType = FsInodeType.valueOf(type);
switch (inodeType) {
case INODE:
if (opaqueLen != 1) {
throw new BadHandleException("Bad file handle: invalid level len :" + opaqueLen);
}
int level = b.get() - 0x30; // 0x30 is ascii code for '0'
if (level < 0 || level > JdbcFs.LEVELS_NUMBER) {
throw new BadHandleException("Bad file handle: invalid level:" + level);
}
inode = new FsInode(_fs, ino, level);
break;
case ID:
inode = new FsInode_ID(_fs, ino);
break;
case TAGS:
inode = new FsInode_TAGS(_fs, ino);
break;
case TAG:
String tag = new String(handle, b.position(), opaqueLen);
inode = new FsInode_TAG(_fs, ino, tag);
break;
case NAMEOF:
inode = new FsInode_NAMEOF(_fs, ino);
break;
case PARENT:
inode = new FsInode_PARENT(_fs, ino);
break;
case PATHOF:
inode = new FsInode_PATHOF(_fs, ino);
break;
case CONST:
inode = new FsInode_CONST(_fs, ino);
break;
case PSET:
inode = new FsInode_PSET(_fs, ino, getArgs(b, opaqueLen));
break;
case PCUR:
inode = new FsInode_PCUR(_fs, ino);
break;
case PLOC:
inode = new FsInode_PLOC(_fs, ino);
break;
case PCRC:
inode = new FsInode_PCRC(_fs, ino);
break;
default:
throw new BadHandleException("Unsupported file handle type: " + inodeType);
}
return inode;
}
private String[] getArgs(ByteBuffer b, int opaqueLen) {
StringTokenizer st = new StringTokenizer(new String(b.array(), b.position(), opaqueLen), "[:]");
int argc = st.countTokens();
String[] args = new String[argc];
for (int i = 0; i < argc; i++) {
args[i] = st.nextToken();
}
return args;
}
}