add-postgresql-instance-user-and-database-validation (#76)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/76 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@ -31,7 +31,8 @@ public final class HashGenerator {
|
||||
public enum Algorithm {
|
||||
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||
LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"),
|
||||
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*");
|
||||
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"),
|
||||
SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256");
|
||||
|
||||
final BiFunction<HashGenerator, String, String> implementation;
|
||||
final String prefix;
|
||||
|
@ -0,0 +1,61 @@
|
||||
package net.hostsharing.hsadminng.hash;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PostgreSQLScramSHA256 {
|
||||
|
||||
private static final String PBKDF_2_WITH_HMAC_SHA256 = "PBKDF2WithHmacSHA256";
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final String SHA256 = "SHA-256";
|
||||
private static final int ITERATIONS = 4096;
|
||||
public static final int KEY_LENGTH_IN_BITS = 256;
|
||||
|
||||
private static final PostgreSQLScramSHA256 scram = new PostgreSQLScramSHA256();
|
||||
|
||||
@SneakyThrows
|
||||
public static String hash(final HashGenerator generator, final String password) {
|
||||
if (generator.getSalt() == null) {
|
||||
throw new IllegalStateException("no salt given");
|
||||
}
|
||||
|
||||
final byte[] salt = generator.getSalt().getBytes(Charset.forName("latin1")); // Base64.getEncoder().encode(generator.getSalt().getBytes());
|
||||
final byte[] saltedPassword = scram.generateSaltedPassword(password, salt);
|
||||
final byte[] clientKey = scram.hmacSHA256(saltedPassword, "Client Key".getBytes());
|
||||
final byte[] storedKey = MessageDigest.getInstance(SHA256).digest(clientKey);
|
||||
final byte[] serverKey = scram.hmacSHA256(saltedPassword, "Server Key".getBytes());
|
||||
|
||||
return "SCRAM-SHA-256${iterations}:{base64EncodedSalt}${base64EncodedStoredKey}:{base64EncodedServerKey}"
|
||||
.replace("{iterations}", Integer.toString(ITERATIONS))
|
||||
.replace("{base64EncodedSalt}", base64(salt))
|
||||
.replace("{base64EncodedStoredKey}", base64(storedKey))
|
||||
.replace("{base64EncodedServerKey}", base64(serverKey));
|
||||
}
|
||||
|
||||
private static String base64(final byte[] salt) {
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
private byte[] generateSaltedPassword(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
final var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH_IN_BITS);
|
||||
return SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA256).generateSecret(spec).getEncoded();
|
||||
}
|
||||
|
||||
private byte[] hmacSHA256(byte[] key, byte[] message)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
final var mac = Mac.getInstance(HMAC_SHA256);
|
||||
mac.init(new SecretKeySpec(key, HMAC_SHA256));
|
||||
return mac.doFinal(message);
|
||||
}
|
||||
|
||||
}
|
@ -82,17 +82,16 @@ public enum HsHostingAssetType implements Node {
|
||||
|
||||
PGSQL_INSTANCE( // TODO.spec: identifier to be specified
|
||||
inGroup("PostgreSQL"),
|
||||
requiredParent(MANAGED_SERVER)),
|
||||
requiredParent(MANAGED_SERVER)), // TODO.spec: or MANAGED_WEBSPACE?
|
||||
|
||||
PGSQL_USER( // named e.g. xyz00_abc
|
||||
inGroup("PostgreSQL"),
|
||||
requiredParent(PGSQL_INSTANCE),
|
||||
assignedTo(MANAGED_WEBSPACE)),
|
||||
requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner
|
||||
assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied
|
||||
|
||||
PGSQL_DATABASE( // named e.g. xyz00_abc
|
||||
inGroup("PostgreSQL"),
|
||||
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or PGSQL_USER?
|
||||
assignedTo(PGSQL_INSTANCE)), // TODO.spec: or swapping parent+assignedTo?
|
||||
requiredParent(PGSQL_USER)), // thus, the PGSQL_USER_USER:Agent becomes RBAC owner
|
||||
|
||||
MARIADB_INSTANCE( // TODO.spec: identifier to be specified
|
||||
inGroup("MariaDB"),
|
||||
@ -101,12 +100,11 @@ public enum HsHostingAssetType implements Node {
|
||||
MARIADB_USER( // named e.g. xyz00_abc
|
||||
inGroup("MariaDB"),
|
||||
requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner
|
||||
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
|
||||
assignedTo(MARIADB_INSTANCE)),
|
||||
|
||||
MARIADB_DATABASE( // named e.g. xyz00_abc
|
||||
inGroup("MariaDB"),
|
||||
requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner
|
||||
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
|
||||
requiredParent(MARIADB_USER)), // thus, the MARIADB_USER:Agent becomes RBAC owner
|
||||
|
||||
IP_NUMBER(
|
||||
inGroup("Server"),
|
||||
|
@ -32,7 +32,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHo
|
||||
|
||||
HostingAssetEntityValidator(
|
||||
final HsHostingAssetType assetType,
|
||||
final AlarmContact alarmContactValidation,
|
||||
final AlarmContact alarmContactValidation, // hostmaster alert address is implicitly added where needed
|
||||
final ValidatableProperty<?, ?>... properties) {
|
||||
super(properties);
|
||||
this.bookingItemReferenceValidation = new ReferenceValidator<>(
|
||||
@ -213,6 +213,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHo
|
||||
super(policy, HsHostingAssetEntity::getAlarmContact);
|
||||
}
|
||||
|
||||
// hostmaster alert address is implicitly added where neccessary
|
||||
static AlarmContact isOptional() {
|
||||
return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL);
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ public class HostingAssetEntityValidatorRegistry {
|
||||
register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator());
|
||||
register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator());
|
||||
register(MARIADB_DATABASE, new HsMariaDbDatabaseHostingAssetValidator());
|
||||
register(PGSQL_INSTANCE, new HsPostgreSqlDbInstanceHostingAssetValidator());
|
||||
register(PGSQL_USER, new HsPostgreSqlUserHostingAssetValidator());
|
||||
register(PGSQL_DATABASE, new HsPostgreSqlDatabaseHostingAssetValidator());
|
||||
}
|
||||
|
||||
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {
|
||||
|
@ -14,7 +14,7 @@ class HsManagedServerHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
public HsManagedServerHostingAssetValidator() {
|
||||
super(
|
||||
MANAGED_SERVER,
|
||||
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
// monitoring
|
||||
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
|
||||
|
@ -10,7 +10,7 @@ class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator
|
||||
public HsManagedWebspaceHostingAssetValidator() {
|
||||
super(
|
||||
MANAGED_WEBSPACE,
|
||||
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
|
||||
AlarmContact.isOptional(),
|
||||
NO_EXTRA_PROPERTIES);
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,6 @@ class HsMariaDbDatabaseHostingAssetValidator extends HostingAssetEntityValidator
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
|
||||
final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier();
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$");
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class HsMariaDbInstanceHostingAssetValidator extends HostingAssetEntityValidator
|
||||
public HsMariaDbInstanceHostingAssetValidator() {
|
||||
super(
|
||||
MARIADB_INSTANCE,
|
||||
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
|
||||
AlarmContact.isOptional(),
|
||||
NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,6 @@ class HsMariaDbUserHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
|
||||
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE;
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
class HsPostgreSqlDatabaseHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
public HsPostgreSqlDatabaseHostingAssetValidator() {
|
||||
super(
|
||||
PGSQL_DATABASE,
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
stringProperty("encoding").matchesRegEx("[A-Z0-9_]+").maxLength(24).provided("LATIN1", "UTF8").withDefault("UTF8")
|
||||
|
||||
// TODO.spec: PostgreSQL extensions in instance and here? also decide which. Free selection or booleans/checkboxes?
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
|
||||
final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier();
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$");
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE;
|
||||
|
||||
class HsPostgreSqlDbInstanceHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
final static String DEFAULT_INSTANCE_IDENTIFIER_SUFFIX = "|PgSql.default"; // TODO.spec: specify instance naming
|
||||
|
||||
public HsPostgreSqlDbInstanceHostingAssetValidator() {
|
||||
super(
|
||||
PGSQL_DATABASE,
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
// TODO.spec: PostgreSQL extensions in database and here? also decide which. Free selection or booleans/checkboxes?
|
||||
NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
|
||||
return Pattern.compile(
|
||||
"^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier()
|
||||
+ DEFAULT_INSTANCE_IDENTIFIER_SUFFIX)
|
||||
+ "$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessEntity(final HsHostingAssetEntity entity) {
|
||||
super.preprocessEntity(entity);
|
||||
if (entity.getIdentifier() == null) {
|
||||
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(
|
||||
pa.getIdentifier() + DEFAULT_INSTANCE_IDENTIFIER_SUFFIX));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||
|
||||
import net.hostsharing.hsadminng.hash.HashGenerator;
|
||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER;
|
||||
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
|
||||
|
||||
class HsPostgreSqlUserHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
public HsPostgreSqlUserHostingAssetValidator() {
|
||||
super(
|
||||
PGSQL_USER,
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
// TODO.impl: we need to be able to suppress updating of fields etc., something like this:
|
||||
// withFieldValidation(
|
||||
// referenceProperty(alarmContact).isOptional(),
|
||||
// referenceProperty(parentAsset).isWriteOnce(),
|
||||
// referenceProperty(assignedToAsset).isWriteOnce(),
|
||||
// );
|
||||
|
||||
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashGenerator.Algorithm.SCRAM_SHA256).writeOnly());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
|
||||
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
|
||||
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user