Implement some kind of support for locking of registered items.

(Or registry items.)
This commit is contained in:
asofold 2018-02-11 13:29:51 +01:00
parent d3a66b01ba
commit 7b8390dd5a
3 changed files with 489 additions and 0 deletions

View File

@ -0,0 +1,241 @@
package fr.neatmonster.nocheatplus.components.registry.lockable;
/**
* Basic implementation to have a quick way to interact, customizable secret
* locking supporting class type checks by default. This implementation is not
* meant to be secure against an attacker, it's meant to help preventing
* re-registration or accidental API-misuse.
* <hr>
* This implementation is not thread-safe (for locking).
*
* @author asofold
*
*/
public class BasicLockable implements ILockable {
/*
* TODO: Consider switching to a settings class with chaining (then
* translate into flags for BasicLockable).
*/
private static final int ALLOW_LOCK_NOSECRET = 0x01;
private static final int ALLOW_LOCK_SECRET = 0x02;
private static final int ALLOW_UNLOCK_SECRET = 0x04;
/** Use identity for a set secret. */
private static final int SECRET_IDENTITY = 0x08;
private static final int EXACT_SECRET_TYPE = 0x10;
private static final int REMOVE_SECRET_UNLOCK = 0x20;
private boolean isLocked = false;
private final int lockFlags;
private final Class<?> lockSecretType;
private Object lockSecret = null;
/**
* Start unlocked with no restrictions. A set secret will be removed on
* unlock.
*/
public BasicLockable() {
this(true, true, true, false, true, void.class, false);
}
/**
* Start with a specific secret set, but with no further restrictions
* applying. Allows permanent locking. The secret is removed with unlock.
*
* @param secret
* May be null, but note that a null secret with isLocked set,
* yields a permanently locked instance.
* @param secretIdentity
* @param isLocked
*/
public BasicLockable(Object secret, boolean secretIdentity, boolean isLocked) {
this(true, true, true, secretIdentity, true, void.class, false);
this.lockSecret = secret;
this.isLocked = isLocked;
}
/**
* Lock down to the class of the given secret, set the secret from start.
* Does not allow permanent locking. The secret is removed with unlock.
*
* @param secret
* @param secretIdentity
* @param exactSecretType
* @param isLocked
*/
public BasicLockable(Object secret, boolean secretIdentity, boolean exactSecretType, boolean isLocked) {
this(false, true, true, secretIdentity, true, secret.getClass(), exactSecretType);
this.lockSecret = secret;
this.isLocked = isLocked;
}
/**
* Lock/Unlock with a secret only. Permanent locking is not supported. The
* secret is removed with unlock.
*
* @param secretIdentity
* @param secretType
* @param exactSecretType
*/
public BasicLockable(boolean secretIdentity, Class<?> secretType, boolean exactSecretType) {
this(false, true, true, secretIdentity, true, secretType, exactSecretType);
}
/**
* Basic constructor.
*
* @param allowLockNoSecret
* @param allowLockSecret
* @param allowUnlockSecret
* @param secretIdentity
* @param secretType
* Set to void.class in order to ignore this option. Setting to
* null yields an IllegalArgumentException.
* @param exactSecretType
* @throws IllegalArgumentException
* In case secretType is null.
*/
public BasicLockable(boolean allowLockNoSecret, boolean allowLockSecret,
boolean allowUnlockSecret, boolean secretIdentity, boolean removeSecretUnlock,
Class<?> secretType, boolean exactSecretType) {
if (secretType == null) {
throw new IllegalArgumentException("Can't pass null for secretType, use void.class instead, to ignore this option.");
}
this.lockSecretType = secretType;
int lockFlags = setLockFlag(ALLOW_LOCK_NOSECRET, allowLockNoSecret, 0x00);
lockFlags = setLockFlag(ALLOW_LOCK_SECRET, allowLockSecret, lockFlags);
lockFlags = setLockFlag(ALLOW_UNLOCK_SECRET, allowUnlockSecret, lockFlags);
lockFlags = setLockFlag(SECRET_IDENTITY, secretIdentity, lockFlags);
lockFlags = setLockFlag(REMOVE_SECRET_UNLOCK, removeSecretUnlock, lockFlags);
lockFlags = setLockFlag(EXACT_SECRET_TYPE, exactSecretType, lockFlags);
this.lockFlags = lockFlags;
}
private final int setLockFlag(int flag, boolean value, int result) {
if (value) {
return result | flag;
}
else {
return result & ~flag;
}
}
private final boolean isLockFlagSet(int flag) {
return (lockFlags & flag) == flag;
}
/**
*
* @param givenSecret
* @param isUnlock
* @return
*/
private final boolean isApplicableLockSecret(final Object givenSecret, final boolean isUnlock) {
// Secret check.
if (this.lockSecret == null) {
// Check if is permanently locked.
if (this.isLocked) {
return false;
}
}
else {
if (isLockFlagSet(SECRET_IDENTITY)) {
if (this.lockSecret != givenSecret) {
return false;
}
}
else if (!this.lockSecret.equals(givenSecret)) {
return false;
}
}
// Type check.
if (lockSecretType != void.class) {
final Class<?> type = givenSecret.getClass();
if (isLockFlagSet(EXACT_SECRET_TYPE)) {
if (this.lockSecretType != type) {
return false;
}
}
else if (!this.lockSecretType.isAssignableFrom(type)) {
return false;
}
}
return furtherLockingRestrictions(givenSecret, isUnlock);
}
/**
* Called upon successful access checks (lock/unlock allowed so far). Note
* that this is called for lock() with givenSecret set to null - otherwise a
* given null secret would yield an IllegalArgumentException.
* <hr>
* Override for functionality (the default implementation always returns
* true).
*
* @param givenSecret
* @param isUnlock
* @return True to allow lock/unlock, false to prevent.
* @throws IllegalStateException
* If custom side conditions are not met, and throwing
* IllegalStateException is preferred over an
* IllegalArgumentException.
*/
protected boolean furtherLockingRestrictions(final Object givenSecret, final boolean isUnlock) {
return true;
}
@Override
public final void lock() {
if (!isLockFlagSet(ALLOW_LOCK_NOSECRET)) {
throw new UnsupportedOperationException();
}
if (!furtherLockingRestrictions(null, false)) {
throw new IllegalStateException();
}
isLocked = true;
// Change to permanently locked.
this.lockSecret = null;
}
@Override
public final void lock(final Object secret) {
if (!isLockFlagSet(ALLOW_LOCK_SECRET)) {
throw new UnsupportedOperationException();
}
if (secret == null || !isApplicableLockSecret(secret, false)) {
throw new IllegalArgumentException();
}
this.lockSecret = secret;
isLocked = true;
}
@Override
public final void unlock(final Object secret) {
if (!isLockFlagSet(ALLOW_UNLOCK_SECRET)) {
throw new UnsupportedOperationException();
}
if (secret == null || !isApplicableLockSecret(secret, true)) {
throw new IllegalArgumentException();
}
if (!isLocked) {
return;
}
if (isLockFlagSet(REMOVE_SECRET_UNLOCK)) {
this.lockSecret = null;
}
isLocked = false;
}
@Override
public boolean isLocked() {
return isLocked;
}
@Override
public void throwIfLocked() {
if (isLocked) {
throw new IllegalStateException();
}
}
}

