Implement some kind of support for locking of registered items.
(Or registry items.)
This commit is contained in:
parent
d3a66b01ba
commit
7b8390dd5a
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue