package diskCacheV111.util; import org.junit.After; import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.regex.PatternSyntaxException; import diskCacheV111.vehicles.ProtocolInfo; import org.dcache.auth.Subjects; import org.dcache.vehicles.FileAttributes; import static com.google.common.base.Preconditions.checkState; import static org.hamcrest.Matchers.is; public class CheckStagePermissionTests { /* * definition is taken verbatim from NFS4ProtocolInfo * (NFS4) and major=4 seem to be redundant */ public static final ProtocolInfo NFS4_PROTOCOL_INFO = new ProtocolInfo () { private static final String _protocolName = "NFS4"; private static final int _minor = 1; private static final int _major = 4; @Override public String getProtocol() { return _protocolName; } @Override public int getMinorVersion() { return _minor; } @Override public int getMajorVersion() { return _major; } @Override public String getVersionString() { return _protocolName + "-" + _major + "." + _minor; } }; public static final ProtocolInfo XROOTD_PROTOCOL_INFO = new ProtocolInfo () { private static final String _protocolName = "Xrootd"; private static final int _minor = 2; private static final int _major = 7; @Override public String getProtocol() { return _protocolName; } @Override public int getMinorVersion() { return _minor; } @Override public int getMajorVersion() { return _major; } @Override public String getVersionString() { return _protocolName + "-" + _major + "." + _minor; } }; private File _file; private CheckStagePermission _check; private boolean _allowed; @Before public void setUp() throws IOException { _file = null; _check = null; } @After public void tearDown() { if (_file != null) { _file.delete(); } } /* * Tests that check behaviour when stage protection is disabled. */ @Test public void shouldAllowUserWithDnAndFqanToStageIfEmptyConstructed() throws Exception { givenCheckConstructedWith(""); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowUserWithDnToStageIfEmptyConstructed() throws Exception { givenCheckConstructedWith(""); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowUserWithDnAndFqanToStageIfNullConstructed() throws Exception { givenCheckConstructedWith(null); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowUserWithDnToStageIfNullConstructed() throws Exception { givenCheckConstructedWith(null); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } /* * Tests that check behaviour when stage protection file is empty. */ @Test() public void shouldDenyUserWithDnAndFqanToStageIfEmpty() throws Exception { given(file().isEmpty()); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } /* * Tests that check behaviour when stage protection file is missing. */ @Test() public void shouldDenyUserWithDnAndFqanToStageIfMissing() throws Exception { given(file().isMissing()); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } /* * Tests that check behaviour when a DN is authorised to stage. */ @Test public void shouldAllowUserWithDnToStageIfDnAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowUserWithDnAndFqanToStageIfDnAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldDenyUserWithWrongDnToStageIfDnAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\"")); whenCheck(Subjects.of().dn("/DC=org/DC=otherExample/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } /* * Tests that check behaviour when a (DN,FQAN) pair is authorised to stage. */ @Test public void shouldDenyUserWithDnToStageIfDnFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } @Test public void shouldDenyUserWithDnAndWrongFqanToStageIfDnFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } @Test public void shouldAllowUserWithDnAndFqanToStageIfDnFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowUserWithDnAndNonprimaryFqanToStageIfDnFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldDenyUserWithWrongDnToStageIfDnFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\"")); whenCheck(Subjects.of().dn("/DC=org/DC=anotherExample/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } /* * Tests to check that updating a file is honoured. */ @Test public void shouldAllowUserWithDnToStageIfDnAuthzAfterReload() throws Exception { given(file().isEmpty()); givenCheckedWith(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"), NFS4_PROTOCOL_INFO); // Note: filesystems have differing granularity of their timestamp: so // we must make sure the mtime has increased. given(file().hasContents("\"/DC=org/DC=example/CN=test user\"").withDifferentMtime()); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"), NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } /* * Tests that check behaviour when a pattern-based (DN,FQAN) pair is * authorised to stage a specific storage class. */ @Test public void shouldAllowUserWithDnToStageIfWildDnWildFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/.*\" \"/atlas/Role=.*\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldDenyUserWithWrongDnToStageIfWildDnWildFqanAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/.*\" \"/atlas/Role=.*\"")); whenCheck(Subjects.of().dn("/DC=org/DC=anotherExample/CN=test").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } @Test public void shouldAllowUserWithDnFqanToStageIfAnyDnAnyFqanAnyStoreAuthz() throws Exception { given(file().hasContents("\".*\" \".*\" \".*\"")); whenCheck(Subjects.of().dn("/DC=org/DC=anotherExample/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } /* * Tests that check behaviour when a (DN,FQAN) pair is authorised to stage * a specific storage class. */ @Test public void shouldAllowUserWithDnFqanToStageStoreIfDnFqanStoreAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\" \"sql:chimera@osm\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldDenyUserWithDnFqanToStageWrongStoreIfDnFqanStoreAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\" \"sql:chimera@osm\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("data:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } /* * Tests that check behaviour when a (DN,FQAN,StorageUnit) triplet is authorised to stage * a specific protocol */ @Test public void shouldAllowUserWithDnFqanStoreToStageIfDnFqanStoreProtocolAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\" \"sql:chimera@osm\" \"NFS4/4\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldDenyUserWithDnFqanStoreWrongProtocoIfDnFqanStoreAuthz() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\" \"sql:chimera@osm\" \"DCap/3\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } @Test public void shouldDenyUserWithDnFqanStoreProtocoIfDnFqanStoreAuthzButPtocolIsBanned() throws Exception { given(file().hasContents("\"/DC=org/DC=example/CN=test user\" \"/atlas/Role=production\" \"sql:chimera@osm\" \"!NFS4/4\"")); whenCheck(Subjects.of().dn("/DC=org/DC=example/CN=test user").fqan("/atlas/Role=production").fqan("/atlas"), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(false)); } @Test public void shouldAllowAnonymousUserIfStoreIsNotBlackListedProtocolNotListed() throws Exception { given(file().hasContents("\"\" \"\" \"!sql:chimera@osm\"")); whenCheck(Subjects.of().dn("").fqan("").fqan(""), FileAttributes.of().storageClass("sql:chimera").hsm("enstore"),NFS4_PROTOCOL_INFO); assertThat(_allowed, is(true)); } @Test public void shouldAllowAnonymousUserStoreIfProtocolNotBlackListed() throws Exception { given(file().hasContents("\"\" \"\" \"sql:chimera@osm\" \"!NFS.*\"")); whenCheck(Subjects.of().dn("").fqan("").fqan(""), FileAttributes.of().storageClass("sql:chimera").hsm("osm"),XROOTD_PROTOCOL_INFO); assertThat(_allowed, is(true)); } private void whenCheck(Subjects.Builder subjectBuilder, FileAttributes.Builder attributeBuilder, ProtocolInfo protocolInfo) throws PatternSyntaxException, IOException { _allowed = getCheck().canPerformStaging(subjectBuilder.build(), attributeBuilder.build(), protocolInfo); } private CheckStagePermission getCheck() throws IOException { if (_check == null) { _check = new CheckStagePermission(_file.getCanonicalPath()); } return _check; } private void givenCheckConstructedWith(String argument) { _check = new CheckStagePermission(argument); } private void givenCheckedWith(Subjects.Builder subjectBuilder, FileAttributes.Builder attributeBuilder, ProtocolInfo protocolInfo) throws PatternSyntaxException, IOException { getCheck().canPerformStaging(subjectBuilder.build(), attributeBuilder.build(),protocolInfo); } private FileStateAssertion file() throws IOException { if (_file == null) { _file = File.createTempFile("stagePermissionFile", null, null); _file.deleteOnExit(); } return new FileStateAssertion(_file); } private void given(FileStateAssertion assertion) throws IOException, InterruptedException { assertion.apply(); } /** * A class to record assumptions about the file's state and a method to * enact those assumptions. */ public static class FileStateAssertion { private final File _file; private String _contents; private boolean _deleteFile; private long _mtime; public FileStateAssertion(File file) { _file = file; } public FileStateAssertion hasContents(String contents) throws IOException { _contents = contents.endsWith("\n") ? contents : (contents + '\n'); return this; } public FileStateAssertion isEmpty() throws IOException { _contents = ""; return this; } public FileStateAssertion isMissing() { _deleteFile = true; return this; } public FileStateAssertion withDifferentMtime() throws InterruptedException { _mtime = _file.lastModified(); return this; } public void apply() throws IOException, InterruptedException { checkState(_deleteFile || _contents != null); checkState(!_deleteFile || _mtime == 0); if (_deleteFile) { _file.delete(); } else { updateContent(); while (_mtime != 0 && _mtime == _file.lastModified()) { Thread.sleep(10); updateContent(); } } } private void updateContent() throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(_file))) { writer.write(_contents); } } } }