View File

@ -0,0 +1,71 @@
package fr.neatmonster.nocheatplus.components.registry.lockable;
/**
* An instance that allows locking, either final, or locking and unlocking via a
* secret.
*
* @author asofold
*
*/
public interface ILockable {
// TODO: Use own registry exceptions.
/**
* Final permanent locking of the item. If a secret is set, the reference
* will be set to null, provided permanent locking is supported. Not
* supposed to be reversible.
*
* @throws UnsupportedOperationException
* If locking without secret is not supported in the first
* place.
* @throws IllegalStateException
* If locking is not supported due to custom side conditions.
*/
public void lock();
/**
* Lock with setting a secret. Restrictions may apply for the nature of the
* given secret. This may lead to overriding an internally stored secret, in
* case the secret is valid for (override) locking, concerning the state the
* ILockable instance is in - depending on implementation.
*
* @param secret
* @throws UnsupportedOperationException
* If locking with a secret is not supported at this moment.
* @throws IllegalArgumentException
* If the secret does not fulfill the requirements (includes
* null).
*/
public void lock(Object secret);
/**
* Unlock using the given secret. Restrictions may apply for the nature of
* the given secret. This may lead to removal of the internally set secret,
* depending on implementation.
*
* @param secret
* @throws UnsupportedOperationException
* If unlocking with a secret is not supported at this moment.
* @throws IllegalArgumentException
* If the secret does not fulfill the requirements (includes
* null).
*/
public void unlock(Object secret);
/**
* Test if is locked.
*
* @return
*/
public boolean isLocked();
/**
* Convenience: throw an IllegalStateException, if already locked.
*
* @throws IllegalStateException
* If already locked.
*/
public void throwIfLocked();
}

View File

