diff --git a/docs/content/development/extensions-core/druid-basic-security.md b/docs/content/development/extensions-core/druid-basic-security.md index 2dfb4834718d..d89a14025024 100644 --- a/docs/content/development/extensions-core/druid-basic-security.md +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -16,13 +16,18 @@ Please see [Authentication and Authorization](../../configuration/auth.html) for The examples in the section will use "MyBasicAuthenticator" and "MyBasicAuthorizer" as names for the Authenticator and Authorizer. +These properties are not tied to specific Authenticator or Authorizer instances. + +These configuration properties should be added to the common runtime properties file. + ### Properties |Property|Description|Default|required| |--------|-----------|-------|--------| |`druid.auth.basic.common.pollingPeriod`|Defines in milliseconds how often nodes should poll the coordinator for the current authenticator/authorizer database state.|60000|No| |`druid.auth.basic.common.maxRandomDelay`|Defines in milliseconds the amount of random delay to add to the pollingPeriod, to spread polling requests across time.|6000|No| +|`druid.auth.basic.common.maxSyncRetries`|Determines how many times a service will retry if the authentication/authorization database state sync with the coordinator fails.|10|No| +|`druid.auth.basic.common.cacheDirectory`|If defined, snapshots of the basic Authenticator and Authorizer database caches will be stored on disk in this directory. If this property is defined, when a service is starting, it will attempt to initialize its caches from these on-disk snapshots, if the service is unable to initialize its state by communicating with the coordinator.|null|No| -These properties are not tied to specific Authenticator or Authorizer instances. ### Creating an Authenticator ``` @@ -48,13 +53,13 @@ The configuration examples in the rest of this document will use "MyBasicAuthent #### Properties |Property|Description|Default|required| |--------|-----------|-------|--------| -|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created.|null|No| -|`druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword`|Initial password for the default internal system user, used for internal node communication. If no password is specified, the default internal system user will not be created.|null|No| +|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created. If the default admin user already exists, setting this property will affect its password.|null|No| +|`druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword`|Initial password for the default internal system user, used for internal node communication. If no password is specified, the default internal system user will not be created. If the default internal system user already exists, setting this property will affect its password.|null|No| |`druid.auth.authenticator.MyBasicAuthenticator.enableCacheNotifications`|If true, the coordinator will notify Druid nodes whenever a configuration change to this Authenticator occurs, allowing them to immediately update their state without waiting for polling.|true|No| |`druid.auth.authenticator.MyBasicAuthenticator.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No| +|`druid.auth.authenticator.MyBasicAuthenticator.credentialIterations`|Number of iterations to use for password hashing.|10000|No| |`druid.auth.authenticator.MyBasicAuthenticator.authorizerName`|Authorizer that requests should be directed to|N/A|Yes| -enableCacheNotifications ### Creating an Escalator ``` @@ -106,22 +111,33 @@ Root path: `/druid-ext/basic-security/authentication` Each API endpoint includes {authenticatorName}, specifying which Authenticator instance is being configured. ##### User/Credential Management -`GET(/{authenticatorName}/users)` +`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users)` Return a list of all user names. -`GET(/{authenticatorName}/users/{userName})` +`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` Return the name and credentials information of the user with name {userName} -`POST(/{authenticatorName}/users/{userName})` +`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` Create a new user with name {userName} -`DELETE(/{authenticatorName}/users/{userName})` +`DELETE(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` Delete the user with name {userName} -`POST(/{authenticatorName}/users/{userName}/credentials)` +`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName}/credentials)` Assign a password used for HTTP basic authentication for {userName} -Content: password string +Content: JSON password request object +Example request body: + +``` +{ + "password": "helloworld" +} +``` + +##### Cache Load Status +`GET(/druid-ext/basic-security/authentication/loadStatus)` +Return the current load status of the local caches of the authentication database. #### Authorization API @@ -130,58 +146,58 @@ Root path: `/druid-ext/basic-security/authorization` Each API endpoint includes {authorizerName}, specifying which Authorizer instance is being configured. ##### User Creation/Deletion -`GET(/{authorizerName}/users)` +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users)` Return a list of all user names. -`GET(/{authorizerName}/users/{userName})` -Return the name and credentials information of the user with name {userName} +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` +Return the name and role information of the user with name {userName} -`POST(/{authorizerName}/users/{userName})` +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` Create a new user with name {userName} -`DELETE(/{authorizerName}/users/{userName})` +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` Delete the user with name {userName} #### Role Creation/Deletion -`GET(/{authorizerName}/roles)` +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles)` Return a list of all role names. -`GET(/{authorizerName}/roles/{roleName})` +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` Return name and permissions for the role named {roleName} -`POST(/{authorizerName}/roles/{roleName})` +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` Create a new role with name {roleName}. Content: username string -`DELETE(/{authorizerName}/roles/{roleName})` +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` Delete the role with name {roleName}. #### Role Assignment -`POST(/{authorizerName}/users/{userName}/roles/{roleName})` +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})` Assign role {roleName} to user {userName}. -`DELETE(/{authorizerName}/users/{userName}/roles/{roleName})` +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})` Unassign role {roleName} from user {userName} #### Permissions -`POST(/{authorizerName}/roles/{roleName}/permissions)` +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName}/permissions)` Set the permissions of {roleName}. This replaces the previous set of permissions on the role. Content: List of JSON Resource-Action objects, e.g.: ``` [ { - resource": { + "resource": { "name": "wiki.*", "type": "DATASOURCE" }, "action": "READ" }, { - resource": { + "resource": { "name": "wikiticker", "type": "DATASOURCE" }, @@ -194,6 +210,10 @@ The "name" field for resources in the permission definitions are regexes used to Please see [Defining permissions](#defining-permissions) for more details. +##### Cache Load Status +`GET(/druid-ext/basic-security/authorization/loadStatus)` +Return the current load status of the local caches of the authorization database. + ## Default user accounts ### Authenticator diff --git a/extensions-core/datasketches/src/main/java/io/druid/query/aggregation/datasketches/quantiles/DoublesSketchComplexMetricSerde.java b/extensions-core/datasketches/src/main/java/io/druid/query/aggregation/datasketches/quantiles/DoublesSketchComplexMetricSerde.java index fa35700c6030..020b5e61b334 100644 --- a/extensions-core/datasketches/src/main/java/io/druid/query/aggregation/datasketches/quantiles/DoublesSketchComplexMetricSerde.java +++ b/extensions-core/datasketches/src/main/java/io/druid/query/aggregation/datasketches/quantiles/DoublesSketchComplexMetricSerde.java @@ -27,12 +27,12 @@ import io.druid.segment.GenericColumnSerializer; import io.druid.segment.column.ColumnBuilder; import io.druid.segment.data.GenericIndexed; -import io.druid.segment.data.IOPeon; import io.druid.segment.data.ObjectStrategy; import io.druid.segment.serde.ComplexColumnPartSupplier; import io.druid.segment.serde.ComplexMetricExtractor; import io.druid.segment.serde.ComplexMetricSerde; import io.druid.segment.serde.LargeColumnSupportedComplexColumnSerializer; +import io.druid.segment.writeout.SegmentWriteOutMedium; import java.nio.ByteBuffer; @@ -109,9 +109,9 @@ public void deserializeColumn(final ByteBuffer buffer, final ColumnBuilder build // support large columns @Override - public GenericColumnSerializer getSerializer(IOPeon peon, String column) + public GenericColumnSerializer getSerializer(SegmentWriteOutMedium segmentWriteOutMedium, String column) { - return LargeColumnSupportedComplexColumnSerializer.create(peon, column, this.getObjectStrategy()); + return LargeColumnSupportedComplexColumnSerializer.create(segmentWriteOutMedium, column, this.getObjectStrategy()); } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthCommonCacheConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthCommonCacheConfig.java index e6d2becc6268..c16314b0a656 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthCommonCacheConfig.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthCommonCacheConfig.java @@ -26,6 +26,7 @@ public class BasicAuthCommonCacheConfig { private static final long DEFAULT_POLLING_PERIOD = 60000; private static final long DEFAULT_MAX_RANDOM_DELAY = DEFAULT_POLLING_PERIOD / 10; + private static final int DEFAULT_MAX_SYNC_RETRIES = 10; @JsonProperty private final long pollingPeriod; @@ -33,14 +34,24 @@ public class BasicAuthCommonCacheConfig @JsonProperty private final long maxRandomDelay; + @JsonProperty + private final String cacheDirectory; + + @JsonProperty + private final int maxSyncRetries; + @JsonCreator public BasicAuthCommonCacheConfig( @JsonProperty("pollingPeriod") Long pollingPeriod, - @JsonProperty("maxRandomDelay") Long maxRandomDelay + @JsonProperty("maxRandomDelay") Long maxRandomDelay, + @JsonProperty("cacheDirectory") String cacheDirectory, + @JsonProperty("maxSyncRetries") Integer maxSyncRetries ) { this.pollingPeriod = pollingPeriod == null ? DEFAULT_POLLING_PERIOD : pollingPeriod; this.maxRandomDelay = maxRandomDelay == null ? DEFAULT_MAX_RANDOM_DELAY : maxRandomDelay; + this.cacheDirectory = cacheDirectory; + this.maxSyncRetries = maxSyncRetries == null ? DEFAULT_MAX_SYNC_RETRIES : maxSyncRetries; } @JsonProperty @@ -54,4 +65,16 @@ public long getMaxRandomDelay() { return maxRandomDelay; } + + @JsonProperty + public String getCacheDirectory() + { + return cacheDirectory; + } + + @JsonProperty + public int getMaxSyncRetries() + { + return maxSyncRetries; + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthDBConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthDBConfig.java index ef05d4f65fdc..6a9727626963 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthDBConfig.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthDBConfig.java @@ -27,18 +27,21 @@ public class BasicAuthDBConfig private final String initialInternalClientPassword; private final boolean enableCacheNotifications; private final long cacheNotificationTimeout; + private final int iterations; public BasicAuthDBConfig( final String initialAdminPassword, final String initialInternalClientPassword, final Boolean enableCacheNotifications, - final Long cacheNotificationTimeout + final Long cacheNotificationTimeout, + final int iterations ) { this.initialAdminPassword = initialAdminPassword; this.initialInternalClientPassword = initialInternalClientPassword; this.enableCacheNotifications = enableCacheNotifications; this.cacheNotificationTimeout = cacheNotificationTimeout; + this.iterations = iterations; } public String getInitialAdminPassword() @@ -60,4 +63,9 @@ public long getCacheNotificationTimeout() { return cacheNotificationTimeout; } + + public int getIterations() + { + return iterations; + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java index ee16a7dfa684..2c60532f794b 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java @@ -19,35 +19,70 @@ package io.druid.security.basic; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; +import io.druid.java.util.common.ISE; import io.druid.java.util.common.StringUtils; import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.security.basic.authorization.entity.UserAndRoleMap; +import javax.annotation.Nullable; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import javax.servlet.http.HttpServletRequest; +import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.util.Base64; +import java.util.Map; public class BasicAuthUtils { + private static final Logger log = new Logger(BasicAuthUtils.class); - private static final Base64.Encoder ENCODER = Base64.getEncoder(); - private static final Base64.Decoder DECODER = Base64.getDecoder(); private static final SecureRandom SECURE_RANDOM = new SecureRandom(); public final static String ADMIN_NAME = "admin"; public final static String INTERNAL_USER_NAME = "druid_system"; - public static int SALT_LENGTH = 32; - public static int KEY_ITERATIONS = 10000; - public static int KEY_LENGTH = 512; - public static String ALGORITHM = "PBKDF2WithHmacSHA512"; + // PBKDF2WithHmacSHA512 is chosen since it has built-in support in Java8. + // Argon2 (https://github.com/p-h-c/phc-winner-argon2) is newer but the only presently + // available Java binding is LGPLv3 licensed. + // Key length is 512-bit to match the PBKDF2WithHmacSHA512 algorithm. + // 256-bit salt should be more than sufficient for uniqueness, expected user count is on the order of thousands. + public final static int SALT_LENGTH = 32; + public final static int DEFAULT_KEY_ITERATIONS = 10000; + public final static int KEY_LENGTH = 512; + public final static String ALGORITHM = "PBKDF2WithHmacSHA512"; + + public static final TypeReference AUTHENTICATOR_USER_MAP_TYPE_REFERENCE = + new TypeReference>() + { + }; + + public static final TypeReference AUTHORIZER_USER_MAP_TYPE_REFERENCE = + new TypeReference>() + { + }; + + public static final TypeReference AUTHORIZER_ROLE_MAP_TYPE_REFERENCE = + new TypeReference>() + { + }; + + public static final TypeReference AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE = + new TypeReference() + { + }; public static String getEncodedCredentials(final String unencodedCreds) { - return ENCODER.encodeToString(StringUtils.toUtf8(unencodedCreds)); + return Base64.getEncoder().encodeToString(StringUtils.toUtf8(unencodedCreds)); } public static byte[] hashPassword(final char[] password, final byte[] salt, final int iterations) @@ -66,11 +101,11 @@ public static byte[] hashPassword(final char[] password, final byte[] salt, fina } catch (InvalidKeySpecException ikse) { log.error("WTF? invalid keyspec"); - throw new RuntimeException(ikse); + throw new RuntimeException("WTF? invalid keyspec", ikse); } catch (NoSuchAlgorithmException nsae) { log.error("%s not supported on this system.", ALGORITHM); - throw new RuntimeException(nsae); + throw new RuntimeException(StringUtils.format("%s not supported on this system.", ALGORITHM), nsae); } } @@ -81,24 +116,120 @@ public static byte[] generateSalt() return salt; } + @Nullable public static String getBasicUserSecretFromHttpReq(HttpServletRequest httpReq) { + String authHeader = httpReq.getHeader("Authorization"); + + if (authHeader == null) { + return null; + } + + if (authHeader.length() < 7) { + return null; + } + + if (!authHeader.substring(0, 6).equals("Basic ")) { + return null; + } + + String encodedUserSecret = authHeader.substring(6); + try { - String authHeader = httpReq.getHeader("Authorization"); + return StringUtils.fromUtf8(Base64.getDecoder().decode(encodedUserSecret)); + } + catch (IllegalArgumentException iae) { + return null; + } + } - if (authHeader == null) { - return null; + public static Map deserializeAuthenticatorUserMap( + ObjectMapper objectMapper, + byte[] userMapBytes + ) + { + Map userMap; + if (userMapBytes == null) { + userMap = Maps.newHashMap(); + } else { + try { + userMap = objectMapper.readValue(userMapBytes, AUTHENTICATOR_USER_MAP_TYPE_REFERENCE); + } + catch (IOException ioe) { + throw new RuntimeException(ioe); } + } + return userMap; + } - if (!authHeader.substring(0, 6).equals("Basic ")) { - return null; + public static byte[] serializeAuthenticatorUserMap( + ObjectMapper objectMapper, + Map userMap + ) + { + try { + return objectMapper.writeValueAsBytes(userMap); + } + catch (IOException ioe) { + throw new ISE("WTF? Couldn't serialize userMap!"); + } + } + + public static Map deserializeAuthorizerUserMap( + ObjectMapper objectMapper, + byte[] userMapBytes + ) + { + Map userMap; + if (userMapBytes == null) { + userMap = Maps.newHashMap(); + } else { + try { + userMap = objectMapper.readValue(userMapBytes, BasicAuthUtils.AUTHORIZER_USER_MAP_TYPE_REFERENCE); } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + return userMap; + } - String encodedUserSecret = authHeader.substring(6); - return StringUtils.fromUtf8(DECODER.decode(encodedUserSecret)); + public static byte[] serializeAuthorizerUserMap(ObjectMapper objectMapper, Map userMap) + { + try { + return objectMapper.writeValueAsBytes(userMap); } - catch (Exception e) { - return null; + catch (IOException ioe) { + throw new ISE("WTF? Couldn't serialize userMap!"); + } + } + + public static Map deserializeAuthorizerRoleMap( + ObjectMapper objectMapper, + byte[] roleMapBytes + ) + { + Map roleMap; + if (roleMapBytes == null) { + roleMap = Maps.newHashMap(); + } else { + try { + roleMap = objectMapper.readValue(roleMapBytes, BasicAuthUtils.AUTHORIZER_ROLE_MAP_TYPE_REFERENCE); + } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + return roleMap; + } + + public static byte[] serializeAuthorizerRoleMap(ObjectMapper objectMapper, Map roleMap) + { + try { + return objectMapper.writeValueAsBytes(roleMap); + } + catch (IOException ioe) { + throw new ISE("WTF? Couldn't serialize roleMap!"); } } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java index 6b95308739d5..17ae325174dc 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java @@ -22,7 +22,7 @@ import io.druid.java.util.common.StringUtils; /** - * Throw this in the BasicSecurityStorageConnectors for invalid resource accesses that are likely a result of user error + * Throw this for invalid resource accesses in the druid-basic-security extension that are likely a result of user error * (e.g., entry not found, duplicate entries). */ public class BasicSecurityDBResourceException extends IllegalArgumentException @@ -31,4 +31,9 @@ public BasicSecurityDBResourceException(String formatText, Object... arguments) { super(StringUtils.nonStrictFormat(formatText, arguments)); } + + public BasicSecurityDBResourceException(Throwable t, String formatText, Object... arguments) + { + super(StringUtils.nonStrictFormat(formatText, arguments), t); + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java index 6fcaedc00b1e..8e14d8770bee 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java @@ -36,13 +36,11 @@ import io.druid.security.basic.authentication.BasicHTTPEscalator; import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager; import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier; -import io.druid.security.basic.authentication.db.cache.CoordinatorBasicAuthenticatorCacheManager; import io.druid.security.basic.authentication.db.cache.CoordinatorBasicAuthenticatorCacheNotifier; -import io.druid.security.basic.authentication.db.cache.DefaultBasicAuthenticatorCacheManager; -import io.druid.security.basic.authentication.db.cache.NoopBasicAuthenticatorCacheNotifier; +import io.druid.security.basic.authentication.db.cache.CoordinatorPollingBasicAuthenticatorCacheManager; +import io.druid.security.basic.authentication.db.cache.MetadataStoragePollingBasicAuthenticatorCacheManager; import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater; import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater; -import io.druid.security.basic.authentication.db.updater.NoopBasicAuthenticatorMetadataStorageUpdater; import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResource; import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResourceHandler; import io.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler; @@ -50,13 +48,11 @@ import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager; import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier; -import io.druid.security.basic.authorization.db.cache.CoordinatorBasicAuthorizerCacheManager; import io.druid.security.basic.authorization.db.cache.CoordinatorBasicAuthorizerCacheNotifier; -import io.druid.security.basic.authorization.db.cache.DefaultBasicAuthorizerCacheManager; -import io.druid.security.basic.authorization.db.cache.NoopBasicAuthorizerCacheNotifier; +import io.druid.security.basic.authorization.db.cache.CoordinatorPollingBasicAuthorizerCacheManager; +import io.druid.security.basic.authorization.db.cache.MetadataStoragePollingBasicAuthorizerCacheManager; import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater; import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; -import io.druid.security.basic.authorization.db.updater.NoopBasicAuthorizerMetadataStorageUpdater; import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResource; import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResourceHandler; import io.druid.security.basic.authorization.endpoint.CoordinatorBasicAuthorizerResourceHandler; @@ -86,7 +82,7 @@ public static BasicAuthenticatorMetadataStorageUpdater createAuthenticatorStorag if (isCoordinator(injector)) { return injector.getInstance(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class); } else { - return injector.getInstance(NoopBasicAuthenticatorMetadataStorageUpdater.class); + return null; } } @@ -94,9 +90,9 @@ public static BasicAuthenticatorMetadataStorageUpdater createAuthenticatorStorag public static BasicAuthenticatorCacheManager createAuthenticatorCacheManager(final Injector injector) { if (isCoordinator(injector)) { - return injector.getInstance(CoordinatorBasicAuthenticatorCacheManager.class); + return injector.getInstance(MetadataStoragePollingBasicAuthenticatorCacheManager.class); } else { - return injector.getInstance(DefaultBasicAuthenticatorCacheManager.class); + return injector.getInstance(CoordinatorPollingBasicAuthenticatorCacheManager.class); } } @@ -116,7 +112,7 @@ public static BasicAuthenticatorCacheNotifier createAuthenticatorCacheNotifier(f if (isCoordinator(injector)) { return injector.getInstance(CoordinatorBasicAuthenticatorCacheNotifier.class); } else { - return injector.getInstance(NoopBasicAuthenticatorCacheNotifier.class); + return null; } } @@ -126,7 +122,7 @@ public static BasicAuthorizerMetadataStorageUpdater createAuthorizerStorageUpdat if (isCoordinator(injector)) { return injector.getInstance(CoordinatorBasicAuthorizerMetadataStorageUpdater.class); } else { - return injector.getInstance(NoopBasicAuthorizerMetadataStorageUpdater.class); + return null; } } @@ -134,9 +130,9 @@ public static BasicAuthorizerMetadataStorageUpdater createAuthorizerStorageUpdat public static BasicAuthorizerCacheManager createAuthorizerCacheManager(final Injector injector) { if (isCoordinator(injector)) { - return injector.getInstance(CoordinatorBasicAuthorizerCacheManager.class); + return injector.getInstance(MetadataStoragePollingBasicAuthorizerCacheManager.class); } else { - return injector.getInstance(DefaultBasicAuthorizerCacheManager.class); + return injector.getInstance(CoordinatorPollingBasicAuthorizerCacheManager.class); } } @@ -156,7 +152,7 @@ public static BasicAuthorizerCacheNotifier createAuthorizerCacheNotifier(final I if (isCoordinator(injector)) { return injector.getInstance(CoordinatorBasicAuthorizerCacheNotifier.class); } else { - return injector.getInstance(NoopBasicAuthorizerCacheNotifier.class); + return null; } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java index cd33ee21c3d2..733bcc50baf7 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -37,12 +37,13 @@ public class BasicSecurityResourceFilter extends AbstractResourceFilter { - private static final List applicablePaths = ImmutableList.of( - "druid/coordinator/v1/security/*", - "druid/security/internal/authentication/*", - "druid/basic-security/authentication/*" + private static final List APPLICABLE_PATHS = ImmutableList.of( + "/druid-ext/basic-security/authentication", + "/druid-ext/basic-security/authorization" ); + private static final String SECURITY_RESOURCE_NAME = "security"; + @Inject public BasicSecurityResourceFilter( AuthorizerMapper authorizerMapper @@ -55,7 +56,7 @@ public BasicSecurityResourceFilter( public ContainerRequest filter(ContainerRequest request) { final ResourceAction resourceAction = new ResourceAction( - new Resource("security", ResourceType.CONFIG), + new Resource(SECURITY_RESOURCE_NAME, ResourceType.CONFIG), getAction(request) ); @@ -79,7 +80,7 @@ public ContainerRequest filter(ContainerRequest request) @Override public boolean isApplicable(String requestPath) { - for (String path : applicablePaths) { + for (String path : APPLICABLE_PATHS) { if (requestPath.startsWith(path) && !requestPath.equals(path)) { return true; } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/CommonCacheNotifier.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/CommonCacheNotifier.java new file mode 100644 index 000000000000..d93294335308 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/CommonCacheNotifier.java @@ -0,0 +1,238 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic; + +import com.google.common.util.concurrent.ListenableFuture; +import com.metamx.emitter.EmittingLogger; +import com.metamx.http.client.HttpClient; +import com.metamx.http.client.Request; +import com.metamx.http.client.response.ClientResponse; +import com.metamx.http.client.response.HttpResponseHandler; +import com.metamx.http.client.response.StatusResponseHolder; +import io.druid.discovery.DiscoveryDruidNode; +import io.druid.discovery.DruidNodeDiscovery; +import io.druid.discovery.DruidNodeDiscoveryProvider; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.concurrent.Execs; +import io.druid.server.DruidNode; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.joda.time.Duration; + +import javax.ws.rs.core.MediaType; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class CommonCacheNotifier +{ + private static final List NODE_TYPES = Arrays.asList( + DruidNodeDiscoveryProvider.NODE_TYPE_BROKER, + DruidNodeDiscoveryProvider.NODE_TYPE_OVERLORD, + DruidNodeDiscoveryProvider.NODE_TYPE_HISTORICAL, + DruidNodeDiscoveryProvider.NODE_TYPE_PEON, + DruidNodeDiscoveryProvider.NODE_TYPE_ROUTER, + DruidNodeDiscoveryProvider.NODE_TYPE_MM + ); + + private final DruidNodeDiscoveryProvider discoveryProvider; + private final HttpClient httpClient; + private final Set itemsToUpdate; + private final Map serializedMaps; + private final Map itemConfigMap; + private final String baseUrl; + private final EmittingLogger logger; + private final String threadName; + + private Thread notifierThread; + + public CommonCacheNotifier( + Map itemConfigMap, + DruidNodeDiscoveryProvider discoveryProvider, + HttpClient httpClient, + String baseUrl, + String threadName, + EmittingLogger logger + ) + { + this.threadName = threadName; + this.logger = logger; + this.itemsToUpdate = new HashSet<>(); + this.itemConfigMap = itemConfigMap; + this.serializedMaps = new HashMap<>(); + this.discoveryProvider = discoveryProvider; + this.httpClient = httpClient; + this.baseUrl = baseUrl; + } + + public void start() + { + notifierThread = Execs.makeThread( + threadName, + () -> { + while (!Thread.interrupted()) { + try { + logger.debug("Waiting for cache update notification"); + Set itemsToUpdateSnapshot; + HashMap serializedMapsSnapshot; + synchronized (itemsToUpdate) { + if (itemsToUpdate.isEmpty()) { + itemsToUpdate.wait(); + } + itemsToUpdateSnapshot = new HashSet<>(itemsToUpdate); + serializedMapsSnapshot = new HashMap(serializedMaps); + itemsToUpdate.clear(); + serializedMaps.clear(); + } + logger.debug("Sending cache update notifications"); + for (String authorizer : itemsToUpdateSnapshot) { + BasicAuthDBConfig authorizerConfig = itemConfigMap.get(authorizer); + if (!authorizerConfig.isEnableCacheNotifications()) { + continue; + } + + // Best effort, if a notification fails, the remote node will eventually poll to update its state + // We wait for responses however, to avoid flooding remote nodes with notifications. + List> futures = sendUpdate( + authorizer, + serializedMapsSnapshot.get(authorizer) + ); + for (ListenableFuture future : futures) { + try { + StatusResponseHolder srh = future.get( + authorizerConfig.getCacheNotificationTimeout(), TimeUnit.MILLISECONDS + ); + logger.debug("Got status: " + srh.getStatus()); + } + catch (Exception e) { + logger.makeAlert(e, "Failed to get response for cache notification.").emit(); + } + } + } + logger.debug("Received responses for cache update notifications."); + } + catch (Throwable t) { + logger.makeAlert(t, "Error occured while handling updates for cachedUserMaps.").emit(); + } + } + }, + true + ); + notifierThread.start(); + } + + public void addUpdate(String updatedItemName, byte[] updatedItemData) + { + synchronized (itemsToUpdate) { + itemsToUpdate.add(updatedItemName); + serializedMaps.put(updatedItemName, updatedItemData); + itemsToUpdate.notify(); + } + } + + private List> sendUpdate(String updatedAuthorizerPrefix, byte[] serializedUserMap) + { + List> futures = new ArrayList<>(); + for (String nodeType : NODE_TYPES) { + DruidNodeDiscovery nodeDiscovery = discoveryProvider.getForNodeType(nodeType); + Collection nodes = nodeDiscovery.getAllNodes(); + for (DiscoveryDruidNode node : nodes) { + URL listenerURL = getListenerURL(node.getDruidNode(), baseUrl, updatedAuthorizerPrefix); + + // best effort, if this fails, remote node will poll and pick up the update eventually + Request req = new Request(HttpMethod.POST, listenerURL); + req.setContent(MediaType.APPLICATION_JSON, serializedUserMap); + + BasicAuthDBConfig itemConfig = itemConfigMap.get(updatedAuthorizerPrefix); + + ListenableFuture future = httpClient.go( + req, + new ResponseHandler(), + Duration.millis(itemConfig.getCacheNotificationTimeout()) + ); + futures.add(future); + } + } + return futures; + } + + private URL getListenerURL(DruidNode druidNode, String baseUrl, String itemName) + { + try { + return new URL( + druidNode.getServiceScheme(), + druidNode.getHost(), + druidNode.getPortToUse(), + StringUtils.format(baseUrl, itemName) + ); + } + catch (MalformedURLException mue) { + logger.error("WTF? Malformed url for DruidNode[%s] and itemName[%s]", druidNode, itemName); + throw new RuntimeException(mue); + } + } + + // Based off StatusResponseHandler, but with response content ignored + private static class ResponseHandler implements HttpResponseHandler + { + @Override + public ClientResponse handleResponse(HttpResponse response) + { + return ClientResponse.unfinished( + new StatusResponseHolder( + response.getStatus(), + null + ) + ); + } + + @Override + public ClientResponse handleChunk( + ClientResponse response, + HttpChunk chunk + ) + { + return response; + } + + @Override + public ClientResponse done(ClientResponse response) + { + return ClientResponse.finished(response.getObj()); + } + + @Override + public void exceptionCaught( + ClientResponse clientResponse, Throwable e + ) + { + // Its safe to Ignore as the ClientResponse returned in handleChunk were unfinished + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java index 11973913180a..b127687999bd 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java @@ -25,8 +25,8 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.inject.Provider; import io.druid.java.util.common.IAE; -import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicAuthDBConfig; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager; import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; @@ -65,7 +65,8 @@ public BasicHTTPAuthenticator( @JsonProperty("initialAdminPassword") String initialAdminPassword, @JsonProperty("initialInternalClientPassword") String initialInternalClientPassword, @JsonProperty("enableCacheNotifications") Boolean enableCacheNotifications, - @JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout + @JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout, + @JsonProperty("credentialIterations") Integer credentialIterations ) { this.name = name; @@ -74,7 +75,8 @@ public BasicHTTPAuthenticator( initialAdminPassword, initialInternalClientPassword, enableCacheNotifications == null ? true : enableCacheNotifications, - cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout + cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout, + credentialIterations == null ? BasicAuthUtils.DEFAULT_KEY_ITERATIONS : credentialIterations ); this.cacheManager = cacheManager; } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheManager.java index 0aecd812dfb0..d5742e106686 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheManager.java @@ -23,9 +23,25 @@ import java.util.Map; +/** + * This class is reponsible for maintaining a cache of the authenticator database state. The BasicHTTPAuthenticator + * uses an injected BasicAuthenticatorCacheManager to make its authentication decisions. + */ public interface BasicAuthenticatorCacheManager { + /** + * Update this cache manager's local state with fresh information pushed by the coordinator. + * + * @param authenticatorPrefix The name of the authenticator this update applies to. + * @param serializedUserMap The updated, serialized user map + */ void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap); + /** + * Return the cache manager's local view of the user map for the authenticator named `authenticatorPrefix`. + * + * @param authenticatorPrefix The name of the authenticator + * @return User map + */ Map getUserMap(String authenticatorPrefix); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheNotifier.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheNotifier.java index 92bb9bccf48f..b9b7901292c5 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheNotifier.java @@ -19,7 +19,16 @@ package io.druid.security.basic.authentication.db.cache; +/** + * Sends a notification to druid services, containing updated authenticator user map state. + */ public interface BasicAuthenticatorCacheNotifier { + /** + * Send the user map state contained in updatedUserMap to all non-coordinator Druid services + * + * @param updatedAuthenticatorPrefix Name of authenticator being updated + * @param updatedUserMap User map state + */ void addUpdate(String updatedAuthenticatorPrefix, byte[] updatedUserMap); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheNotifier.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheNotifier.java index c35577859444..d9499f354dca 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheNotifier.java @@ -20,45 +20,23 @@ package io.druid.security.basic.authentication.db.cache; import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.Inject; import com.metamx.emitter.EmittingLogger; import com.metamx.http.client.HttpClient; -import com.metamx.http.client.Request; -import com.metamx.http.client.response.ClientResponse; -import com.metamx.http.client.response.HttpResponseHandler; -import com.metamx.http.client.response.StatusResponseHolder; import io.druid.concurrent.LifecycleLock; -import io.druid.discovery.DiscoveryDruidNode; -import io.druid.discovery.DruidNodeDiscovery; import io.druid.discovery.DruidNodeDiscoveryProvider; import io.druid.guice.ManageLifecycle; import io.druid.guice.annotations.EscalatedClient; import io.druid.java.util.common.ISE; -import io.druid.java.util.common.StringUtils; -import io.druid.java.util.common.concurrent.Execs; import io.druid.java.util.common.lifecycle.LifecycleStart; -import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.BasicAuthDBConfig; -import io.druid.server.DruidNode; +import io.druid.security.basic.CommonCacheNotifier; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.server.security.Authenticator; import io.druid.server.security.AuthenticatorMapper; -import org.jboss.netty.handler.codec.http.HttpChunk; -import org.jboss.netty.handler.codec.http.HttpMethod; -import org.jboss.netty.handler.codec.http.HttpResponse; -import org.joda.time.Duration; -import javax.ws.rs.core.MediaType; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; @ManageLifecycle @@ -66,23 +44,8 @@ public class CoordinatorBasicAuthenticatorCacheNotifier implements BasicAuthenti { private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthenticatorCacheNotifier.class); - private static final List NODE_TYPES = Arrays.asList( - DruidNodeDiscoveryProvider.NODE_TYPE_BROKER, - DruidNodeDiscoveryProvider.NODE_TYPE_OVERLORD, - DruidNodeDiscoveryProvider.NODE_TYPE_HISTORICAL, - DruidNodeDiscoveryProvider.NODE_TYPE_PEON, - DruidNodeDiscoveryProvider.NODE_TYPE_ROUTER, - DruidNodeDiscoveryProvider.NODE_TYPE_MM - ); - - private final DruidNodeDiscoveryProvider discoveryProvider; - private final HttpClient httpClient; - private final Set authenticatorsToUpdate; - private final Map serializedMaps; - private final Map authenticatorConfigMap; private final LifecycleLock lifecycleLock = new LifecycleLock(); - - private Thread notifierThread; + private CommonCacheNotifier cacheNotifier; @Inject public CoordinatorBasicAuthenticatorCacheNotifier( @@ -91,12 +54,14 @@ public CoordinatorBasicAuthenticatorCacheNotifier( @EscalatedClient HttpClient httpClient ) { - this.discoveryProvider = discoveryProvider; - this.httpClient = httpClient; - this.authenticatorsToUpdate = new HashSet<>(); - this.serializedMaps = new HashMap<>(); - this.authenticatorConfigMap = new HashMap<>(); - initAuthenticatorConfigMap(authenticatorMapper); + cacheNotifier = new CommonCacheNotifier( + initAuthenticatorConfigMap(authenticatorMapper), + discoveryProvider, + httpClient, + "/druid-ext/basic-security/authentication/listen/%s", + "CoordinatorBasicAuthenticatorCacheNotifier-notifierThread", + LOG + ); } @LifecycleStart @@ -107,58 +72,7 @@ public void start() } try { - notifierThread = Execs.makeThread( - "CoordinatorBasicAuthenticatorCacheNotifier-notifierThread", - () -> { - while (!Thread.interrupted()) { - try { - LOG.debug("Waiting for cache update notification"); - Set authenticatorsToUpdateSnapshot; - HashMap serializedUserMapsSnapshot; - synchronized (authenticatorsToUpdate) { - if (authenticatorsToUpdate.isEmpty()) { - authenticatorsToUpdate.wait(); - } - authenticatorsToUpdateSnapshot = new HashSet<>(authenticatorsToUpdate); - serializedUserMapsSnapshot = new HashMap(serializedMaps); - authenticatorsToUpdate.clear(); - serializedMaps.clear(); - } - LOG.debug("Sending cache update notifications"); - for (String authenticator : authenticatorsToUpdateSnapshot) { - BasicAuthDBConfig authenticatorConfig = authenticatorConfigMap.get(authenticator); - if (!authenticatorConfig.isEnableCacheNotifications()) { - continue; - } - - // Best effort, if a notification fails, the remote node will eventually poll to update its state - // We wait for responses however, to avoid flooding remote nodes with notifications. - List> futures = sendUpdate( - authenticator, - serializedUserMapsSnapshot.get(authenticator) - ); - for (ListenableFuture future : futures) { - try { - StatusResponseHolder srh = future.get( - authenticatorConfig.getCacheNotificationTimeout(), TimeUnit.MILLISECONDS - ); - LOG.debug("Got status: " + srh.getStatus()); - } - catch (Exception e) { - LOG.makeAlert("Failed to get response for cache notification.").emit(); - } - } - } - LOG.debug("Received responses for cache update notifications."); - } - catch (Throwable t) { - LOG.makeAlert(t, "Error occured while handling updates for cachedUserMaps.").emit(); - } - } - }, - true - ); - notifierThread.start(); + cacheNotifier.start(); lifecycleLock.started(); } finally { @@ -167,51 +81,18 @@ public void start() } @Override - public void addUpdate(String updatedAuthenticatorPrefix, byte[] userAndRoleMap) + public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap) { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); - - synchronized (authenticatorsToUpdate) { - authenticatorsToUpdate.add(updatedAuthenticatorPrefix); - serializedMaps.put(updatedAuthenticatorPrefix, userAndRoleMap); - authenticatorsToUpdate.notify(); - } + cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap); } - private List> sendUpdate( - String updatedAuthenticatorPrefix, - byte[] serializedUserAndRoleMap - ) + private Map initAuthenticatorConfigMap(AuthenticatorMapper mapper) { - List> futures = new ArrayList<>(); - for (String nodeType : NODE_TYPES) { - DruidNodeDiscovery nodeDiscovery = discoveryProvider.getForNodeType(nodeType); - Collection nodes = nodeDiscovery.getAllNodes(); - for (DiscoveryDruidNode node : nodes) { - URL listenerURL = getListenerURL(node.getDruidNode(), updatedAuthenticatorPrefix); + Preconditions.checkNotNull(mapper); + Preconditions.checkNotNull(mapper.getAuthenticatorMap()); - // best effort, if this fails, remote node will poll and pick up the update eventually - Request req = new Request(HttpMethod.POST, listenerURL); - req.setContent(MediaType.APPLICATION_JSON, serializedUserAndRoleMap); - - BasicAuthDBConfig authenticatorConfig = authenticatorConfigMap.get(updatedAuthenticatorPrefix); - - ListenableFuture future = httpClient.go( - req, - new ResponseHandler(), - Duration.millis(authenticatorConfig.getCacheNotificationTimeout()) - ); - futures.add(future); - } - } - return futures; - } - - private void initAuthenticatorConfigMap(AuthenticatorMapper mapper) - { - if (mapper == null || mapper.getAuthenticatorMap() == null) { - return; - } + Map authenticatorConfigMap = new HashMap<>(); for (Map.Entry entry : mapper.getAuthenticatorMap().entrySet()) { Authenticator authenticator = entry.getValue(); @@ -222,59 +103,7 @@ private void initAuthenticatorConfigMap(AuthenticatorMapper mapper) authenticatorConfigMap.put(authenticatorName, dbConfig); } } - } - - private static URL getListenerURL(DruidNode druidNode, String authPrefix) - { - try { - return new URL( - druidNode.getServiceScheme(), - druidNode.getHost(), - druidNode.getPortToUse(), - StringUtils.format("/druid-ext/basic-security/authentication/listen/%s", authPrefix) - ); - } - catch (MalformedURLException mue) { - LOG.error("WTF? Malformed url for DruidNode[%s] and authPrefix[%s]", druidNode, authPrefix); - throw new RuntimeException(mue); - } - } - // Based off StatusResponseHandler, but with response content ignored - private static class ResponseHandler implements HttpResponseHandler - { - @Override - public ClientResponse handleResponse(HttpResponse response) - { - return ClientResponse.unfinished( - new StatusResponseHolder( - response.getStatus(), - null - ) - ); - } - - @Override - public ClientResponse handleChunk( - ClientResponse response, - HttpChunk chunk - ) - { - return response; - } - - @Override - public ClientResponse done(ClientResponse response) - { - return ClientResponse.finished(response.getObj()); - } - - @Override - public void exceptionCaught( - ClientResponse clientResponse, Throwable e - ) - { - // Its safe to Ignore as the ClientResponse returned in handleChunk were unfinished - } + return authenticatorConfigMap; } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/DefaultBasicAuthenticatorCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java similarity index 70% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/DefaultBasicAuthenticatorCacheManager.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java index ea4e282315b5..606173486b9f 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/DefaultBasicAuthenticatorCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; +import com.google.common.io.Files; import com.google.inject.Inject; import com.google.inject.Injector; import com.metamx.emitter.EmittingLogger; @@ -36,18 +37,20 @@ import io.druid.java.util.common.concurrent.Execs; import io.druid.java.util.common.concurrent.ScheduledExecutors; import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +import io.druid.security.basic.BasicAuthCommonCacheConfig; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.authentication.BytesFullResponseHandler; import io.druid.security.basic.authentication.BytesFullResponseHolder; -import io.druid.security.basic.BasicAuthCommonCacheConfig; -import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; import io.druid.server.security.Authenticator; import io.druid.server.security.AuthenticatorMapper; import org.jboss.netty.handler.codec.http.HttpMethod; import org.joda.time.Duration; -import java.io.IOException; +import javax.annotation.Nullable; +import java.io.File; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -56,10 +59,13 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; +/** + * Cache manager for non-coordinator services that polls the coordinator for authentication database state. + */ @ManageLifecycle -public class DefaultBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager +public class CoordinatorPollingBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager { - private static final EmittingLogger LOG = new EmittingLogger(DefaultBasicAuthenticatorCacheManager.class); + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthenticatorCacheManager.class); private final ConcurrentHashMap> cachedUserMaps; private final Set authenticatorPrefixes; @@ -68,17 +74,17 @@ public class DefaultBasicAuthenticatorCacheManager implements BasicAuthenticator private final LifecycleLock lifecycleLock = new LifecycleLock(); private final DruidLeaderClient druidLeaderClient; private final BasicAuthCommonCacheConfig commonCacheConfig; - - private volatile ScheduledExecutorService exec; + private final ScheduledExecutorService exec; @Inject - public DefaultBasicAuthenticatorCacheManager( + public CoordinatorPollingBasicAuthenticatorCacheManager( Injector injector, BasicAuthCommonCacheConfig commonCacheConfig, @Smile ObjectMapper objectMapper, @Coordinator DruidLeaderClient druidLeaderClient ) { + this.exec = Execs.scheduledSingleThreaded("BasicAuthenticatorCacheManager-Exec--%d"); this.injector = injector; this.commonCacheConfig = commonCacheConfig; this.objectMapper = objectMapper; @@ -99,14 +105,16 @@ public void start() try { initUserMaps(); - this.exec = Execs.scheduledSingleThreaded("BasicAuthenticatorCacheManager-Exec--%d"); - ScheduledExecutors.scheduleWithFixedDelay( exec, new Duration(commonCacheConfig.getPollingPeriod()), new Duration(commonCacheConfig.getPollingPeriod()), () -> { try { + long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay()); + LOG.debug("Inserting random polling delay of [%s] ms", randomDelay); + Thread.sleep(randomDelay); + LOG.debug("Scheduled cache poll is running"); for (String authenticatorPrefix : authenticatorPrefixes) { Map userMap = fetchUserMapFromCoordinator(authenticatorPrefix, false); @@ -115,10 +123,6 @@ public void start() } } LOG.debug("Scheduled cache poll is done"); - - long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay()); - LOG.debug("Inserting random polling delay of [%s] ms", randomDelay); - Thread.sleep(randomDelay); } catch (Throwable t) { LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); @@ -134,6 +138,18 @@ public void start() } } + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + throw new ISE("can't stop."); + } + + LOG.info("DefaultBasicAuthenticatorCacheManager is stopping."); + exec.shutdown(); + LOG.info("DefaultBasicAuthenticatorCacheManager is stopped."); + } + @Override public void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap) { @@ -144,12 +160,16 @@ public void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] seriali authenticatorPrefix, objectMapper.readValue( serializedUserMap, - CoordinatorBasicAuthenticatorMetadataStorageUpdater.USER_MAP_TYPE_REFERENCE + BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE ) ); + + if (commonCacheConfig.getCacheDirectory() != null) { + writeUserMapToDisk(authenticatorPrefix, serializedUserMap); + } } - catch (IOException ioe) { - LOG.makeAlert("WTF? Could not deserialize user map received from coordinator.").emit(); + catch (Exception e) { + LOG.makeAlert(e, "WTF? Could not deserialize user map received from coordinator.").emit(); } } @@ -161,7 +181,8 @@ public Map getUserMap(String authenticatorPrefix return cachedUserMaps.get(authenticatorPrefix); } - private Map fetchUserMapFromCoordinator(String prefix, boolean throwOnFailure) + @Nullable + private Map fetchUserMapFromCoordinator(String prefix, boolean isInit) { try { return RetryUtils.retry( @@ -169,24 +190,57 @@ private Map fetchUserMapFromCoordinator(String p return tryFetchUserMapFromCoordinator(prefix); }, e -> true, - 10 + commonCacheConfig.getMaxSyncRetries() ); } catch (Exception e) { LOG.makeAlert(e, "Encountered exception while fetching user map for authenticator [%s]", prefix); - if (throwOnFailure) { - throw new RuntimeException(e); - } else { - return null; + if (isInit) { + if (commonCacheConfig.getCacheDirectory() != null) { + try { + LOG.info("Attempting to load user map snapshot from disk."); + return loadUserMapFromDisk(prefix); + } + catch (Exception e2) { + LOG.makeAlert(e2, "Encountered exception while loading user map snapshot for authenticator [%s]", prefix); + } + } } + return null; + } + } + + private String getUserMapFilename(String prefix) + { + return StringUtils.format("%s.authenticator.cache", prefix); + } + + @Nullable + private Map loadUserMapFromDisk(String prefix) throws Exception + { + File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserMapFilename(prefix)); + if (!userMapFile.exists()) { + return null; } + return objectMapper.readValue( + userMapFile, + BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE + ); + } + + private void writeUserMapToDisk(String prefix, byte[] userMapBytes) throws Exception + { + File cacheDir = new File(commonCacheConfig.getCacheDirectory()); + cacheDir.mkdirs(); + File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserMapFilename(prefix)); + Files.write(userMapBytes, userMapFile); } private Map tryFetchUserMapFromCoordinator(String prefix) throws Exception { Request req = druidLeaderClient.makeRequest( HttpMethod.GET, - StringUtils.format("/druid-ext/basic-security/authentication/%s/cachedSerializedUserMap", prefix) + StringUtils.format("/druid-ext/basic-security/authentication/db/%s/cachedSerializedUserMap", prefix) ); BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go( req, @@ -195,8 +249,11 @@ private Map tryFetchUserMapFromCoordinator(Strin byte[] userMapBytes = responseHolder.getBytes(); Map userMap = objectMapper.readValue( userMapBytes, - CoordinatorBasicAuthenticatorMetadataStorageUpdater.USER_MAP_TYPE_REFERENCE + BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE ); + if (userMap != null && commonCacheConfig.getCacheDirectory() != null) { + writeUserMapToDisk(prefix, userMapBytes); + } return userMap; } @@ -212,10 +269,11 @@ private void initUserMaps() Authenticator authenticator = entry.getValue(); if (authenticator instanceof BasicHTTPAuthenticator) { String authenticatorName = entry.getKey(); - BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator; - Map userMap = fetchUserMapFromCoordinator(authenticatorName, true); - cachedUserMaps.put(authenticatorName, userMap); authenticatorPrefixes.add(authenticatorName); + Map userMap = fetchUserMapFromCoordinator(authenticatorName, true); + if (userMap != null) { + cachedUserMaps.put(authenticatorName, userMap); + } } } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/MetadataStoragePollingBasicAuthenticatorCacheManager.java similarity index 80% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheManager.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/MetadataStoragePollingBasicAuthenticatorCacheManager.java index 44cb228dbda7..b5b7eea5ccc8 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/MetadataStoragePollingBasicAuthenticatorCacheManager.java @@ -26,14 +26,18 @@ import java.util.Map; -public class CoordinatorBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager +/** + * Used on coordinator nodes, reading from a BasicAuthenticatorMetadataStorageUpdater that has direct access to the + * metadata store. + */ +public class MetadataStoragePollingBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager { - private static final Logger log = new Logger(CoordinatorBasicAuthenticatorCacheManager.class); + private static final Logger log = new Logger(MetadataStoragePollingBasicAuthenticatorCacheManager.class); private final BasicAuthenticatorMetadataStorageUpdater storageUpdater; @Inject - public CoordinatorBasicAuthenticatorCacheManager( + public MetadataStoragePollingBasicAuthenticatorCacheManager( BasicAuthenticatorMetadataStorageUpdater storageUpdater ) { diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/BasicAuthenticatorMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/BasicAuthenticatorMetadataStorageUpdater.java index c0a9832abfe9..a5a9d59825c7 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/BasicAuthenticatorMetadataStorageUpdater.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/BasicAuthenticatorMetadataStorageUpdater.java @@ -19,17 +19,24 @@ package io.druid.security.basic.authentication.db.updater; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; import java.util.Map; +/** + * Implementations of this interface are responsible for connecting directly to the metadata storage, + * modifying the authenticator database state or reading it. This interface is used by the + * MetadataStoragePollingBasicAuthenticatorCacheManager (for reads) and the CoordinatorBasicAuthenticatorResourceHandler + * (for handling configuration read/writes). + */ public interface BasicAuthenticatorMetadataStorageUpdater { void createUser(String prefix, String userName); void deleteUser(String prefix, String userName); - void setUserCredentials(String prefix, String userName, char[] password); + void setUserCredentials(String prefix, String userName, BasicAuthenticatorCredentialUpdate update); Map getCachedUserMap(String prefix); @@ -37,7 +44,5 @@ public interface BasicAuthenticatorMetadataStorageUpdater byte[] getCurrentUserMapBytes(String prefix); - Map deserializeUserMap(byte[] userMapBytes); - - byte[] serializeUserMap(Map userMap); + void refreshAllNotification(); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/CoordinatorBasicAuthenticatorMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/CoordinatorBasicAuthenticatorMetadataStorageUpdater.java index 6c2fdbe17442..eacc02422373 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/CoordinatorBasicAuthenticatorMetadataStorageUpdater.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/CoordinatorBasicAuthenticatorMetadataStorageUpdater.java @@ -19,10 +19,8 @@ package io.druid.security.basic.authentication.db.updater; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; -import com.google.common.collect.Maps; import com.google.inject.Inject; import com.metamx.emitter.EmittingLogger; import io.druid.common.config.ConfigManager; @@ -38,25 +36,28 @@ import io.druid.metadata.MetadataCASUpdate; import io.druid.metadata.MetadataStorageConnector; import io.druid.metadata.MetadataStorageTablesConfig; +import io.druid.security.basic.BasicAuthCommonCacheConfig; +import io.druid.security.basic.BasicAuthDBConfig; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; -import io.druid.security.basic.BasicAuthDBConfig; -import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorUserMapBundle; import io.druid.server.security.Authenticator; import io.druid.server.security.AuthenticatorMapper; import org.joda.time.Duration; -import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @ManageLifecycle @@ -64,11 +65,8 @@ public class CoordinatorBasicAuthenticatorMetadataStorageUpdater implements Basi { private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class); - - public static final String USERS = "users"; - public static final TypeReference USER_MAP_TYPE_REFERENCE = new TypeReference>() - { - }; + private static final String USERS = "users"; + private static final long UPDATE_RETRY_DELAY = 1000; private final AuthenticatorMapper authenticatorMapper; private final MetadataStorageConnector connector; @@ -78,12 +76,11 @@ public class CoordinatorBasicAuthenticatorMetadataStorageUpdater implements Basi private final BasicAuthenticatorCacheNotifier cacheNotifier; private final int numRetries = 5; - private final Map> cachedUserMaps; - private final Map cachedSerializedUserMaps; + private final Map cachedUserMaps; private final Set authenticatorPrefixes; private final LifecycleLock lifecycleLock = new LifecycleLock(); - private volatile ScheduledExecutorService exec; + private final ScheduledExecutorService exec; private volatile boolean stopped = false; @Inject @@ -97,14 +94,14 @@ public CoordinatorBasicAuthenticatorMetadataStorageUpdater( ConfigManager configManager // ConfigManager creates the db table we need, set a dependency here ) { + this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthenticatorMetadataStorageUpdater-Exec--%d"); this.authenticatorMapper = authenticatorMapper; this.connector = connector; this.connectorConfig = connectorConfig; this.commonCacheConfig = commonCacheConfig; this.objectMapper = objectMapper; this.cacheNotifier = cacheNotifier; - this.cachedUserMaps = new HashMap<>(); - this.cachedSerializedUserMaps = new HashMap<>(); + this.cachedUserMaps = new ConcurrentHashMap<>(); this.authenticatorPrefixes = new HashSet<>(); } @@ -129,28 +126,39 @@ public void start() BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator; BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig(); byte[] userMapBytes = getCurrentUserMapBytes(authenticatorName); - Map userMap = deserializeUserMap(userMapBytes); - cachedUserMaps.put(authenticatorName, userMap); - cachedSerializedUserMaps.put(authenticatorName, userMapBytes); - - if (dbConfig.getInitialAdminPassword() != null && !userMap.containsKey("admin")) { - createUserInternal(authenticatorName, "admin"); - setUserCredentialsInternal(authenticatorName, "admin", dbConfig.getInitialAdminPassword().toCharArray()); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + userMapBytes + ); + cachedUserMaps.put(authenticatorName, new BasicAuthenticatorUserMapBundle(userMap, userMapBytes)); + + if (dbConfig.getInitialAdminPassword() != null && !userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { + createUserInternal(authenticatorName, BasicAuthUtils.ADMIN_NAME); + setUserCredentialsInternal( + authenticatorName, + BasicAuthUtils.ADMIN_NAME, + new BasicAuthenticatorCredentialUpdate( + dbConfig.getInitialAdminPassword(), + BasicAuthUtils.DEFAULT_KEY_ITERATIONS + ) + ); } - if (dbConfig.getInitialInternalClientPassword() != null && !userMap.containsKey("druid_system")) { - createUserInternal(authenticatorName, "druid_system"); + if (dbConfig.getInitialInternalClientPassword() != null + && !userMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) { + createUserInternal(authenticatorName, BasicAuthUtils.INTERNAL_USER_NAME); setUserCredentialsInternal( authenticatorName, - "druid_system", - dbConfig.getInitialInternalClientPassword().toCharArray() + BasicAuthUtils.INTERNAL_USER_NAME, + new BasicAuthenticatorCredentialUpdate( + dbConfig.getInitialInternalClientPassword(), + BasicAuthUtils.DEFAULT_KEY_ITERATIONS + ) ); } } } - this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthenticatorMetadataStorageUpdater-Exec--%d"); - ScheduledExecutors.scheduleWithFixedDelay( exec, new Duration(commonCacheConfig.getPollingPeriod()), @@ -168,12 +176,12 @@ public ScheduledExecutors.Signal call() throws Exception for (String authenticatorPrefix : authenticatorPrefixes) { byte[] userMapBytes = getCurrentUserMapBytes(authenticatorPrefix); - Map userMap = deserializeUserMap(userMapBytes); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + userMapBytes + ); if (userMapBytes != null) { - synchronized (cachedUserMaps) { - cachedUserMaps.put(authenticatorPrefix, userMap); - cachedSerializedUserMaps.put(authenticatorPrefix, userMapBytes); - } + cachedUserMaps.put(authenticatorPrefix, new BasicAuthenticatorUserMapBundle(userMap, userMapBytes)); } } LOG.debug("Scheduled db poll is done"); @@ -220,10 +228,10 @@ public void deleteUser(String prefix, String userName) } @Override - public void setUserCredentials(String prefix, String userName, char[] password) + public void setUserCredentials(String prefix, String userName, BasicAuthenticatorCredentialUpdate update) { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); - setUserCredentialsInternal(prefix, userName, password); + setUserCredentialsInternal(prefix, userName, update); } @Override @@ -231,8 +239,11 @@ public Map getCachedUserMap(String prefix) { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); - synchronized (cachedUserMaps) { - return cachedUserMaps.get(prefix); + BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix); + if (bundle == null) { + return null; + } else { + return bundle.getUserMap(); } } @@ -241,8 +252,11 @@ public byte[] getCachedSerializedUserMap(String prefix) { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); - synchronized (cachedUserMaps) { - return cachedSerializedUserMaps.get(prefix); + BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix); + if (bundle == null) { + return null; + } else { + return bundle.getSerializedUserMap(); } } @@ -258,31 +272,13 @@ public byte[] getCurrentUserMapBytes(String prefix) } @Override - public Map deserializeUserMap(byte[] userMapBytes) + public void refreshAllNotification() { - Map userMap; - if (userMapBytes == null) { - userMap = Maps.newHashMap(); - } else { - try { - userMap = objectMapper.readValue(userMapBytes, USER_MAP_TYPE_REFERENCE); - } - catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - return userMap; - } - - @Override - public byte[] serializeUserMap(Map userMap) - { - try { - return objectMapper.writeValueAsBytes(userMap); - } - catch (IOException ioe) { - throw new ISE("WTF? Couldn't serialize userMap!"); - } + cachedUserMaps.forEach( + (authenticatorName, userMapBundle) -> { + cacheNotifier.addUpdate(authenticatorName, userMapBundle.getSerializedUserMap()); + } + ); } private static String getPrefixedKeyColumn(String keyPrefix, String keyName) @@ -298,28 +294,25 @@ private boolean tryUpdateUserMap( ) { try { - synchronized (cachedUserMaps) { - MetadataCASUpdate update = new MetadataCASUpdate( - connectorConfig.getConfigTable(), - MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, - MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, - getPrefixedKeyColumn(prefix, USERS), - oldValue, - newValue - ); - - boolean succeeded = connector.compareAndSwap( - Collections.singletonList(update) - ); - - if (succeeded) { - cachedUserMaps.put(prefix, userMap); - cachedSerializedUserMaps.put(prefix, newValue); - cacheNotifier.addUpdate(prefix, newValue); - return true; - } else { - return false; - } + MetadataCASUpdate update = new MetadataCASUpdate( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, USERS), + oldValue, + newValue + ); + + boolean succeeded = connector.compareAndSwap( + Collections.singletonList(update) + ); + + if (succeeded) { + cachedUserMaps.put(prefix, new BasicAuthenticatorUserMapBundle(userMap, newValue)); + cacheNotifier.addUpdate(prefix, newValue); + return true; + } else { + return false; } } catch (Exception e) { @@ -336,6 +329,12 @@ private void createUserInternal(String prefix, String userName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not create user[%s] due to concurrent update contention.", userName); } @@ -349,13 +348,35 @@ private void deleteUserInternal(String prefix, String userName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not delete user[%s] due to concurrent update contention.", userName); } - private void setUserCredentialsInternal(String prefix, String userName, char[] password) + private void setUserCredentialsInternal(String prefix, String userName, BasicAuthenticatorCredentialUpdate update) { - BasicAuthenticatorCredentials credentials = new BasicAuthenticatorCredentials(password); + BasicAuthenticatorCredentials credentials; + + // use default iteration count from Authenticator if not specified in request + if (update.getIterations() == -1) { + BasicHTTPAuthenticator authenticator = (BasicHTTPAuthenticator) authenticatorMapper.getAuthenticatorMap().get( + prefix + ); + credentials = new BasicAuthenticatorCredentials( + new BasicAuthenticatorCredentialUpdate( + update.getPassword(), + authenticator.getDbConfig().getIterations() + ) + ); + } else { + credentials = new BasicAuthenticatorCredentials(update); + } + int attempts = 0; while (attempts < numRetries) { if (setUserCredentialOnce(prefix, userName, credentials)) { @@ -363,6 +384,12 @@ private void setUserCredentialsInternal(String prefix, String userName, char[] p } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not set credentials for user[%s] due to concurrent update contention.", userName); } @@ -370,39 +397,48 @@ private void setUserCredentialsInternal(String prefix, String userName, char[] p private boolean createUserOnce(String prefix, String userName) { byte[] oldValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldValue); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + oldValue + ); if (userMap.get(userName) != null) { throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); } else { userMap.put(userName, new BasicAuthenticatorUser(userName, null)); } - byte[] newValue = serializeUserMap(userMap); + byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap); return tryUpdateUserMap(prefix, userMap, oldValue, newValue); } private boolean deleteUserOnce(String prefix, String userName) { byte[] oldValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldValue); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + oldValue + ); if (userMap.get(userName) == null) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } else { userMap.remove(userName); } - byte[] newValue = serializeUserMap(userMap); + byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap); return tryUpdateUserMap(prefix, userMap, oldValue, newValue); } private boolean setUserCredentialOnce(String prefix, String userName, BasicAuthenticatorCredentials credentials) { byte[] oldValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldValue); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + oldValue + ); if (userMap.get(userName) == null) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } else { userMap.put(userName, new BasicAuthenticatorUser(userName, credentials)); } - byte[] newValue = serializeUserMap(userMap); + byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap); return tryUpdateUserMap(prefix, userMap, oldValue, newValue); } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/NoopBasicAuthenticatorMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/NoopBasicAuthenticatorMetadataStorageUpdater.java deleted file mode 100644 index c5c834aeda84..000000000000 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/NoopBasicAuthenticatorMetadataStorageUpdater.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package io.druid.security.basic.authentication.db.updater; - -import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; - -import java.util.Map; - -public class NoopBasicAuthenticatorMetadataStorageUpdater implements BasicAuthenticatorMetadataStorageUpdater -{ - @Override - public void createUser(String prefix, String userName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void deleteUser(String prefix, String userName) - { - throw new UnsupportedOperationException("not supported"); - - } - - @Override - public void setUserCredentials(String prefix, String userName, char[] password) - { - throw new UnsupportedOperationException("not supported"); - - } - - @Override - public Map getCachedUserMap(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public byte[] getCachedSerializedUserMap(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public byte[] getCurrentUserMapBytes(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public Map deserializeUserMap(byte[] userMapBytes) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public byte[] serializeUserMap(Map userMap) - { - throw new UnsupportedOperationException("not supported"); - } -} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java index 4cb4e4e31c7b..3ff0c34b9e4f 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java @@ -24,6 +24,7 @@ import com.sun.jersey.spi.container.ResourceFilters; import io.druid.guice.LazySingleton; import io.druid.security.basic.BasicSecurityResourceFilter; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -51,13 +52,48 @@ public BasicAuthenticatorResource( this.handler = handler; } + /** + * @param req HTTP request + * + * @return Load status of authenticator DB caches + */ + @GET + @Path("/loadStatus") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getLoadStatus( + @Context HttpServletRequest req + ) + { + return handler.getLoadStatus(); + } + + /** + * @param req HTTP request + * + * Sends an "update" notification to all services with the current user database state, + * causing them to refresh their DB cache state. + */ + @GET + @Path("/refreshAll") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response refreshAll( + @Context HttpServletRequest req + ) + { + return handler.refreshAll(); + } + /** * @param req HTTP request * * @return List of all users */ @GET - @Path("/{authenticatorName}/users") + @Path("/db/{authenticatorName}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -76,7 +112,7 @@ public Response getAllUsers( * @return Name and credentials of the user with userName, 400 error response if user doesn't exist */ @GET - @Path("/{authenticatorName}/users/{userName}") + @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -98,7 +134,7 @@ public Response getUser( * @return OK response, or 400 error response if user already exists */ @POST - @Path("/{authenticatorName}/users/{userName}") + @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -120,7 +156,7 @@ public Response createUser( * @return OK response, or 400 error response if user doesn't exist */ @DELETE - @Path("/{authenticatorName}/users/{userName}") + @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -143,7 +179,7 @@ public Response deleteUser( * @return OK response, 400 error if user doesn't exist */ @POST - @Path("/{authenticatorName}/users/{userName}/credentials") + @Path("/db/{authenticatorName}/users/{userName}/credentials") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -151,10 +187,10 @@ public Response updateUserCredentials( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, @PathParam("userName") String userName, - String password + BasicAuthenticatorCredentialUpdate update ) { - return handler.updateUserCredentials(authenticatorName, userName, password); + return handler.updateUserCredentials(authenticatorName, userName, update); } /** @@ -163,7 +199,7 @@ public Response updateUserCredentials( * @return serialized user map */ @GET - @Path("/{authenticatorName}/cachedSerializedUserMap") + @Path("/db/{authenticatorName}/cachedSerializedUserMap") @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResourceHandler.java index d000a9feb4c9..de0d7dd5d7b7 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResourceHandler.java @@ -19,10 +19,18 @@ package io.druid.security.basic.authentication.endpoint; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; + import javax.ws.rs.core.Response; +/** + * Handles authenticator-related API calls. Coordinator and non-coordinator methods are combined here because of an + * inability to selectively inject jetty resources in configure(Binder binder) of the extension module based + * on node type. + */ public interface BasicAuthenticatorResourceHandler { + // coordinator methods Response getAllUsers(String authenticatorName); Response getUser(String authenticatorName, String userName); @@ -31,9 +39,15 @@ public interface BasicAuthenticatorResourceHandler Response deleteUser(String authenticatorName, String userName); - Response updateUserCredentials(String authenticatorName, String userName, String password); + Response updateUserCredentials(String authenticatorName, String userName, BasicAuthenticatorCredentialUpdate update); Response getCachedSerializedUserMap(String authenticatorName); + Response refreshAll(); + + // non-coordinator methods Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap); + + // common methods + Response getLoadStatus(); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/CoordinatorBasicAuthenticatorResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/CoordinatorBasicAuthenticatorResourceHandler.java index 157b4b9e4f6b..5f02684a4a21 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/CoordinatorBasicAuthenticatorResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/CoordinatorBasicAuthenticatorResourceHandler.java @@ -19,27 +19,30 @@ package io.druid.security.basic.authentication.endpoint; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.inject.Inject; import io.druid.java.util.common.StringUtils; -import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; import io.druid.server.security.Authenticator; import io.druid.server.security.AuthenticatorMapper; import javax.ws.rs.core.Response; +import java.util.HashMap; import java.util.Map; public class CoordinatorBasicAuthenticatorResourceHandler implements BasicAuthenticatorResourceHandler { - private static final Logger log = new Logger(CoordinatorBasicAuthenticatorResourceHandler.class); - private final BasicAuthenticatorMetadataStorageUpdater storageUpdater; private final Map authenticatorMap; + private final ObjectMapper objectMapper; @Inject public CoordinatorBasicAuthenticatorResourceHandler( @@ -48,6 +51,7 @@ public CoordinatorBasicAuthenticatorResourceHandler( ) { this.storageUpdater = storageUpdater; + this.objectMapper = new ObjectMapper(new SmileFactory()); this.authenticatorMap = Maps.newHashMap(); for (Map.Entry authenticatorEntry : authenticatorMapper.getAuthenticatorMap().entrySet()) { @@ -72,7 +76,8 @@ public Response getAllUsers( return makeResponseForAuthenticatorNotFound(authenticatorName); } - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authenticatorName) ); @@ -87,7 +92,8 @@ public Response getUser(String authenticatorName, String userName) return makeResponseForAuthenticatorNotFound(authenticatorName); } - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authenticatorName) ); @@ -138,7 +144,7 @@ public Response deleteUser(String authenticatorName, String userName) } @Override - public Response updateUserCredentials(String authenticatorName, String userName, String password) + public Response updateUserCredentials(String authenticatorName, String userName, BasicAuthenticatorCredentialUpdate update) { final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); if (authenticator == null) { @@ -146,7 +152,7 @@ public Response updateUserCredentials(String authenticatorName, String userName, } try { - storageUpdater.setUserCredentials(authenticatorName, userName, password.toCharArray()); + storageUpdater.setUserCredentials(authenticatorName, userName, update); return Response.ok().build(); } catch (BasicSecurityDBResourceException cfe) { @@ -165,12 +171,31 @@ public Response getCachedSerializedUserMap(String authenticatorName) return Response.ok(storageUpdater.getCachedSerializedUserMap(authenticatorName)).build(); } + @Override + public Response refreshAll() + { + storageUpdater.refreshAllNotification(); + return Response.ok().build(); + } + @Override public Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap) { return Response.status(Response.Status.NOT_FOUND).build(); } + @Override + public Response getLoadStatus() + { + Map loadStatus = new HashMap<>(); + authenticatorMap.forEach( + (authenticatorName, authenticator) -> { + loadStatus.put(authenticatorName, storageUpdater.getCachedUserMap(authenticatorName) != null); + } + ); + return Response.ok(loadStatus).build(); + } + private static Response makeResponseForAuthenticatorNotFound(String authenticatorName) { return Response.status(Response.Status.BAD_REQUEST) diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/DefaultBasicAuthenticatorResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/DefaultBasicAuthenticatorResourceHandler.java index 373b47bf3a61..d1211bf50941 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/DefaultBasicAuthenticatorResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/DefaultBasicAuthenticatorResourceHandler.java @@ -26,10 +26,12 @@ import io.druid.java.util.common.logger.Logger; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.server.security.Authenticator; import io.druid.server.security.AuthenticatorMapper; import javax.ws.rs.core.Response; +import java.util.HashMap; import java.util.Map; public class DefaultBasicAuthenticatorResourceHandler implements BasicAuthenticatorResourceHandler @@ -86,7 +88,11 @@ public Response deleteUser(String authenticatorName, String userName) } @Override - public Response updateUserCredentials(String authenticatorName, String userName, String password) + public Response updateUserCredentials( + String authenticatorName, + String userName, + BasicAuthenticatorCredentialUpdate update + ) { return NOT_FOUND_RESPONSE; } @@ -97,6 +103,12 @@ public Response getCachedSerializedUserMap(String authenticatorName) return NOT_FOUND_RESPONSE; } + @Override + public Response refreshAll() + { + return NOT_FOUND_RESPONSE; + } + @Override public Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap) { @@ -115,4 +127,16 @@ public Response authenticatorUpdateListener(String authenticatorName, byte[] ser cacheManager.handleAuthenticatorUpdate(authenticatorName, serializedUserMap); return Response.ok().build(); } + + @Override + public Response getLoadStatus() + { + Map loadStatus = new HashMap<>(); + authenticatorMap.forEach( + (authenticatorName, authenticator) -> { + loadStatus.put(authenticatorName, cacheManager.getUserMap(authenticatorName) != null); + } + ); + return Response.ok(loadStatus).build(); + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentialUpdate.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentialUpdate.java new file mode 100644 index 000000000000..36bcb2d6e4d7 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentialUpdate.java @@ -0,0 +1,55 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.authentication.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import org.apache.commons.lang3.StringUtils; + +public class BasicAuthenticatorCredentialUpdate +{ + private final String password; + private final int iterations; + + @JsonCreator + public BasicAuthenticatorCredentialUpdate( + @JsonProperty("password") String password, + @JsonProperty("iterations") Integer iterations + ) + { + Preconditions.checkNotNull(password, "Cannot assign null password."); + Preconditions.checkArgument(!StringUtils.isEmpty(password), "Cannot assign empty password."); + this.password = password; + this.iterations = iterations == null ? -1 : iterations; + } + + @JsonProperty + public String getPassword() + { + return password; + } + + @JsonProperty + public int getIterations() + { + return iterations; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentials.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentials.java index f7fef9b2e745..e6aea0f51d0a 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentials.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentials.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; import io.druid.security.basic.BasicAuthUtils; import java.util.Arrays; @@ -38,16 +39,18 @@ public BasicAuthenticatorCredentials( @JsonProperty("iterations") int iterations ) { + Preconditions.checkNotNull(salt); + Preconditions.checkNotNull(hash); this.salt = salt; this.hash = hash; this.iterations = iterations; } - public BasicAuthenticatorCredentials(char[] password) + public BasicAuthenticatorCredentials(BasicAuthenticatorCredentialUpdate update) { - this.iterations = BasicAuthUtils.KEY_ITERATIONS; + this.iterations = update.getIterations(); this.salt = BasicAuthUtils.generateSalt(); - this.hash = BasicAuthUtils.hashPassword(password, salt, iterations); + this.hash = BasicAuthUtils.hashPassword(update.getPassword().toCharArray(), salt, iterations); } @JsonProperty diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUser.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUser.java index 9a47c7bf3853..3a8bd5633752 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUser.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUser.java @@ -65,7 +65,6 @@ public boolean equals(Object o) return false; } return getCredentials() != null ? getCredentials().equals(that.getCredentials()) : that.getCredentials() == null; - } @Override diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUserMapBundle.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUserMapBundle.java new file mode 100644 index 000000000000..59cc5405575f --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUserMapBundle.java @@ -0,0 +1,53 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.authentication.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public class BasicAuthenticatorUserMapBundle +{ + private final Map userMap; + private final byte[] serializedUserMap; + + @JsonCreator + public BasicAuthenticatorUserMapBundle( + @JsonProperty("userMap") Map userMap, + @JsonProperty("serializedUserMap") byte[] serializedUserMap + ) + { + this.userMap = userMap; + this.serializedUserMap = serializedUserMap; + } + + @JsonProperty + public Map getUserMap() + { + return userMap; + } + + @JsonProperty + public byte[] getSerializedUserMap() + { + return serializedUserMap; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java index 77b4572acbce..8dd55c27b257 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -61,7 +61,8 @@ public BasicRoleBasedAuthorizer( null, null, enableCacheNotifications == null ? true : enableCacheNotifications, - cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout + cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout, + 0 ); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheManager.java index 8c82ea58f7ea..77eaecd0b673 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheManager.java @@ -24,11 +24,33 @@ import java.util.Map; +/** + * This class is reponsible for maintaining a cache of the authorization database state. The BasicRBACAuthorizer + * uses an injected BasicAuthorizerCacheManager to make its authorization decisions. + */ public interface BasicAuthorizerCacheManager { + /** + * Update this cache manager's local state with fresh information pushed by the coordinator. + * + * @param authorizerPrefix The name of the authorizer this update applies to. + * @param serializedUserAndRoleMap The updated, serialized user and role maps + */ void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap); + /** + * Return the cache manager's local view of the user map for the authorizer named `authorizerPrefix`. + * + * @param authorizerPrefix The name of the authorizer + * @return User map + */ Map getUserMap(String authorizerPrefix); + /** + * Return the cache manager's local view of the role map for the authorizer named `authorizerPrefix`. + * + * @param authorizerPrefix The name of the authorizer + * @return Role map + */ Map getRoleMap(String authorizerPrefix); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheNotifier.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheNotifier.java index 530974becfe1..95bd80759d4b 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheNotifier.java @@ -19,7 +19,16 @@ package io.druid.security.basic.authorization.db.cache; +/** + * Sends a notification to druid services, containing updated authorizer user/role map state. + */ public interface BasicAuthorizerCacheNotifier { + /** + * Send the user map state contained in updatedUserMap to all non-coordinator Druid services + * + * @param authorizerPrefix Name of authorizer being updated + * @param userAndRoleMap User/role map state + */ void addUpdate(String authorizerPrefix, byte[] userAndRoleMap); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheNotifier.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheNotifier.java index 98074ff5332d..d1f201222a8b 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheNotifier.java @@ -20,45 +20,23 @@ package io.druid.security.basic.authorization.db.cache; import com.google.common.base.Preconditions; -import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.Inject; import com.metamx.emitter.EmittingLogger; import com.metamx.http.client.HttpClient; -import com.metamx.http.client.Request; -import com.metamx.http.client.response.ClientResponse; -import com.metamx.http.client.response.HttpResponseHandler; -import com.metamx.http.client.response.StatusResponseHolder; import io.druid.concurrent.LifecycleLock; -import io.druid.discovery.DiscoveryDruidNode; -import io.druid.discovery.DruidNodeDiscovery; import io.druid.discovery.DruidNodeDiscoveryProvider; import io.druid.guice.ManageLifecycle; import io.druid.guice.annotations.EscalatedClient; import io.druid.java.util.common.ISE; -import io.druid.java.util.common.StringUtils; -import io.druid.java.util.common.concurrent.Execs; import io.druid.java.util.common.lifecycle.LifecycleStart; import io.druid.security.basic.BasicAuthDBConfig; +import io.druid.security.basic.CommonCacheNotifier; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.server.DruidNode; import io.druid.server.security.Authorizer; import io.druid.server.security.AuthorizerMapper; -import org.jboss.netty.handler.codec.http.HttpChunk; -import org.jboss.netty.handler.codec.http.HttpMethod; -import org.jboss.netty.handler.codec.http.HttpResponse; -import org.joda.time.Duration; -import javax.ws.rs.core.MediaType; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; @ManageLifecycle @@ -66,23 +44,8 @@ public class CoordinatorBasicAuthorizerCacheNotifier implements BasicAuthorizerC { private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthorizerCacheNotifier.class); - private static final List NODE_TYPES = Arrays.asList( - DruidNodeDiscoveryProvider.NODE_TYPE_BROKER, - DruidNodeDiscoveryProvider.NODE_TYPE_OVERLORD, - DruidNodeDiscoveryProvider.NODE_TYPE_HISTORICAL, - DruidNodeDiscoveryProvider.NODE_TYPE_PEON, - DruidNodeDiscoveryProvider.NODE_TYPE_ROUTER, - DruidNodeDiscoveryProvider.NODE_TYPE_MM - ); - - private final DruidNodeDiscoveryProvider discoveryProvider; - private final HttpClient httpClient; - private final Set authorizersToUpdate; - private final Map serializedMaps; - private final Map authorizerConfigMap; private final LifecycleLock lifecycleLock = new LifecycleLock(); - - private Thread notifierThread; + private CommonCacheNotifier cacheNotifier; @Inject public CoordinatorBasicAuthorizerCacheNotifier( @@ -91,12 +54,14 @@ public CoordinatorBasicAuthorizerCacheNotifier( @EscalatedClient HttpClient httpClient ) { - this.discoveryProvider = discoveryProvider; - this.httpClient = httpClient; - this.authorizersToUpdate = new HashSet<>(); - this.serializedMaps = new HashMap<>(); - this.authorizerConfigMap = new HashMap<>(); - initAuthorizerConfigMap(authorizerMapper); + cacheNotifier = new CommonCacheNotifier( + getAuthorizerConfigMap(authorizerMapper), + discoveryProvider, + httpClient, + "/druid-ext/basic-security/authorization/listen/%s", + "CoordinatorBasicAuthorizerCacheNotifier-notifierThread", + LOG + ); } @LifecycleStart @@ -107,58 +72,7 @@ public void start() } try { - notifierThread = Execs.makeThread( - "CoordinatorBasicAuthorizerCacheNotifier-notifierThread", - () -> { - while (!Thread.interrupted()) { - try { - LOG.debug("Waiting for cache update notification"); - Set authorizersToUpdateSnapshot; - HashMap serializedUserMapsSnapshot; - synchronized (authorizersToUpdate) { - if (authorizersToUpdate.isEmpty()) { - authorizersToUpdate.wait(); - } - authorizersToUpdateSnapshot = new HashSet<>(authorizersToUpdate); - serializedUserMapsSnapshot = new HashMap(serializedMaps); - authorizersToUpdate.clear(); - serializedMaps.clear(); - } - LOG.debug("Sending cache update notifications"); - for (String authorizer : authorizersToUpdateSnapshot) { - BasicAuthDBConfig authorizerConfig = authorizerConfigMap.get(authorizer); - if (!authorizerConfig.isEnableCacheNotifications()) { - continue; - } - - // Best effort, if a notification fails, the remote node will eventually poll to update its state - // We wait for responses however, to avoid flooding remote nodes with notifications. - List> futures = sendUpdate( - authorizer, - serializedUserMapsSnapshot.get(authorizer) - ); - for (ListenableFuture future : futures) { - try { - StatusResponseHolder srh = future.get( - authorizerConfig.getCacheNotificationTimeout(), TimeUnit.MILLISECONDS - ); - LOG.debug("Got status: " + srh.getStatus()); - } - catch (Exception e) { - LOG.makeAlert("Failed to get response for cache notification.").emit(); - } - } - } - LOG.debug("Received responses for cache update notifications."); - } - catch (Throwable t) { - LOG.makeAlert(t, "Error occured while handling updates for cachedUserMaps.").emit(); - } - } - }, - true - ); - notifierThread.start(); + cacheNotifier.start(); lifecycleLock.started(); } finally { @@ -170,46 +84,15 @@ public void start() public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap) { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); - - synchronized (authorizersToUpdate) { - authorizersToUpdate.add(updatedAuthorizerPrefix); - serializedMaps.put(updatedAuthorizerPrefix, updatedUserMap); - authorizersToUpdate.notify(); - } + cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap); } - private List> sendUpdate(String updatedAuthorizerPrefix, byte[] serializedUserMap) + private Map getAuthorizerConfigMap(AuthorizerMapper mapper) { - List> futures = new ArrayList<>(); - for (String nodeType : NODE_TYPES) { - DruidNodeDiscovery nodeDiscovery = discoveryProvider.getForNodeType(nodeType); - Collection nodes = nodeDiscovery.getAllNodes(); - for (DiscoveryDruidNode node : nodes) { - URL listenerURL = getListenerURL(node.getDruidNode(), updatedAuthorizerPrefix); - - // best effort, if this fails, remote node will poll and pick up the update eventually - Request req = new Request(HttpMethod.POST, listenerURL); - req.setContent(MediaType.APPLICATION_JSON, serializedUserMap); - - BasicAuthDBConfig authorizerConfig = authorizerConfigMap.get(updatedAuthorizerPrefix); - - ListenableFuture future = httpClient.go( - req, - new ResponseHandler(), - Duration.millis(authorizerConfig.getCacheNotificationTimeout()) - ); - futures.add(future); - } - } - return futures; - } - - private void initAuthorizerConfigMap(AuthorizerMapper mapper) - { - if (mapper == null || mapper.getAuthorizerMap() == null) { - return; - } + Preconditions.checkNotNull(mapper); + Preconditions.checkNotNull(mapper.getAuthorizerMap()); + Map authorizerConfigMap = new HashMap<>(); for (Map.Entry entry : mapper.getAuthorizerMap().entrySet()) { Authorizer authorizer = entry.getValue(); if (authorizer instanceof BasicRoleBasedAuthorizer) { @@ -219,59 +102,7 @@ private void initAuthorizerConfigMap(AuthorizerMapper mapper) authorizerConfigMap.put(authorizerName, dbConfig); } } - } - private static URL getListenerURL(DruidNode druidNode, String authPrefix) - { - try { - return new URL( - druidNode.getServiceScheme(), - druidNode.getHost(), - druidNode.getPortToUse(), - StringUtils.format("/druid-ext/basic-security/authorization/listen/%s", authPrefix) - ); - } - catch (MalformedURLException mue) { - LOG.error("WTF? Malformed url for DruidNode[%s] and authPrefix[%s]", druidNode, authPrefix); - throw new RuntimeException(mue); - } - } - - // Based off StatusResponseHandler, but with response content ignored - private static class ResponseHandler implements HttpResponseHandler - { - @Override - public ClientResponse handleResponse(HttpResponse response) - { - return ClientResponse.unfinished( - new StatusResponseHolder( - response.getStatus(), - null - ) - ); - } - - @Override - public ClientResponse handleChunk( - ClientResponse response, - HttpChunk chunk - ) - { - return response; - } - - @Override - public ClientResponse done(ClientResponse response) - { - return ClientResponse.finished(response.getObj()); - } - - @Override - public void exceptionCaught( - ClientResponse clientResponse, Throwable e - ) - { - // Its safe to Ignore as the ClientResponse returned in handleChunk were unfinished - } + return authorizerConfigMap; } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/DefaultBasicAuthorizerCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java similarity index 68% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/DefaultBasicAuthorizerCacheManager.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java index ffa257c25e8c..da15b828764d 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/DefaultBasicAuthorizerCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; +import com.google.common.io.Files; import com.google.inject.Inject; import com.google.inject.Injector; import com.metamx.emitter.EmittingLogger; @@ -36,11 +37,12 @@ import io.druid.java.util.common.concurrent.Execs; import io.druid.java.util.common.concurrent.ScheduledExecutors; import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +import io.druid.security.basic.BasicAuthCommonCacheConfig; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.authentication.BytesFullResponseHandler; import io.druid.security.basic.authentication.BytesFullResponseHolder; -import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; import io.druid.security.basic.authorization.entity.UserAndRoleMap; @@ -49,7 +51,8 @@ import org.jboss.netty.handler.codec.http.HttpMethod; import org.joda.time.Duration; -import java.io.IOException; +import javax.annotation.Nullable; +import java.io.File; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -59,9 +62,9 @@ import java.util.concurrent.TimeUnit; @ManageLifecycle -public class DefaultBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager +public class CoordinatorPollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager { - private static final EmittingLogger LOG = new EmittingLogger(DefaultBasicAuthorizerCacheManager.class); + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthorizerCacheManager.class); private final ConcurrentHashMap> cachedUserMaps; private final ConcurrentHashMap> cachedRoleMaps; @@ -72,17 +75,17 @@ public class DefaultBasicAuthorizerCacheManager implements BasicAuthorizerCacheM private final LifecycleLock lifecycleLock = new LifecycleLock(); private final DruidLeaderClient druidLeaderClient; private final BasicAuthCommonCacheConfig commonCacheConfig; - - private volatile ScheduledExecutorService exec; + private final ScheduledExecutorService exec; @Inject - public DefaultBasicAuthorizerCacheManager( + public CoordinatorPollingBasicAuthorizerCacheManager( Injector injector, BasicAuthCommonCacheConfig commonCacheConfig, @Smile ObjectMapper objectMapper, @Coordinator DruidLeaderClient druidLeaderClient ) { + this.exec = Execs.scheduledSingleThreaded("CoordinatorPollingBasicAuthorizerCacheManager-Exec--%d"); this.injector = injector; this.commonCacheConfig = commonCacheConfig; this.objectMapper = objectMapper; @@ -99,19 +102,21 @@ public void start() throw new ISE("can't start."); } - LOG.info("Starting DefaultBasicAuthorizerCacheManager."); + LOG.info("Starting CoordinatorPollingBasicAuthorizerCacheManager."); try { initUserMaps(); - this.exec = Execs.scheduledSingleThreaded("BasicAuthorizerCacheManager-Exec--%d"); - ScheduledExecutors.scheduleWithFixedDelay( exec, new Duration(commonCacheConfig.getPollingPeriod()), new Duration(commonCacheConfig.getPollingPeriod()), () -> { try { + long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay()); + LOG.debug("Inserting random polling delay of [%s] ms", randomDelay); + Thread.sleep(randomDelay); + LOG.debug("Scheduled cache poll is running"); for (String authorizerPrefix : authorizerPrefixes) { UserAndRoleMap userAndRoleMap = fetchUserAndRoleMapFromCoordinator(authorizerPrefix, false); @@ -121,10 +126,6 @@ public void start() } } LOG.debug("Scheduled cache poll is done"); - - long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay()); - LOG.debug("Inserting random polling delay of [%s] ms", randomDelay); - Thread.sleep(randomDelay); } catch (Throwable t) { LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); @@ -133,13 +134,25 @@ public void start() ); lifecycleLock.started(); - LOG.info("Started DefaultBasicAuthorizerCacheManager."); + LOG.info("Started CoordinatorPollingBasicAuthorizerCacheManager."); } finally { lifecycleLock.exitStart(); } } + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + throw new ISE("can't stop."); + } + + LOG.info("CoordinatorPollingBasicAuthorizerCacheManager is stopping."); + exec.shutdown(); + LOG.info("CoordinatorPollingBasicAuthorizerCacheManager is stopped."); + } + @Override public void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap) { @@ -148,14 +161,18 @@ public void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUse try { UserAndRoleMap userAndRoleMap = objectMapper.readValue( serializedUserAndRoleMap, - CoordinatorBasicAuthorizerMetadataStorageUpdater.USER_AND_ROLE_MAP_TYPE_REFERENCE + BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE ); cachedUserMaps.put(authorizerPrefix, userAndRoleMap.getUserMap()); cachedRoleMaps.put(authorizerPrefix, userAndRoleMap.getRoleMap()); + + if (commonCacheConfig.getCacheDirectory() != null) { + writeMapToDisk(authorizerPrefix, serializedUserAndRoleMap); + } } - catch (IOException ioe) { - LOG.makeAlert("WTF? Could not deserialize user map received from coordinator.").emit(); + catch (Exception e) { + LOG.makeAlert(e, "WTF? Could not deserialize user/role map received from coordinator.").emit(); } } @@ -171,7 +188,34 @@ public Map getRoleMap(String authorizerPrefix) return cachedRoleMaps.get(authorizerPrefix); } - private UserAndRoleMap fetchUserAndRoleMapFromCoordinator(String prefix, boolean throwOnFailure) + private String getUserRoleMapFilename(String prefix) + { + return StringUtils.format("%s.authorizer.cache", prefix); + } + + @Nullable + private UserAndRoleMap loadUserAndRoleMapFromDisk(String prefix) throws Exception + { + File userAndRoleMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix)); + if (!userAndRoleMapFile.exists()) { + return null; + } + return objectMapper.readValue( + userAndRoleMapFile, + BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE + ); + } + + private void writeMapToDisk(String prefix, byte[] userMapBytes) throws Exception + { + File cacheDir = new File(commonCacheConfig.getCacheDirectory()); + cacheDir.mkdirs(); + File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix)); + Files.write(userMapBytes, userMapFile); + } + + @Nullable + private UserAndRoleMap fetchUserAndRoleMapFromCoordinator(String prefix, boolean isInit) { try { return RetryUtils.retry( @@ -179,16 +223,23 @@ private UserAndRoleMap fetchUserAndRoleMapFromCoordinator(String prefix, boolean return tryFetchMapsFromCoordinator(prefix); }, e -> true, - 10 + commonCacheConfig.getMaxSyncRetries() ); } catch (Exception e) { - LOG.makeAlert(e, "Encountered exception while fetching user map for authorizer [%s]", prefix); - if (throwOnFailure) { - throw new RuntimeException(e); - } else { - return null; + LOG.makeAlert(e, "Encountered exception while fetching user and role map for authorizer [%s]", prefix); + if (isInit) { + if (commonCacheConfig.getCacheDirectory() != null) { + try { + LOG.info("Attempting to load user map snapshot from disk."); + return loadUserAndRoleMapFromDisk(prefix); + } + catch (Exception e2) { + LOG.makeAlert(e2, "Encountered exception while loading user-role map snapshot for authorizer [%s]", prefix); + } + } } + return null; } } @@ -198,18 +249,21 @@ private UserAndRoleMap tryFetchMapsFromCoordinator( { Request req = druidLeaderClient.makeRequest( HttpMethod.GET, - StringUtils.format("/druid-ext/basic-security/authorization/%s/cachedSerializedUserMap", prefix) + StringUtils.format("/druid-ext/basic-security/authorization/db/%s/cachedSerializedUserMap", prefix) ); BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go( req, new BytesFullResponseHandler() ); - byte[] userMapBytes = responseHolder.getBytes(); + byte[] userRoleMapBytes = responseHolder.getBytes(); UserAndRoleMap userAndRoleMap = objectMapper.readValue( - userMapBytes, - CoordinatorBasicAuthorizerMetadataStorageUpdater.USER_AND_ROLE_MAP_TYPE_REFERENCE + userRoleMapBytes, + BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE ); + if (userAndRoleMap != null && commonCacheConfig.getCacheDirectory() != null) { + writeMapToDisk(prefix, userRoleMapBytes); + } return userAndRoleMap; } @@ -225,11 +279,12 @@ private void initUserMaps() Authorizer authorizer = entry.getValue(); if (authorizer instanceof BasicRoleBasedAuthorizer) { String authorizerName = entry.getKey(); - BasicRoleBasedAuthorizer roleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer; - UserAndRoleMap userAndRoleMap = fetchUserAndRoleMapFromCoordinator(authorizerName, true); - cachedUserMaps.put(authorizerName, userAndRoleMap.getUserMap()); - cachedRoleMaps.put(authorizerName, userAndRoleMap.getRoleMap()); authorizerPrefixes.add(authorizerName); + UserAndRoleMap userAndRoleMap = fetchUserAndRoleMapFromCoordinator(authorizerName, true); + if (userAndRoleMap != null) { + cachedUserMaps.put(authorizerName, userAndRoleMap.getUserMap()); + cachedRoleMaps.put(authorizerName, userAndRoleMap.getRoleMap()); + } } } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/MetadataStoragePollingBasicAuthorizerCacheManager.java similarity index 91% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheManager.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/MetadataStoragePollingBasicAuthorizerCacheManager.java index 6e60b7601311..36b1d7a3176a 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheManager.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/MetadataStoragePollingBasicAuthorizerCacheManager.java @@ -26,12 +26,12 @@ import java.util.Map; -public class CoordinatorBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager +public class MetadataStoragePollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager { private final BasicAuthorizerMetadataStorageUpdater storageUpdater; @Inject - public CoordinatorBasicAuthorizerCacheManager( + public MetadataStoragePollingBasicAuthorizerCacheManager( BasicAuthorizerMetadataStorageUpdater storageUpdater ) { diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/BasicAuthorizerMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/BasicAuthorizerMetadataStorageUpdater.java index c820878a8e27..2cd148037fe8 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/BasicAuthorizerMetadataStorageUpdater.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/BasicAuthorizerMetadataStorageUpdater.java @@ -26,6 +26,12 @@ import java.util.List; import java.util.Map; +/** + * Implementations of this interface are responsible for connecting directly to the metadata storage, + * modifying the authorizer database state or reading it. This interface is used by the + * MetadataStoragePollingBasicAuthorizerCacheManager (for reads) and the CoordinatorBasicAuthorizerResourceHandler + * (for handling configuration read/writes). + */ public interface BasicAuthorizerMetadataStorageUpdater { void createUser(String prefix, String userName); @@ -46,11 +52,9 @@ public interface BasicAuthorizerMetadataStorageUpdater Map getCachedRoleMap(String prefix); - Map deserializeUserMap(byte[] userMapBytes); - - Map deserializeRoleMap(byte[] roleMapBytes); - byte[] getCurrentUserMapBytes(String prefix); byte[] getCurrentRoleMapBytes(String prefix); + + void refreshAllNotification(); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java index ae5cef84b803..ca80d19712d6 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java @@ -19,11 +19,9 @@ package io.druid.security.basic.authorization.db.updater; -import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; -import com.google.common.collect.Maps; import com.google.inject.Inject; import com.metamx.emitter.EmittingLogger; import io.druid.common.config.ConfigManager; @@ -39,13 +37,16 @@ import io.druid.metadata.MetadataCASUpdate; import io.druid.metadata.MetadataStorageConnector; import io.druid.metadata.MetadataStorageTablesConfig; -import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.BasicAuthCommonCacheConfig; +import io.druid.security.basic.BasicAuthUtils; +import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier; import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission; import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleMapBundle; import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUserMapBundle; import io.druid.security.basic.authorization.entity.UserAndRoleMap; import io.druid.server.security.Action; import io.druid.server.security.Authorizer; @@ -55,15 +56,17 @@ import io.druid.server.security.ResourceType; import org.joda.time.Duration; +import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @ManageLifecycle @@ -72,23 +75,13 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdater implements BasicAu private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthorizerMetadataStorageUpdater.class); - public static final List SUPERUSER_PERMISSIONS = makeSuperUserPermissions(); + private static final long UPDATE_RETRY_DELAY = 1000; - public static final String USERS = "users"; - public static final String ROLES = "roles"; - public static final TypeReference USER_MAP_TYPE_REFERENCE = new TypeReference>() - { - }; - public static final TypeReference ROLE_MAP_TYPE_REFERENCE = new TypeReference>() - { - }; - public static final TypeReference USER_AND_ROLE_MAP_TYPE_REFERENCE = new TypeReference() - { - }; + private static final String USERS = "users"; + private static final String ROLES = "roles"; + + public static final List SUPERUSER_PERMISSIONS = makeSuperUserPermissions(); - public static final String DEFAULT_ADMIN_NAME = "admin"; - public static final String DEFAULT_INTERNAL_SYSTEM_NAME = "druid_system"; - private final AuthorizerMapper authorizerMapper; private final MetadataStorageConnector connector; private final MetadataStorageTablesConfig connectorConfig; @@ -97,16 +90,13 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdater implements BasicAu private final ObjectMapper objectMapper; private final int numRetries = 5; - private final Map> cachedUserMaps; - private final Map cachedSerializedUserMaps; - - private final Map> cachedRoleMaps; - private final Map cachedSerializedRoleMaps; + private final Map cachedUserMaps; + private final Map cachedRoleMaps; private final Set authorizerNames; private final LifecycleLock lifecycleLock = new LifecycleLock(); - private volatile ScheduledExecutorService exec; + private final ScheduledExecutorService exec; private volatile boolean stopped = false; @Inject @@ -120,16 +110,15 @@ public CoordinatorBasicAuthorizerMetadataStorageUpdater( ConfigManager configManager // ConfigManager creates the db table we need, set a dependency here ) { + this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthorizerMetadataStorageUpdater-Exec--%d"); this.authorizerMapper = authorizerMapper; this.connector = connector; this.connectorConfig = connectorConfig; this.commonCacheConfig = commonCacheConfig; this.objectMapper = objectMapper; this.cacheNotifier = cacheNotifier; - this.cachedUserMaps = new HashMap<>(); - this.cachedSerializedUserMaps = new HashMap<>(); - this.cachedRoleMaps = new HashMap<>(); - this.cachedSerializedRoleMaps = new HashMap<>(); + this.cachedUserMaps = new ConcurrentHashMap<>(); + this.cachedRoleMaps = new ConcurrentHashMap<>(); this.authorizerNames = new HashSet<>(); } @@ -154,21 +143,23 @@ public void start() BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer; byte[] userMapBytes = getCurrentUserMapBytes(authorizerName); - Map userMap = deserializeUserMap(userMapBytes); - cachedUserMaps.put(authorizerName, userMap); - cachedSerializedUserMaps.put(authorizerName, userMapBytes); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + userMapBytes + ); + cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes)); byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName); - Map roleMap = deserializeRoleMap(roleMapBytes); - cachedRoleMaps.put(authorizerName, roleMap); - cachedSerializedRoleMaps.put(authorizerName, roleMapBytes); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + roleMapBytes + ); + cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes)); initSuperusers(authorizerName, userMap, roleMap); } } - this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthorizerMetadataStorageUpdater-Exec--%d"); - ScheduledExecutors.scheduleWithFixedDelay( exec, new Duration(commonCacheConfig.getPollingPeriod()), @@ -186,20 +177,24 @@ public ScheduledExecutors.Signal call() throws Exception for (String authorizerName : authorizerNames) { byte[] userMapBytes = getCurrentUserMapBytes(authorizerName); - Map userMap = deserializeUserMap(userMapBytes); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + userMapBytes + ); if (userMapBytes != null) { synchronized (cachedUserMaps) { - cachedUserMaps.put(authorizerName, userMap); - cachedSerializedUserMaps.put(authorizerName, userMapBytes); + cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes)); } } byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName); - Map roleMap = deserializeRoleMap(roleMapBytes); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + roleMapBytes + ); if (roleMapBytes != null) { synchronized (cachedUserMaps) { - cachedRoleMaps.put(authorizerName, roleMap); - cachedSerializedRoleMaps.put(authorizerName, roleMapBytes); + cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes)); } } } @@ -269,58 +264,50 @@ private boolean tryUpdateUserAndRoleMap( ) { try { - synchronized (cachedRoleMaps) { - List updates = new ArrayList<>(); + List updates = new ArrayList<>(); + if (userMap != null) { + updates.add( + new MetadataCASUpdate( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, USERS), + oldUserMapValue, + newUserMapValue + ) + ); + } + + if (roleMap != null) { + updates.add( + new MetadataCASUpdate( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, ROLES), + oldRoleMapValue, + newRoleMapValue + ) + ); + } + + boolean succeeded = connector.compareAndSwap(updates); + if (succeeded) { if (userMap != null) { - updates.add( - new MetadataCASUpdate( - connectorConfig.getConfigTable(), - MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, - MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, - getPrefixedKeyColumn(prefix, USERS), - oldUserMapValue, - newUserMapValue - ) - ); + cachedUserMaps.put(prefix, new BasicAuthorizerUserMapBundle(userMap, newUserMapValue)); } - if (roleMap != null) { - updates.add( - new MetadataCASUpdate( - connectorConfig.getConfigTable(), - MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, - MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, - getPrefixedKeyColumn(prefix, ROLES), - oldRoleMapValue, - newRoleMapValue - ) - ); + cachedRoleMaps.put(prefix, new BasicAuthorizerRoleMapBundle(roleMap, newRoleMapValue)); } - boolean succeeded = connector.compareAndSwap(updates); - if (succeeded) { - if (userMap != null) { - cachedUserMaps.put(prefix, userMap); - cachedSerializedUserMaps.put(prefix, newUserMapValue); - } - if (roleMap != null) { - cachedRoleMaps.put(prefix, roleMap); - cachedSerializedRoleMaps.put(prefix, newRoleMapValue); - } + byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(prefix); + cacheNotifier.addUpdate(prefix, serializedUserAndRoleMap); - UserAndRoleMap userAndRoleMap = new UserAndRoleMap( - cachedUserMaps.get(prefix), - cachedRoleMaps.get(prefix) - ); - - byte[] serializedUserAndRoleMap = objectMapper.writeValueAsBytes(userAndRoleMap); - cacheNotifier.addUpdate(prefix, serializedUserAndRoleMap); - - return true; - } else { - return false; - } + return true; + } else { + return false; } + } catch (Exception e) { throw new RuntimeException(e); @@ -379,15 +366,19 @@ public void setPermissions( } @Override + @Nullable public Map getCachedUserMap(String prefix) { - return cachedUserMaps.get(prefix); + BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix); + return userMapBundle == null ? null : userMapBundle.getUserMap(); } @Override + @Nullable public Map getCachedRoleMap(String prefix) { - return cachedRoleMaps.get(prefix); + BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix); + return roleMapBundle == null ? null : roleMapBundle.getRoleMap(); } @Override @@ -413,57 +404,32 @@ public byte[] getCurrentRoleMapBytes(String prefix) } @Override - public Map deserializeUserMap(byte[] userMapBytes) + public void refreshAllNotification() { - Map userMap; - if (userMapBytes == null) { - userMap = Maps.newHashMap(); - } else { - try { - userMap = objectMapper.readValue(userMapBytes, USER_MAP_TYPE_REFERENCE); - } - catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - return userMap; + authorizerNames.forEach( + (authorizerName) -> { + try { + byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(authorizerName); + cacheNotifier.addUpdate(authorizerName, serializedUserAndRoleMap); + } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + ); } - public byte[] serializeUserMap(Map userMap) + private byte[] getCurrentUserAndRoleMapSerialized(String prefix) throws IOException { - try { - return objectMapper.writeValueAsBytes(userMap); - } - catch (IOException ioe) { - throw new ISE("WTF? Couldn't serialize userMap!"); - } - } + BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix); + BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix); - @Override - public Map deserializeRoleMap(byte[] roleMapBytes) - { - Map roleMap; - if (roleMapBytes == null) { - roleMap = Maps.newHashMap(); - } else { - try { - roleMap = objectMapper.readValue(roleMapBytes, ROLE_MAP_TYPE_REFERENCE); - } - catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - return roleMap; - } + UserAndRoleMap userAndRoleMap = new UserAndRoleMap( + userMapBundle == null ? null : userMapBundle.getUserMap(), + roleMapBundle == null ? null : roleMapBundle.getRoleMap() + ); - public byte[] serializeRoleMap(Map roleMap) - { - try { - return objectMapper.writeValueAsBytes(roleMap); - } - catch (IOException ioe) { - throw new ISE("WTF? Couldn't serialize roleMap!"); - } + return objectMapper.writeValueAsBytes(userAndRoleMap); } private void createUserInternal(String prefix, String userName) @@ -475,6 +441,12 @@ private void createUserInternal(String prefix, String userName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not create user[%s] due to concurrent update contention.", userName); } @@ -488,6 +460,12 @@ private void deleteUserInternal(String prefix, String userName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not delete user[%s] due to concurrent update contention.", userName); } @@ -501,6 +479,12 @@ private void createRoleInternal(String prefix, String roleName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not create role[%s] due to concurrent update contention.", roleName); } @@ -514,6 +498,12 @@ private void deleteRoleInternal(String prefix, String roleName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not delete role[%s] due to concurrent update contention.", roleName); } @@ -527,6 +517,12 @@ private void assignRoleInternal(String prefix, String userName, String roleName) } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not assign role[%s] to user[%s] due to concurrent update contention.", roleName, userName); } @@ -540,6 +536,12 @@ private void unassignRoleInternal(String prefix, String userName, String roleNam } else { attempts++; } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } } throw new ISE("Could not unassign role[%s] from user[%s] due to concurrent update contention.", roleName, userName); } @@ -553,6 +555,12 @@ private void setPermissionsInternal(String prefix, String roleName, List userMap = deserializeUserMap(oldValue); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap(objectMapper, oldValue); if (userMap.get(userName) != null) { throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); } else { userMap.put(userName, new BasicAuthorizerUser(userName, null)); } - byte[] newValue = serializeUserMap(userMap); + byte[] newValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); return tryUpdateUserMap(prefix, userMap, oldValue, newValue); } private boolean deleteUserOnce(String prefix, String userName) { byte[] oldValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldValue); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap(objectMapper, oldValue); if (userMap.get(userName) == null) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } else { userMap.remove(userName); } - byte[] newValue = serializeUserMap(userMap); + byte[] newValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); return tryUpdateUserMap(prefix, userMap, oldValue, newValue); } private boolean createRoleOnce(String prefix, String roleName) { byte[] oldValue = getCurrentRoleMapBytes(prefix); - Map roleMap = deserializeRoleMap(oldValue); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(objectMapper, oldValue); if (roleMap.get(roleName) != null) { throw new BasicSecurityDBResourceException("Role [%s] already exists.", roleName); } else { roleMap.put(roleName, new BasicAuthorizerRole(roleName, null)); } - byte[] newValue = serializeRoleMap(roleMap); + byte[] newValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap); return tryUpdateRoleMap(prefix, roleMap, oldValue, newValue); } private boolean deleteRoleOnce(String prefix, String roleName) { byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); - Map roleMap = deserializeRoleMap(oldRoleMapValue); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); if (roleMap.get(roleName) == null) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } else { @@ -607,12 +618,15 @@ private boolean deleteRoleOnce(String prefix, String roleName) } byte[] oldUserMapValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldUserMapValue); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); for (BasicAuthorizerUser user : userMap.values()) { user.getRoles().remove(roleName); } - byte[] newUserMapValue = serializeUserMap(userMap); - byte[] newRoleMapValue = serializeRoleMap(roleMap); + byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); + byte[] newRoleMapValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap); return tryUpdateUserAndRoleMap( prefix, @@ -624,13 +638,19 @@ private boolean deleteRoleOnce(String prefix, String roleName) private boolean assignRoleOnce(String prefix, String userName, String roleName) { byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); - Map roleMap = deserializeRoleMap(oldRoleMapValue); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); if (roleMap.get(roleName) == null) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } byte[] oldUserMapValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldUserMapValue); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); BasicAuthorizerUser user = userMap.get(userName); if (userMap.get(userName) == null) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -641,7 +661,7 @@ private boolean assignRoleOnce(String prefix, String userName, String roleName) } user.getRoles().add(roleName); - byte[] newUserMapValue = serializeUserMap(userMap); + byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); // Role map is unchanged, but submit as an update to ensure that the table didn't change (e.g., role deleted) return tryUpdateUserAndRoleMap( @@ -654,13 +674,19 @@ private boolean assignRoleOnce(String prefix, String userName, String roleName) private boolean unassignRoleOnce(String prefix, String userName, String roleName) { byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); - Map roleMap = deserializeRoleMap(oldRoleMapValue); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); if (roleMap.get(roleName) == null) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } byte[] oldUserMapValue = getCurrentUserMapBytes(prefix); - Map userMap = deserializeUserMap(oldUserMapValue); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); BasicAuthorizerUser user = userMap.get(userName); if (userMap.get(userName) == null) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -671,7 +697,7 @@ private boolean unassignRoleOnce(String prefix, String userName, String roleName } user.getRoles().remove(roleName); - byte[] newUserMapValue = serializeUserMap(userMap); + byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); // Role map is unchanged, but submit as an update to ensure that the table didn't change (e.g., role deleted) return tryUpdateUserAndRoleMap( @@ -684,7 +710,10 @@ private boolean unassignRoleOnce(String prefix, String userName, String roleName private boolean setPermissionsOnce(String prefix, String roleName, List permissions) { byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); - Map roleMap = deserializeRoleMap(oldRoleMapValue); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); if (roleMap.get(roleName) == null) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -692,7 +721,7 @@ private boolean setPermissionsOnce(String prefix, String roleName, List roleMap ) { - if (!roleMap.containsKey(DEFAULT_ADMIN_NAME)) { - createRoleInternal(authorizerName, DEFAULT_ADMIN_NAME); - setPermissionsInternal(authorizerName, DEFAULT_ADMIN_NAME, SUPERUSER_PERMISSIONS); + if (!roleMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { + createRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME); + setPermissionsInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, SUPERUSER_PERMISSIONS); } - if (!roleMap.containsKey(DEFAULT_INTERNAL_SYSTEM_NAME)) { - createRoleInternal(authorizerName, DEFAULT_INTERNAL_SYSTEM_NAME); - setPermissionsInternal(authorizerName, DEFAULT_INTERNAL_SYSTEM_NAME, SUPERUSER_PERMISSIONS); + if (!roleMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) { + createRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME); + setPermissionsInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, SUPERUSER_PERMISSIONS); } - if (!userMap.containsKey(DEFAULT_INTERNAL_SYSTEM_NAME)) { - createUserInternal(authorizerName, DEFAULT_INTERNAL_SYSTEM_NAME); - assignRoleInternal(authorizerName, DEFAULT_INTERNAL_SYSTEM_NAME, DEFAULT_INTERNAL_SYSTEM_NAME); + if (!userMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) { + createUserInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME); + assignRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, BasicAuthUtils.INTERNAL_USER_NAME); } - if (!userMap.containsKey(DEFAULT_ADMIN_NAME)) { - createUserInternal(authorizerName, DEFAULT_ADMIN_NAME); - assignRoleInternal(authorizerName, DEFAULT_ADMIN_NAME, DEFAULT_ADMIN_NAME); + if (!userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { + createUserInternal(authorizerName, BasicAuthUtils.ADMIN_NAME); + assignRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.ADMIN_NAME); } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/NoopBasicAuthorizerMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/NoopBasicAuthorizerMetadataStorageUpdater.java deleted file mode 100644 index 4d767e593ea9..000000000000 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/NoopBasicAuthorizerMetadataStorageUpdater.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package io.druid.security.basic.authorization.db.updater; - -import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; -import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; -import io.druid.server.security.ResourceAction; - -import java.util.List; -import java.util.Map; - -public class NoopBasicAuthorizerMetadataStorageUpdater implements BasicAuthorizerMetadataStorageUpdater -{ - @Override - public void createUser(String prefix, String userName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void deleteUser(String prefix, String userName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void createRole(String prefix, String roleName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void deleteRole(String prefix, String roleName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void assignRole(String prefix, String userName, String roleName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void unassignRole(String prefix, String userName, String roleName) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public void setPermissions( - String prefix, String roleName, List permissions - ) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public Map getCachedUserMap(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public Map getCachedRoleMap(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public Map deserializeUserMap(byte[] userMapBytes) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public Map deserializeRoleMap(byte[] roleMapBytes) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public byte[] getCurrentUserMapBytes(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } - - @Override - public byte[] getCurrentRoleMapBytes(String prefix) - { - throw new UnsupportedOperationException("not supported"); - } -} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java index 4ffb4863ff00..1951cfb1128f 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java @@ -54,13 +54,49 @@ public BasicAuthorizerResource( this.resourceHandler = resourceHandler; } + /** + * @param req HTTP request + * + * @return Load status of authenticator DB caches + */ + @GET + @Path("/loadStatus") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getLoadStatus( + @Context HttpServletRequest req + ) + { + return resourceHandler.getLoadStatus(); + } + + /** + * @param req HTTP request + * + * Sends an "update" notification to all services with the current user/role database state, + * causing them to refresh their DB cache state. + */ + @GET + @Path("/refreshAll") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response refreshAll( + @Context HttpServletRequest req + ) + { + return resourceHandler.refreshAll(); + } + + /** * @param req HTTP request * * @return List of all users */ @GET - @Path("/{authorizerName}/users") + @Path("/db/{authorizerName}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -79,7 +115,7 @@ public Response getAllUsers( * @return Name, roles, and permissions of the user with userName, 400 error response if user doesn't exist */ @GET - @Path("/{authorizerName}/users/{userName}") + @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -102,7 +138,7 @@ public Response getUser( * @return OK response, or 400 error response if user already exists */ @POST - @Path("/{authorizerName}/users/{userName}") + @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -124,7 +160,7 @@ public Response createUser( * @return OK response, or 400 error response if user doesn't exist */ @DELETE - @Path("/{authorizerName}/users/{userName}") + @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -143,7 +179,7 @@ public Response deleteUser( * @return List of all roles */ @GET - @Path("/{authorizerName}/roles") + @Path("/db/{authorizerName}/roles") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -164,7 +200,7 @@ public Response getAllRoles( * @return Role name, users with role, and permissions of role. 400 error if role doesn't exist. */ @GET - @Path("/{authorizerName}/roles/{roleName}") + @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -187,7 +223,7 @@ public Response getRole( * @return OK response, 400 error if role already exists */ @POST - @Path("/{authorizerName}/roles/{roleName}") + @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -209,7 +245,7 @@ public Response createRole( * @return OK response, 400 error if role doesn't exist. */ @DELETE - @Path("/{authorizerName}/roles/{roleName}") + @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -232,7 +268,7 @@ public Response deleteRole( * @return OK response. 400 error if user/role don't exist, or if user already has the role */ @POST - @Path("/{authorizerName}/users/{userName}/roles/{roleName}") + @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -256,7 +292,7 @@ public Response assignRoleToUser( * @return OK response. 400 error if user/role don't exist, or if user does not have the role. */ @DELETE - @Path("/{authorizerName}/users/{userName}/roles/{roleName}") + @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -280,7 +316,7 @@ public Response unassignRoleFromUser( * @return OK response. 400 error if role doesn't exist. */ @POST - @Path("/{authorizerName}/roles/{roleName}/permissions") + @Path("/db/{authorizerName}/roles/{roleName}/permissions") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) @@ -300,7 +336,7 @@ public Response setRolePermissions( * @return serialized user map */ @GET - @Path("/{authorizerName}/cachedSerializedUserMap") + @Path("/db/{authorizerName}/cachedSerializedUserMap") @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResourceHandler.java index 41cdbdf727a9..2186f8d86fb0 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResourceHandler.java @@ -24,8 +24,14 @@ import javax.ws.rs.core.Response; import java.util.List; +/** + * Handles authorizer-related API calls. Coordinator and non-coordinator methods are combined here because of an + * inability to selectively inject jetty resources in configure(Binder binder) of the extension module based + * on node type. + */ public interface BasicAuthorizerResourceHandler { + // coordinator methods Response getAllUsers(String authorizerName); Response getUser(String authorizerName, String userName, boolean isFull); @@ -50,5 +56,11 @@ public interface BasicAuthorizerResourceHandler Response getCachedMaps(String authorizerName); + Response refreshAll(); + + // non-coordinator methods Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap); + + // common + Response getLoadStatus(); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/CoordinatorBasicAuthorizerResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/CoordinatorBasicAuthorizerResourceHandler.java index 2fa9a1d70cb2..407b893952af 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/CoordinatorBasicAuthorizerResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/CoordinatorBasicAuthorizerResourceHandler.java @@ -26,6 +26,7 @@ import io.druid.guice.annotations.Smile; import io.druid.java.util.common.StringUtils; import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater; @@ -39,6 +40,7 @@ import io.druid.server.security.ResourceAction; import javax.ws.rs.core.Response; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -83,7 +85,8 @@ public Response getAllUsers(String authorizerName) return makeResponseForAuthorizerNotFound(authorizerName); } - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authorizerName) ); return Response.ok(userMap.keySet()).build(); @@ -146,7 +149,8 @@ public Response getAllRoles(String authorizerName) return makeResponseForAuthorizerNotFound(authorizerName); } - Map roleMap = storageUpdater.deserializeRoleMap( + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, storageUpdater.getCurrentRoleMapBytes(authorizerName) ); @@ -271,12 +275,31 @@ public Response getCachedMaps(String authorizerName) return Response.ok(userAndRoleMap).build(); } + @Override + public Response refreshAll() + { + storageUpdater.refreshAllNotification(); + return Response.ok().build(); + } + @Override public Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap) { return Response.status(Response.Status.NOT_FOUND).build(); } + @Override + public Response getLoadStatus() + { + Map loadStatus = new HashMap<>(); + authorizerMap.forEach( + (authorizerName, authorizer) -> { + loadStatus.put(authorizerName, storageUpdater.getCachedUserMap(authorizerName) != null); + } + ); + return Response.ok(loadStatus).build(); + } + private static Response makeResponseForAuthorizerNotFound(String authorizerName) { return Response.status(Response.Status.BAD_REQUEST) @@ -298,7 +321,8 @@ private static Response makeResponseForBasicSecurityDBResourceException(BasicSec private Response getUserSimple(String authorizerName, String userName) { - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authorizerName) ); @@ -316,11 +340,13 @@ private Response getUserSimple(String authorizerName, String userName) private Response getUserFull(String authorizerName, String userName) { - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authorizerName) ); - Map roleMap = storageUpdater.deserializeRoleMap( + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, storageUpdater.getCurrentRoleMapBytes(authorizerName) ); @@ -350,7 +376,8 @@ private Response getUserFull(String authorizerName, String userName) private Response getRoleSimple(String authorizerName, String roleName) { - Map roleMap = storageUpdater.deserializeRoleMap( + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, storageUpdater.getCurrentRoleMapBytes(authorizerName) ); @@ -368,11 +395,13 @@ private Response getRoleSimple(String authorizerName, String roleName) private Response getRoleFull(String authorizerName, String roleName) { - Map roleMap = storageUpdater.deserializeRoleMap( + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, storageUpdater.getCurrentRoleMapBytes(authorizerName) ); - Map userMap = storageUpdater.deserializeUserMap( + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, storageUpdater.getCurrentUserMapBytes(authorizerName) ); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/DefaultBasicAuthorizerResourceHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/DefaultBasicAuthorizerResourceHandler.java index 6bab03487274..2531bebe6d59 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/DefaultBasicAuthorizerResourceHandler.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/DefaultBasicAuthorizerResourceHandler.java @@ -31,6 +31,7 @@ import io.druid.server.security.ResourceAction; import javax.ws.rs.core.Response; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -138,6 +139,12 @@ public Response getCachedMaps(String authorizerName) return NOT_FOUND_RESPONSE; } + @Override + public Response refreshAll() + { + return NOT_FOUND_RESPONSE; + } + @Override public Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap) { @@ -156,4 +163,16 @@ public Response authorizerUpdateListener(String authorizerName, byte[] serialize cacheManager.handleAuthorizerUpdate(authorizerName, serializedUserAndRoleMap); return Response.ok().build(); } + + @Override + public Response getLoadStatus() + { + Map loadStatus = new HashMap<>(); + authorizerMap.forEach( + (authorizerName, authorizer) -> { + loadStatus.put(authorizerName, cacheManager.getUserMap(authorizerName) != null); + } + ); + return Response.ok(loadStatus).build(); + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerPermission.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerPermission.java index d0da19600e4b..643b84a98c3e 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerPermission.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerPermission.java @@ -54,6 +54,7 @@ public BasicAuthorizerPermission( } catch (PatternSyntaxException pse) { throw new BasicSecurityDBResourceException( + pse, "Invalid permission, resource name regex[%s] does not compile.", resourceAction.getResource().getName() ); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleMapBundle.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleMapBundle.java new file mode 100644 index 000000000000..9ea457a7f69d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleMapBundle.java @@ -0,0 +1,53 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.authorization.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public class BasicAuthorizerRoleMapBundle +{ + private final Map roleMap; + private final byte[] serializedRoleMap; + + @JsonCreator + public BasicAuthorizerRoleMapBundle( + @JsonProperty("roleMap") Map roleMap, + @JsonProperty("serializedRoleMap") byte[] serializedRoleMap + ) + { + this.roleMap = roleMap; + this.serializedRoleMap = serializedRoleMap; + } + + @JsonProperty + public Map getRoleMap() + { + return roleMap; + } + + @JsonProperty + public byte[] getSerializedRoleMap() + { + return serializedRoleMap; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserMapBundle.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserMapBundle.java new file mode 100644 index 000000000000..109c36ec07f0 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserMapBundle.java @@ -0,0 +1,53 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.authorization.entity; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public class BasicAuthorizerUserMapBundle +{ + private final Map userMap; + private final byte[] serializedUserMap; + + @JsonCreator + public BasicAuthorizerUserMapBundle( + @JsonProperty("userMap") Map userMap, + @JsonProperty("serializedUserMap") byte[] serializedUserMap + ) + { + this.userMap = userMap; + this.serializedUserMap = serializedUserMap; + } + + @JsonProperty + public Map getUserMap() + { + return userMap; + } + + @JsonProperty + public byte[] getSerializedUserMap() + { + return serializedUserMap; + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java index 5c0afedf7395..c453126f6991 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java @@ -29,7 +29,7 @@ public class BasicAuthUtilsTest public void testHashPassword() { char[] password = "HELLO".toCharArray(); - int iterations = BasicAuthUtils.KEY_ITERATIONS; + int iterations = BasicAuthUtils.DEFAULT_KEY_ITERATIONS; byte[] salt = BasicAuthUtils.generateSalt(); byte[] hash = BasicAuthUtils.hashPassword(password, salt, iterations); diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest.java index 55612adbd178..3ea3b11847d6 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest.java @@ -33,13 +33,13 @@ import io.druid.initialization.Initialization; import io.druid.metadata.MetadataStorageTablesConfig; import io.druid.metadata.TestDerbyConnector; +import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicSecurityDBResourceException; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.authentication.BasicHTTPEscalator; -import io.druid.security.basic.BasicAuthCommonCacheConfig; -import io.druid.security.basic.authentication.db.cache.NoopBasicAuthenticatorCacheNotifier; import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; import io.druid.server.DruidNode; @@ -66,14 +66,15 @@ public class CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest private TestDerbyConnector connector; private MetadataStorageTablesConfig tablesConfig; private CoordinatorBasicAuthenticatorMetadataStorageUpdater updater; + private ObjectMapper objectMapper; @Before public void setUp() throws Exception { + objectMapper = new ObjectMapper(new SmileFactory()); connector = derbyConnectorRule.getConnector(); tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); connector.createConfigTable(); - //injector = setupInjector(); updater = new CoordinatorBasicAuthenticatorMetadataStorageUpdater( new AuthenticatorMapper( @@ -86,14 +87,15 @@ public void setUp() throws Exception null, null, null, + null, null ) ) ), connector, tablesConfig, - new BasicAuthCommonCacheConfig(null, null), - new ObjectMapper(new SmileFactory()), + new BasicAuthCommonCacheConfig(null, null, null, null), + objectMapper, new NoopBasicAuthenticatorCacheNotifier(), null ); @@ -114,7 +116,10 @@ public void createUser() Map expectedUserMap = ImmutableMap.of( "druid", new BasicAuthenticatorUser("druid", null) ); - Map actualUserMap = updater.deserializeUserMap(updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)); + Map actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); Assert.assertEquals(expectedUserMap, actualUserMap); // create duplicate should fail @@ -129,7 +134,10 @@ public void deleteUser() updater.createUser(AUTHENTICATOR_NAME, "druid"); updater.deleteUser(AUTHENTICATOR_NAME, "druid"); Map expectedUserMap = ImmutableMap.of(); - Map actualUserMap = updater.deserializeUserMap(updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)); + Map actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); Assert.assertEquals(expectedUserMap, actualUserMap); // delete non-existent user should fail @@ -142,9 +150,12 @@ public void deleteUser() public void setCredentials() { updater.createUser(AUTHENTICATOR_NAME, "druid"); - updater.setUserCredentials(AUTHENTICATOR_NAME, "druid", "helloworld".toCharArray()); + updater.setUserCredentials(AUTHENTICATOR_NAME, "druid", new BasicAuthenticatorCredentialUpdate("helloworld", null)); - Map userMap = updater.deserializeUserMap(updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); BasicAuthenticatorCredentials credentials = userMap.get("druid").getCredentials(); byte[] recalculatedHash = BasicAuthUtils.hashPassword( @@ -187,6 +198,7 @@ public void configure(Binder binder) null, null, null, + null, null ) ) diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorResourceTest.java index 52130f1440a4..2c5e0a8e2df6 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorResourceTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorResourceTest.java @@ -34,14 +34,14 @@ import io.druid.initialization.Initialization; import io.druid.metadata.MetadataStorageTablesConfig; import io.druid.metadata.TestDerbyConnector; +import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.authentication.BasicHTTPAuthenticator; import io.druid.security.basic.authentication.BasicHTTPEscalator; -import io.druid.security.basic.BasicAuthCommonCacheConfig; -import io.druid.security.basic.authentication.db.cache.NoopBasicAuthenticatorCacheNotifier; import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater; import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResource; import io.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials; import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; import io.druid.server.DruidNode; @@ -96,6 +96,7 @@ public void setUp() throws Exception "druid", "druid", null, + null, null ), AUTHENTICATOR_NAME2, @@ -106,6 +107,7 @@ public void setUp() throws Exception "druid", "druid", null, + null, null ) ) @@ -115,7 +117,7 @@ public void setUp() throws Exception authenticatorMapper, connector, tablesConfig, - new BasicAuthCommonCacheConfig(null, null), + new BasicAuthCommonCacheConfig(null, null, null, null), new ObjectMapper(new SmileFactory()), new NoopBasicAuthenticatorCacheNotifier(), null @@ -153,15 +155,15 @@ public void testGetAllUsers() { Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME); Assert.assertEquals(200, response.getStatus()); - Assert.assertEquals(ImmutableSet.of("admin", "druid_system"), response.getEntity()); + Assert.assertEquals(ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), response.getEntity()); resource.createUser(req, AUTHENTICATOR_NAME, "druid"); resource.createUser(req, AUTHENTICATOR_NAME, "druid2"); resource.createUser(req, AUTHENTICATOR_NAME, "druid3"); Set expectedUsers = ImmutableSet.of( - "admin", - "druid_system", + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, "druid", "druid2", "druid3" @@ -177,7 +179,7 @@ public void testSeparateDatabaseTables() { Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME); Assert.assertEquals(200, response.getStatus()); - Assert.assertEquals(ImmutableSet.of("admin", "druid_system"), response.getEntity()); + Assert.assertEquals(ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), response.getEntity()); resource.createUser(req, AUTHENTICATOR_NAME, "druid"); resource.createUser(req, AUTHENTICATOR_NAME, "druid2"); @@ -188,16 +190,16 @@ public void testSeparateDatabaseTables() resource.createUser(req, AUTHENTICATOR_NAME2, "druid6"); Set expectedUsers = ImmutableSet.of( - "admin", - "druid_system", + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, "druid", "druid2", "druid3" ); Set expectedUsers2 = ImmutableSet.of( - "admin", - "druid_system", + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, "druid4", "druid5", "druid6" @@ -241,7 +243,12 @@ public void testUserCredentials() Response response = resource.createUser(req, AUTHENTICATOR_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = resource.updateUserCredentials(req, AUTHENTICATOR_NAME, "druid", "helloworld"); + response = resource.updateUserCredentials( + req, + AUTHENTICATOR_NAME, + "druid", + new BasicAuthenticatorCredentialUpdate("helloworld", null) + ); Assert.assertEquals(200, response.getStatus()); response = resource.getUser(req, AUTHENTICATOR_NAME, "druid"); @@ -255,7 +262,7 @@ public void testUserCredentials() int iterations = credentials.getIterations(); Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length); Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length); - Assert.assertEquals(BasicAuthUtils.KEY_ITERATIONS, iterations); + Assert.assertEquals(BasicAuthUtils.DEFAULT_KEY_ITERATIONS, iterations); byte[] recalculatedHash = BasicAuthUtils.hashPassword( "helloworld".toCharArray(), @@ -271,7 +278,12 @@ public void testUserCredentials() Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); - response = resource.updateUserCredentials(req, AUTHENTICATOR_NAME, "druid", "helloworld"); + response = resource.updateUserCredentials( + req, + AUTHENTICATOR_NAME, + "druid", + new BasicAuthenticatorCredentialUpdate("helloworld", null) + ); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/NoopBasicAuthenticatorCacheNotifier.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java similarity index 88% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/NoopBasicAuthenticatorCacheNotifier.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java index 7f8b430d5191..22809cc8d4c6 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/NoopBasicAuthenticatorCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java @@ -17,7 +17,9 @@ * under the License. */ -package io.druid.security.basic.authentication.db.cache; +package io.druid.security.authentication; + +import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier; public class NoopBasicAuthenticatorCacheNotifier implements BasicAuthenticatorCacheNotifier { diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/BasicRoleBasedAuthorizerTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/BasicRoleBasedAuthorizerTest.java index daaec56ae8a5..6132753c684d 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/BasicRoleBasedAuthorizerTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/BasicRoleBasedAuthorizerTest.java @@ -27,8 +27,7 @@ import io.druid.metadata.TestDerbyConnector; import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.security.basic.authorization.db.cache.CoordinatorBasicAuthorizerCacheManager; -import io.druid.security.basic.authorization.db.cache.NoopBasicAuthorizerCacheNotifier; +import io.druid.security.basic.authorization.db.cache.MetadataStoragePollingBasicAuthorizerCacheManager; import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; import io.druid.server.security.Access; import io.druid.server.security.Action; @@ -78,7 +77,7 @@ public void setUp() throws Exception ), connector, tablesConfig, - new BasicAuthCommonCacheConfig(null, null), + new BasicAuthCommonCacheConfig(null, null, null, null), new ObjectMapper(new SmileFactory()), new NoopBasicAuthorizerCacheNotifier(), null @@ -87,7 +86,7 @@ public void setUp() throws Exception updater.start(); authorizer = new BasicRoleBasedAuthorizer( - new CoordinatorBasicAuthorizerCacheManager( + new MetadataStoragePollingBasicAuthorizerCacheManager( updater ), AUTHORIZER_NAME, diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java index 7f4811d33cd0..d1f6b1fb17ea 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java @@ -27,11 +27,10 @@ import com.google.common.collect.Maps; import io.druid.metadata.MetadataStorageTablesConfig; import io.druid.metadata.TestDerbyConnector; +import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicSecurityDBResourceException; -import io.druid.security.basic.BasicAuthCommonCacheConfig; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.security.basic.authorization.db.cache.NoopBasicAuthorizerCacheNotifier; import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission; import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; @@ -55,8 +54,10 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdaterTest private final static String AUTHORIZER_NAME = "test"; private final static Map BASE_USER_MAP = ImmutableMap.of( - BasicAuthUtils.ADMIN_NAME, new BasicAuthorizerUser(BasicAuthUtils.ADMIN_NAME, ImmutableSet.of(BasicAuthUtils.ADMIN_NAME)), - BasicAuthUtils.INTERNAL_USER_NAME, new BasicAuthorizerUser(BasicAuthUtils.INTERNAL_USER_NAME, ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + new BasicAuthorizerUser(BasicAuthUtils.ADMIN_NAME, ImmutableSet.of(BasicAuthUtils.ADMIN_NAME)), + BasicAuthUtils.INTERNAL_USER_NAME, + new BasicAuthorizerUser(BasicAuthUtils.INTERNAL_USER_NAME, ImmutableSet.of( BasicAuthUtils.INTERNAL_USER_NAME)) ); @@ -82,10 +83,12 @@ BasicAuthUtils.INTERNAL_USER_NAME, new BasicAuthorizerUser(BasicAuthUtils.INTERN private TestDerbyConnector connector; private MetadataStorageTablesConfig tablesConfig; private CoordinatorBasicAuthorizerMetadataStorageUpdater updater; + private ObjectMapper objectMapper; @Before public void setUp() throws Exception { + objectMapper = new ObjectMapper(new SmileFactory()); connector = derbyConnectorRule.getConnector(); tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); connector.createConfigTable(); @@ -104,8 +107,8 @@ public void setUp() throws Exception ), connector, tablesConfig, - new BasicAuthCommonCacheConfig(null, null), - new ObjectMapper(new SmileFactory()), + new BasicAuthCommonCacheConfig(null, null, null, null), + objectMapper, new NoopBasicAuthorizerCacheNotifier(), null ); @@ -120,14 +123,16 @@ public void testCreateDeleteUser() throws Exception updater.createUser(AUTHORIZER_NAME, "druid"); Map expectedUserMap = Maps.newHashMap(BASE_USER_MAP); expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of())); - Map actualUserMap = updater.deserializeUserMap( + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); Assert.assertEquals(expectedUserMap, actualUserMap); updater.deleteUser(AUTHORIZER_NAME, "druid"); expectedUserMap.remove("druid"); - actualUserMap = updater.deserializeUserMap( + actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); Assert.assertEquals(expectedUserMap, actualUserMap); @@ -157,14 +162,16 @@ public void testCreateDeleteRole() throws Exception updater.createRole(AUTHORIZER_NAME, "druid"); Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); expectedRoleMap.put("druid", new BasicAuthorizerRole("druid", ImmutableList.of())); - Map actualRoleMap = updater.deserializeRoleMap( + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); Assert.assertEquals(expectedRoleMap, actualRoleMap); updater.deleteRole(AUTHORIZER_NAME, "druid"); expectedRoleMap.remove("druid"); - actualRoleMap = updater.deserializeRoleMap( + actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); Assert.assertEquals(expectedRoleMap, actualRoleMap); @@ -201,11 +208,13 @@ public void testAddAndRemoveRole() throws Exception Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); - Map actualUserMap = updater.deserializeUserMap( + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); - Map actualRoleMap = updater.deserializeRoleMap( + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); @@ -214,7 +223,8 @@ public void testAddAndRemoveRole() throws Exception updater.unassignRole(AUTHORIZER_NAME, "druid", "druidRole"); expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of())); - actualUserMap = updater.deserializeUserMap( + actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); @@ -266,11 +276,13 @@ public void testUnassignInvalidRoleAssignmentFails() throws Exception Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); - Map actualUserMap = updater.deserializeUserMap( + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); - Map actualRoleMap = updater.deserializeRoleMap( + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); @@ -306,11 +318,13 @@ public void testSetRolePermissions() throws Exception new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(permsToAdd)) ); - Map actualUserMap = updater.deserializeUserMap( + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, updater.getCurrentUserMapBytes(AUTHORIZER_NAME) ); - Map actualRoleMap = updater.deserializeRoleMap( + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); @@ -319,7 +333,8 @@ public void testSetRolePermissions() throws Exception updater.setPermissions(AUTHORIZER_NAME, "druidRole", null); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", null)); - actualRoleMap = updater.deserializeRoleMap( + actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) ); diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java index 77de184e5040..763b1f7d5f94 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java @@ -27,10 +27,9 @@ import com.google.common.collect.Sets; import io.druid.metadata.MetadataStorageTablesConfig; import io.druid.metadata.TestDerbyConnector; -import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.BasicAuthCommonCacheConfig; +import io.druid.security.basic.BasicAuthUtils; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.security.basic.authorization.db.cache.NoopBasicAuthorizerCacheNotifier; import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResource; import io.druid.security.basic.authorization.endpoint.CoordinatorBasicAuthorizerResourceHandler; @@ -107,7 +106,7 @@ public void setUp() throws Exception authorizerMapper, connector, tablesConfig, - new BasicAuthCommonCacheConfig(null, null), + new BasicAuthCommonCacheConfig(null, null, null, null), new ObjectMapper(new SmileFactory()), new NoopBasicAuthorizerCacheNotifier(), null diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/NoopBasicAuthorizerCacheNotifier.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java similarity index 88% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/NoopBasicAuthorizerCacheNotifier.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java index b5376ef98280..ca37a214c820 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/NoopBasicAuthorizerCacheNotifier.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java @@ -17,7 +17,9 @@ * under the License. */ -package io.druid.security.basic.authorization.db.cache; +package io.druid.security.authorization; + +import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier; public class NoopBasicAuthorizerCacheNotifier implements BasicAuthorizerCacheNotifier { diff --git a/integration-tests/docker/broker.conf b/integration-tests/docker/broker.conf index 44a3de1b60f7..e7e744d5db8a 100644 --- a/integration-tests/docker/broker.conf +++ b/integration-tests/docker/broker.conf @@ -27,6 +27,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/broker -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/docker/coordinator.conf b/integration-tests/docker/coordinator.conf index eb5fd800c7e4..41ed94889acc 100644 --- a/integration-tests/docker/coordinator.conf +++ b/integration-tests/docker/coordinator.conf @@ -21,6 +21,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/coordinator -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/docker/historical.conf b/integration-tests/docker/historical.conf index 34c5e659e0f1..58cbd84e2416 100644 --- a/integration-tests/docker/historical.conf +++ b/integration-tests/docker/historical.conf @@ -25,6 +25,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/historical -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/docker/middlemanager.conf b/integration-tests/docker/middlemanager.conf index f935f72fa139..2ca1560fb2c7 100644 --- a/integration-tests/docker/middlemanager.conf +++ b/integration-tests/docker/middlemanager.conf @@ -28,6 +28,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/middleManager -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/docker/overlord.conf b/integration-tests/docker/overlord.conf index 2fa991d33fd6..1b15400170fd 100644 --- a/integration-tests/docker/overlord.conf +++ b/integration-tests/docker/overlord.conf @@ -23,6 +23,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/overlord -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/docker/router.conf b/integration-tests/docker/router.conf index b73cbb66bb6e..3222c18db36b 100644 --- a/integration-tests/docker/router.conf +++ b/integration-tests/docker/router.conf @@ -16,6 +16,7 @@ command=java -Ddruid.auth.authenticator.basic.initialAdminPassword=priest -Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock -Ddruid.auth.authenticator.basic.authorizerName=basic + -Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router -Ddruid.escalator.type=basic -Ddruid.escalator.internalClientUsername=druid_system -Ddruid.escalator.internalClientPassword=warlock diff --git a/integration-tests/src/test/java/io/druid/tests/security/ITBasicAuthConfigurationTest.java b/integration-tests/src/test/java/io/druid/tests/security/ITBasicAuthConfigurationTest.java index a4bd9d1ff956..d03ce64b346a 100644 --- a/integration-tests/src/test/java/io/druid/tests/security/ITBasicAuthConfigurationTest.java +++ b/integration-tests/src/test/java/io/druid/tests/security/ITBasicAuthConfigurationTest.java @@ -19,6 +19,7 @@ package io.druid.tests.security; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Charsets; import com.google.common.base.Throwables; @@ -33,6 +34,7 @@ import io.druid.java.util.common.ISE; import io.druid.java.util.common.StringUtils; import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; import io.druid.server.security.Action; import io.druid.server.security.Resource; import io.druid.server.security.ResourceAction; @@ -41,6 +43,7 @@ import io.druid.testing.guice.DruidTestModuleFactory; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; +import org.testng.Assert; import org.testng.annotations.Guice; import org.testng.annotations.Test; @@ -48,12 +51,18 @@ import java.net.URL; import java.util.Arrays; import java.util.List; +import java.util.Map; @Guice(moduleFactory = DruidTestModuleFactory.class) public class ITBasicAuthConfigurationTest { private static final Logger LOG = new Logger(ITBasicAuthConfigurationTest.class); + private static final TypeReference LOAD_STATUS_TYPE_REFERENCE = + new TypeReference>() + { + }; + @Inject IntegrationTestingConfig config; @@ -94,35 +103,35 @@ public void testAuthConfiguration() throws Exception makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/basic/users/druid", + config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid", null ); makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/basic/users/druid/credentials", - StringUtils.toUtf8("helloworld") + config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid/credentials", + jsonMapper.writeValueAsBytes(new BasicAuthenticatorCredentialUpdate("helloworld", 5000)) ); makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/basic/users/druid", + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid", null ); makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/basic/roles/druidrole", + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/roles/druidrole", null ); makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/basic/users/druid/roles/druidrole", + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid/roles/druidrole", null ); @@ -136,12 +145,58 @@ public void testAuthConfiguration() throws Exception makeRequest( adminClient, HttpMethod.POST, - config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/basic/roles/druidrole/permissions", + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/roles/druidrole/permissions", permissionsBytes ); // check that the new user works checkNodeAccess(newUserClient); + + // check loadStatus + checkLoadStatus(adminClient); + + + // create 100 users + for (int i = 0; i < 100; i++) { + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid" + i, + null + ); + + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid" + i, + null + ); + + LOG.info("Finished creating user druid" + i); + } + + // setup the last of 100 users and check that it works + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid99/credentials", + jsonMapper.writeValueAsBytes(new BasicAuthenticatorCredentialUpdate("helloworld", 5000)) + ); + + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid99/roles/druidrole", + null + ); + + HttpClient newUser99Client = new CredentialedHttpClient( + new BasicCredentials("druid99", "helloworld"), + httpClient + ); + + LOG.info("Checking access for user druid99."); + checkNodeAccess(newUser99Client); } private void checkNodeAccess(HttpClient httpClient) @@ -153,6 +208,42 @@ private void checkNodeAccess(HttpClient httpClient) makeRequest(httpClient, HttpMethod.GET, config.getRouterUrl() + "/status", null); } + private void checkLoadStatus(HttpClient httpClient) throws Exception + { + checkLoadStatusSingle(httpClient, config.getCoordinatorUrl()); + checkLoadStatusSingle(httpClient, config.getIndexerUrl()); + checkLoadStatusSingle(httpClient, config.getBrokerUrl()); + checkLoadStatusSingle(httpClient, config.getHistoricalUrl()); + checkLoadStatusSingle(httpClient, config.getRouterUrl()); + } + + private void checkLoadStatusSingle(HttpClient httpClient, String baseUrl) throws Exception + { + StatusResponseHolder holder = makeRequest( + httpClient, + HttpMethod.GET, + baseUrl + "/druid-ext/basic-security/authentication/loadStatus", + null + ); + String content = holder.getContent(); + Map loadStatus = jsonMapper.readValue(content, LOAD_STATUS_TYPE_REFERENCE); + + Assert.assertNotNull(loadStatus.get("basic")); + Assert.assertTrue(loadStatus.get("basic")); + + holder = makeRequest( + httpClient, + HttpMethod.GET, + baseUrl + "/druid-ext/basic-security/authorization/loadStatus", + null + ); + content = holder.getContent(); + loadStatus = jsonMapper.readValue(content, LOAD_STATUS_TYPE_REFERENCE); + + Assert.assertNotNull(loadStatus.get("basic")); + Assert.assertTrue(loadStatus.get("basic")); + } + private StatusResponseHolder makeRequest(HttpClient httpClient, HttpMethod method, String url, byte[] content) { try {