diff --git a/modules/common/src/main/java/org/dcache/auth/ExemptFromNamespaceChecks.java b/modules/common/src/main/java/org/dcache/auth/ExemptFromNamespaceChecks.java
new file mode 100644
index 00000000000..9d3734719e8
--- /dev/null
+++ b/modules/common/src/main/java/org/dcache/auth/ExemptFromNamespaceChecks.java
@@ -0,0 +1,46 @@
+/*
+ * dCache - http://www.dcache.org/
+ *
+ * Copyright (C) 2021 Deutsches Elektronen-Synchrotron
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package org.dcache.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+
+/**
+ * The presence of this principal indicates that the user is exempt from the
+ * normal namespace permission rules. The
+ * {@link org.dcache.auth.attributes.Restriction} accompanying a namespace
+ * request is still enforced. Code that inserts this principal should (very
+ * likely) also add restrictions, otherwise the user will have root-like
+ * authority.
+ */
+@AuthenticationOutput
+public class ExemptFromNamespaceChecks implements Principal, Serializable
+{
+ @Override
+ public String getName()
+ {
+ return "full"; // all namespace checks are by-passed.
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ExemptFromNamespaceChecks";
+ }
+}
diff --git a/modules/common/src/main/java/org/dcache/auth/Subjects.java b/modules/common/src/main/java/org/dcache/auth/Subjects.java
index e888dbb483c..60a83a8f3a5 100644
--- a/modules/common/src/main/java/org/dcache/auth/Subjects.java
+++ b/modules/common/src/main/java/org/dcache/auth/Subjects.java
@@ -22,6 +22,8 @@
import java.util.Set;
import java.util.stream.Collectors;
+import org.dcache.util.PrincipalSetMaker;
+
import static com.google.common.base.Preconditions.checkArgument;
public class Subjects
@@ -67,6 +69,21 @@ public static boolean isRoot(Subject subject)
return hasUid(subject, 0);
}
+ /**
+ * Return true if the subject is root or has the special
+ * ExemptFromNamespaceChecks principal.
+ * @param subject The identity of the user.
+ * @return if the user is except from namespace checks.
+ * @see #isRoot(javax.security.auth.Subject)
+ */
+ public static boolean isExemptFromNamespaceChecks(Subject subject)
+ {
+ return subject.getPrincipals().stream()
+ .anyMatch(p -> p instanceof UidPrincipal && ((UidPrincipal)p).getUid() == 0
+ ||
+ p instanceof ExemptFromNamespaceChecks);
+ }
+
/**
* Returns true if and only if the subject is nobody, i.e., does
* not have a UID.
@@ -678,7 +695,7 @@ private static StringBuilder appendOptionallyInQuotes(StringBuilder sb, String a
// Returned Subject must NOT be readOnly.
public static Subject of(int uid, int gid, int[] gids)
{
- Builder builder = of().uid(uid).gid(gid);
+ Builder builder = Subjects.of().uid(uid).gid(gid);
for (int g : gids) {
builder.gid(g);
}
@@ -690,6 +707,18 @@ public static Builder of()
return new Builder();
}
+ public static Subject ofPrincipals(Set principals)
+ {
+ Subject subject = new Subject();
+ subject.getPrincipals().addAll(principals);
+ return subject;
+ }
+
+ public static Subject of(PrincipalSetMaker maker)
+ {
+ return ofPrincipals(maker.build());
+ }
+
public static class Builder
{
private final Subject _subject = new Subject();
diff --git a/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java b/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java
index 6f48a546760..140d56ef4a5 100644
--- a/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java
+++ b/modules/common/src/main/java/org/dcache/util/PrincipalSetMaker.java
@@ -11,6 +11,7 @@
import org.dcache.auth.DesiredRole;
import org.dcache.auth.EmailAddressPrincipal;
+import org.dcache.auth.ExemptFromNamespaceChecks;
import org.dcache.auth.FQANPrincipal;
import org.dcache.auth.GidPrincipal;
import org.dcache.auth.GroupNamePrincipal;
@@ -181,6 +182,12 @@ public PrincipalSetMaker withKerberos(String kerberos)
return this;
}
+ public PrincipalSetMaker withExemptFromNamespaceChecks()
+ {
+ _principals.add(new ExemptFromNamespaceChecks());
+ return this;
+ }
+
/**
* Provide a unmodifiable view of the set of principals.
*/
diff --git a/modules/common/src/test/java/org/dcache/auth/SubjectsTest.java b/modules/common/src/test/java/org/dcache/auth/SubjectsTest.java
index f033ee97c03..ca68bd40f49 100644
--- a/modules/common/src/test/java/org/dcache/auth/SubjectsTest.java
+++ b/modules/common/src/test/java/org/dcache/auth/SubjectsTest.java
@@ -12,7 +12,10 @@
import java.util.NoSuchElementException;
import java.util.Set;
+import org.dcache.util.PrincipalSetMaker;
+
import static java.util.Arrays.asList;
+import static org.dcache.util.PrincipalSetMaker.aSetOfPrincipals;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.*;
@@ -285,4 +288,35 @@ public void shouldPrincipalsFromArgsForOidc()
assertThat(principals, hasItem(new OidcSubjectPrincipal("sub-claim", "OP")));
}
+
+ @Test
+ public void normalUserShouldNotBeExceptFromNamespaceChecks()
+ {
+ var subject = Subjects.of(aSetOfPrincipals().withOidc("sub-claim", "OP"));
+
+ assertFalse(Subjects.isExemptFromNamespaceChecks(_subject1));
+ assertFalse(Subjects.isExemptFromNamespaceChecks(_subject2));
+ assertFalse(Subjects.isExemptFromNamespaceChecks(_subject3));
+ assertFalse(Subjects.isExemptFromNamespaceChecks(_subject4));
+ assertFalse(Subjects.isExemptFromNamespaceChecks(subject));
+ }
+
+ @Test
+ public void rootShouldBeExceptFromNamespaceChecks()
+ {
+ var root = Subjects.of(aSetOfPrincipals().withUid(0).withPrimaryGid(0));
+
+ assertTrue(Subjects.isExemptFromNamespaceChecks(root));
+ }
+
+ @Test
+ public void exemptUserShouldBeExceptFromNamespaceChecks()
+ {
+ var root = Subjects.of(aSetOfPrincipals()
+ .withUid(1000)
+ .withPrimaryGid(1000)
+ .withExemptFromNamespaceChecks());
+
+ assertTrue(Subjects.isExemptFromNamespaceChecks(root));
+ }
}
diff --git a/modules/dcache-chimera/src/main/java/org/dcache/chimera/namespace/ChimeraNameSpaceProvider.java b/modules/dcache-chimera/src/main/java/org/dcache/chimera/namespace/ChimeraNameSpaceProvider.java
index 39727382593..ce8e14f3544 100644
--- a/modules/dcache-chimera/src/main/java/org/dcache/chimera/namespace/ChimeraNameSpaceProvider.java
+++ b/modules/dcache-chimera/src/main/java/org/dcache/chimera/namespace/ChimeraNameSpaceProvider.java
@@ -228,7 +228,7 @@ public void setUploadSubDirectory(String path)
private ExtendedInode pathToInode(Subject subject, String path)
throws ChimeraFsException, CacheException
{
- if (Subjects.isRoot(subject)) {
+ if (Subjects.isExemptFromNamespaceChecks(subject)) {
return new ExtendedInode(_fs, _fs.path2inode(path));
}
@@ -304,7 +304,7 @@ public FileAttributes createFile(Subject subject, String path,
}
ExtendedInode parent = pathToInode(subject, parentPath);
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes
= getFileAttributesForPermissionHandler(parent);
if (_permissionHandler.canCreateFile(subject, attributes) != ACCESS_ALLOWED) {
@@ -409,7 +409,7 @@ public PnfsId createSymLink(Subject subject, String path, String dest, FileAttri
}
ExtendedInode parent = pathToInode(subject, parentPath);
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes
= getFileAttributesForPermissionHandler(parent);
if (_permissionHandler.canCreateFile(subject, attributes) != ACCESS_ALLOWED) {
@@ -505,7 +505,7 @@ public FileAttributes deleteEntry(Subject subject, Set allowed, PnfsId
checkAllowed(allowed, inode);
- if (!Subjects.isRoot(subject) && !canDelete(subject, inode.getParent(), inode)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) && !canDelete(subject, inode.getParent(), inode)) {
throw new PermissionDeniedCacheException("Access denied: " + pnfsId);
}
@@ -540,7 +540,7 @@ public FileAttributes deleteEntry(Subject subject, Set allowed,
ExtendedInode inode = parent.inodeOf(name, STAT);
checkAllowed(allowed, inode);
- if (!Subjects.isRoot(subject) && !canDelete(subject, parent, inode)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) && !canDelete(subject, parent, inode)) {
throw new PermissionDeniedCacheException("Access denied: " + path);
}
@@ -579,7 +579,7 @@ public FileAttributes deleteEntry(Subject subject, Set allowed,
checkAllowed(allowed, inode);
- if (!Subjects.isRoot(subject) && !canDelete(subject, parent, inode)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) && !canDelete(subject, parent, inode)) {
throw new PermissionDeniedCacheException("Access denied: " + path);
}
@@ -617,7 +617,7 @@ public void rename(Subject subject, @Nullable PnfsId pnfsId,
if (pnfsId != null) {
inode = new ExtendedInode(_fs, pnfsId, STAT);
} else {
- if (!Subjects.isRoot(subject) &&
+ if (!Subjects.isExemptFromNamespaceChecks(subject) &&
_permissionHandler.canLookup(subject, sourceDirAttributes) != ACCESS_ALLOWED) {
throw new PermissionDeniedCacheException("Access denied: " + sourcePath);
}
@@ -659,8 +659,8 @@ public void rename(Subject subject, @Nullable PnfsId pnfsId,
/* Permission checks.
*/
- if (!Subjects.isRoot(subject) || !overwrite) {
- if (!Subjects.isRoot(subject) &&
+ if (!Subjects.isExemptFromNamespaceChecks(subject) || !overwrite) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) &&
_permissionHandler.canRename(subject,
sourceDirAttributes,
destDirAttributes,
@@ -1023,7 +1023,7 @@ public FileAttributes getFileAttributes(Subject subject, PnfsId pnfsId,
try {
ExtendedInode inode = new ExtendedInode(_fs, pnfsId, STAT);
- if (Subjects.isRoot(subject)) {
+ if (Subjects.isExemptFromNamespaceChecks(subject)) {
return getFileAttributes(inode, attr);
}
@@ -1060,9 +1060,9 @@ public FileAttributes setFileAttributes(Subject subject, PnfsId pnfsId,
_log.debug("File attributes update: {}", attr.getDefinedAttributes());
try {
- ExtendedInode inode = new ExtendedInode(_fs, pnfsId, Subjects.isRoot(subject) ? NO_STAT : STAT);
+ ExtendedInode inode = new ExtendedInode(_fs, pnfsId, Subjects.isExemptFromNamespaceChecks(subject) ? NO_STAT : STAT);
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes =
getFileAttributesForPermissionHandler(inode);
@@ -1210,7 +1210,7 @@ public void list(Subject subject, String path, Glob glob, Range range,
throw new NotDirCacheException("Not a directory: " + path);
}
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes =
getFileAttributesForPermissionHandler(dir);
if (!dir.isDirectory()) {
@@ -1258,7 +1258,7 @@ public void list(Subject subject, String path, Glob glob, Range range,
private ExtendedInode mkdir(Subject subject, ExtendedInode parent, String name, int uid, int gid, int mode)
throws ChimeraFsException, CacheException
{
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributesOfParent
= getFileAttributesForPermissionHandler(parent);
if (_permissionHandler.canCreateSubDir(subject, attributesOfParent) != ACCESS_ALLOWED) {
@@ -1338,7 +1338,7 @@ public FsPath createUploadPath(Subject subject, FsPath path, FsPath rootPath,
: lookupDirectory(subject, path.parent());
FileAttributes attributesOfParent =
- !Subjects.isRoot(subject)
+ !Subjects.isExemptFromNamespaceChecks(subject)
? getFileAttributesForPermissionHandler(parentOfPath)
: null;
@@ -1352,7 +1352,7 @@ public FsPath createUploadPath(Subject subject, FsPath path, FsPath rootPath,
}
/* User must be authorized to delete existing file.
*/
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributesOfPath =
getFileAttributesForPermissionHandler(inodeOfPath);
if (_permissionHandler.canDeleteFile(subject,
@@ -1366,7 +1366,7 @@ public FsPath createUploadPath(Subject subject, FsPath path, FsPath rootPath,
/* User must be authorized to create file.
*/
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
if (_permissionHandler.canCreateFile(subject, attributesOfParent) != ACCESS_ALLOWED) {
throw new PermissionDeniedCacheException("Access denied: " + path);
}
@@ -1536,7 +1536,7 @@ public FileAttributes commitUpload(Subject subject, FsPath temporaryPath, FsPath
}
/* User must be authorized to delete existing file.
*/
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributesOfParent =
getFileAttributesForPermissionHandler(finalDirInode);
FileAttributes attributesOfFile =
@@ -1650,7 +1650,7 @@ public byte[] readExtendedAttribute(Subject subject, FsPath path, String name)
try {
ExtendedInode target = pathToInode(subject, path.toString());
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes = getFileAttributesForPermissionHandler(target);
if (target.isDirectory()) {
if (_permissionHandler.canListDir(subject, attributes) != ACCESS_ALLOWED) {
@@ -1678,7 +1678,7 @@ public void writeExtendedAttribute(Subject subject, FsPath path, String name,
try {
ExtendedInode target = pathToInode(subject, path.toString());
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes = getFileAttributesForPermissionHandler(target);
if (target.isDirectory()) {
if (_permissionHandler.canCreateFile(subject, attributes) != ACCESS_ALLOWED) {
@@ -1725,7 +1725,7 @@ public Set listExtendedAttributes(Subject subject, FsPath path)
try {
ExtendedInode target = pathToInode(subject, path.toString());
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes = getFileAttributesForPermissionHandler(target);
if (target.isDirectory()) {
if (_permissionHandler.canListDir(subject, attributes) != ACCESS_ALLOWED) {
@@ -1753,7 +1753,7 @@ public void removeExtendedAttribute(Subject subject, FsPath path, String name)
try {
ExtendedInode target = pathToInode(subject, path.toString());
- if (!Subjects.isRoot(subject)) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject)) {
FileAttributes attributes = getFileAttributesForPermissionHandler(target);
if (target.isDirectory()) {
if (_permissionHandler.canCreateFile(subject, attributes) != ACCESS_ALLOWED) {
diff --git a/modules/dcache/src/main/java/diskCacheV111/namespace/PnfsManagerV3.java b/modules/dcache/src/main/java/diskCacheV111/namespace/PnfsManagerV3.java
index 7729881ba5f..60ae7512e1c 100644
--- a/modules/dcache/src/main/java/diskCacheV111/namespace/PnfsManagerV3.java
+++ b/modules/dcache/src/main/java/diskCacheV111/namespace/PnfsManagerV3.java
@@ -2474,7 +2474,7 @@ private void checkMask(PnfsMessage message)
private void checkMask(Subject subject, PnfsId pnfsId, Set mask)
throws CacheException
{
- if (!Subjects.isRoot(subject) && !mask.isEmpty()) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) && !mask.isEmpty()) {
Set required =
_permissionHandler.getRequiredAttributes();
FileAttributes attributes =
@@ -2491,7 +2491,7 @@ private void checkMask(Subject subject, PnfsId pnfsId, Set mask)
private void checkMask(Subject subject, String path, Set mask)
throws CacheException
{
- if (!Subjects.isRoot(subject) && !mask.isEmpty()) {
+ if (!Subjects.isExemptFromNamespaceChecks(subject) && !mask.isEmpty()) {
Set required =
_permissionHandler.getRequiredAttributes();
PnfsId pnfsId = _nameSpaceProvider.pathToPnfsid(ROOT, path, false);
diff --git a/modules/gplazma2-scitoken/src/main/java/org/dcache/gplazma/scitoken/SciTokenPlugin.java b/modules/gplazma2-scitoken/src/main/java/org/dcache/gplazma/scitoken/SciTokenPlugin.java
index 9f662c19b89..57dc583d079 100644
--- a/modules/gplazma2-scitoken/src/main/java/org/dcache/gplazma/scitoken/SciTokenPlugin.java
+++ b/modules/gplazma2-scitoken/src/main/java/org/dcache/gplazma/scitoken/SciTokenPlugin.java
@@ -41,6 +41,7 @@
import diskCacheV111.util.FsPath;
import org.dcache.auth.BearerTokenCredential;
+import org.dcache.auth.ExemptFromNamespaceChecks;
import org.dcache.auth.JwtJtiPrincipal;
import org.dcache.auth.JwtSubPrincipal;
import org.dcache.auth.Subjects;
@@ -157,6 +158,7 @@ public void authenticate(Set