@ -0,0 +1,177 @@
package fr.neatmonster.nocheatplus.test;
import static org.junit.Assert.fail;
import org.junit.Test;
import fr.neatmonster.nocheatplus.components.registry.lockable.BasicLockable;
import fr.neatmonster.nocheatplus.components.registry.lockable.ILockable;
public class TestBasicLockable {
static class Dummy {
Dummy equalsOther;
Dummy() {
}
Dummy(Dummy equalsOther) {
this.equalsOther = equalsOther;
}
@Override
public boolean equals(Object obj) {
return obj == this || this.equalsOther != null && this.equalsOther == obj;
}
}
static class DummMY extends Dummy {}
private BasicLockable getLocked() {
BasicLockable lock = new BasicLockable();
try {
lock.lock();
}
catch (Exception e) {
fail("lock() should work here");
}
return lock;
}
/**
* Attempt to lock an already permanently locked item with a secret.
*/
@Test(expected = IllegalArgumentException.class)
public void testFailChangeLockNoSecret() {
BasicLockable lock = getLocked();
lock.lock(new Object());
}
/**
* Lock twice permanently (expect no exception).
*/
@Test
public void testLockTwice() {
BasicLockable lock = getLocked();
lock.lock();
}
@Test
public void testPermanentLockOverrideSecret() {
BasicLockable lock = new BasicLockable();
lock.lock(lock);
lock.lock();
}
@Test(expected = IllegalArgumentException.class)
public void testSecretTypeIdentity() {
BasicLockable lock = new BasicLockable(new Dummy(), true, true);
lock.lock(new Dummy());
}
@Test(expected = IllegalArgumentException.class)
public void testSecretTypeExactFail1() {
BasicLockable lock = new BasicLockable(new Dummy(), false, true, false);
lock.lock(new Dummy());
}
@Test(expected = IllegalArgumentException.class)
public void testSecretTypeExactFail2() {
BasicLockable lock = new BasicLockable(new Dummy(), false, true, false);
lock.lock(new DummMY());
}
@Test
public void testSecretTypeSubClass() {
BasicLockable lock = new BasicLockable(true, Dummy.class, false);
lock.lock(new DummMY());
}
private void checkIsLocked(ILockable lockable, boolean expected) {
if (lockable.isLocked() ^ expected) {
fail("Expect lock to be " + (expected ? "locked" : "not locked") + ", instead it's " + (expected ? "not locked." : "locked."));
}
}
@Test
public void testConstructorsIsLocked() {
// Default
checkIsLocked(new BasicLockable(), false);
//
checkIsLocked(new BasicLockable(new Dummy(), true, true), true);
checkIsLocked(new BasicLockable(new Dummy(), true, false), false);
//
checkIsLocked(new BasicLockable(true, Dummy.class, true), false);
//
checkIsLocked(new BasicLockable(new Dummy(), true, true, true), true);
checkIsLocked(new BasicLockable(new Dummy(), true, true, false), false);
//
checkIsLocked(new BasicLockable(true, true, true, true, true, void.class, false), false);
}
@Test
public void testUnlock() {
Dummy secret1 = new Dummy();
Dummy secret2 = new Dummy(secret1);
Dummy secret3 = new DummMY();
// secretIdentity
BasicLockable lock = new BasicLockable(secret2, true, true);
lock.unlock(secret2);
checkIsLocked(lock, false);
lock.lock();
// Unlock with different instance (equals).
lock = new BasicLockable(false, Dummy.class, false);
lock.lock(secret2);
lock.unlock(secret1);
checkIsLocked(lock, false);
// secretTypeExact
lock = new BasicLockable(false, Dummy.class, true);
lock.lock(secret1);
lock.unlock(secret1);
lock.lock(secret2);
lock.unlock(secret2);
// !secretTypeExact with sub class.
lock = new BasicLockable(false, Dummy.class, false);
lock.lock(secret1);
lock.unlock(secret1);
lock.lock(secret3);
lock.unlock(secret3);
}
@Test
public void testUnlockWithSecretRemoval() {
Dummy secret = new Dummy();
BasicLockable lock = new BasicLockable(null, true, false);
lock.lock(secret);
lock.unlock(secret);
secret = new DummMY();
lock.lock(secret);
lock.unlock(secret);
}
@Test(expected = IllegalArgumentException.class)
public void testUnlockAfterPermanentLockFail() {
Dummy secret = new Dummy();
BasicLockable lock = new BasicLockable(secret, true, true);
lock.lock();
lock.unlock(secret);
}
@Test(expected = IllegalArgumentException.class)
public void testUnlockWithWrongSecretFail() {
Dummy secret = new Dummy();
BasicLockable lock = new BasicLockable(secret, true, true);
lock.unlock(new Dummy());
}
@Test(expected = IllegalArgumentException.class)
public void testUnlockwithSubClassFail() {
BasicLockable lock = new BasicLockable(false, Dummy.class, true);
lock.lock(new Dummy());
lock.unlock(new Dummy());
lock.lock(new DummMY());
}
}