diff --git a/benchmarks/src/main/java/io/druid/benchmark/BasicAuthUserMapSerdeBenchmark.java b/benchmarks/src/main/java/io/druid/benchmark/BasicAuthUserMapSerdeBenchmark.java new file mode 100644 index 00000000000..fcf7d0584ac --- /dev/null +++ b/benchmarks/src/main/java/io/druid/benchmark/BasicAuthUserMapSerdeBenchmark.java @@ -0,0 +1,182 @@ +/* + * 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.benchmark; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 10) +@Measurement(iterations = 25) +public class BasicAuthUserMapSerdeBenchmark +{ + @Param({"1000"}) + private int numUsers; + + private ObjectMapper smileMapper; + private Map userMap; + private List serializedUsers; + + @Setup + public void setup() throws IOException + { + smileMapper = new ObjectMapper(new SmileFactory()); + userMap = new HashMap<>(); + for (int i = 0; i < numUsers; i++) { + BenchmarkUser user = makeUser(); + userMap.put(user.getName(), user); + } + + serializedUsers = new ArrayList<>(); + for (BenchmarkUser user : userMap.values()) { + byte[] serializedUser = smileMapper.writeValueAsBytes(user); + serializedUsers.add(serializedUser); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void serialize(Blackhole blackhole) throws Exception + { + for (BenchmarkUser user : userMap.values()) { + byte[] serializedUser = smileMapper.writeValueAsBytes(user); + blackhole.consume(serializedUser); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void deserialize(Blackhole blackhole) throws Exception + { + for (byte[] serializedUser : serializedUsers) { + BenchmarkUser user = smileMapper.readValue(serializedUser, BenchmarkUser.class); + blackhole.consume(user); + } + } + + private BenchmarkUser makeUser() + { + byte[] salt = new byte[32]; + byte[] hash = new byte[64]; + + Random random = new Random(); + random.nextBytes(salt); + random.nextBytes(hash); + return new BenchmarkUser( + UUID.randomUUID().toString(), + new BenchmarkCredentials( + salt, + hash, + 10000 + ) + ); + } + + private static class BenchmarkUser + { + private final String name; + private final BenchmarkCredentials credentials; + + @JsonCreator + public BenchmarkUser( + @JsonProperty("name") String name, + @JsonProperty("credentials") BenchmarkCredentials credentials + ) + { + this.name = name; + this.credentials = credentials; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public BenchmarkCredentials getCredentials() + { + return credentials; + } + } + + private static class BenchmarkCredentials + { + private final byte[] salt; + private final byte[] hash; + private final int iterations; + + @JsonCreator + public BenchmarkCredentials( + @JsonProperty("salt") byte[] salt, + @JsonProperty("hash") byte[] hash, + @JsonProperty("iterations") int iterations + ) + { + this.salt = salt; + this.hash = hash; + this.iterations = iterations; + } + + @JsonProperty + public byte[] getSalt() + { + return salt; + } + + @JsonProperty + public byte[] getHash() + { + return hash; + } + + @JsonProperty + public int getIterations() + { + return iterations; + } + } +} diff --git a/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java b/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java new file mode 100644 index 00000000000..7f081d75487 --- /dev/null +++ b/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java @@ -0,0 +1,195 @@ +/* + * 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.benchmark; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.druid.jackson.DefaultObjectMapper; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@State(Scope.Benchmark) +@Fork(value = 1) +@Warmup(iterations = 10) +@Measurement(iterations = 25) +public class RegexMatchBenchmark +{ + @Param({"100000"}) + private int numPatterns; + + private ObjectMapper jsonMapper; + + private List uuids; + + private String granularityPathRegex = "^.*[Yy]=(\\d{4})/(?:[Mm]=(\\d{2})/(?:[Dd]=(\\d{2})/(?:[Hh]=(\\d{2})/(?:[Mm]=(\\d{2})/(?:[Ss]=(\\d{2})/)?)?)?)?)?.*$"; + private String uuidRegex = "[\\w]{8}-[\\w]{4}-[\\w]{4}-[\\w]{4}-[\\w]{12}"; + private Pattern uuidPattern = Pattern.compile(uuidRegex); + private Pattern granularityPathPattern = Pattern.compile(granularityPathRegex); + private byte[] uuidPatternBytes; + private byte[] granularityPathPatternBytes; + private String randomUUID = UUID.randomUUID().toString(); + + @Setup + public void setup() throws IOException + { + jsonMapper = new DefaultObjectMapper(); + + uuids = new ArrayList<>(); + for (int i = 0; i < numPatterns; i++) { + UUID uuid = UUID.randomUUID(); + uuids.add(uuid.toString()); + } + + uuidPatternBytes = jsonMapper.writeValueAsBytes(uuidPattern); + granularityPathPatternBytes = jsonMapper.writeValueAsBytes(granularityPathPattern); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileUUIDsAsRegex(final Blackhole blackhole) + { + for (String uuid : uuids) { + Pattern pattern = Pattern.compile(uuid); + blackhole.consume(pattern); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileUUIDsAsRegexAndMatchRandomUUID(final Blackhole blackhole) + { + for (String uuid : uuids) { + Pattern pattern = Pattern.compile(uuid); + Matcher matcher = pattern.matcher(randomUUID); + blackhole.consume(matcher.matches()); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileGranularityPathRegex(final Blackhole blackhole) + { + for (int i = 0; i < numPatterns; i++) { + Pattern pattern = Pattern.compile(granularityPathRegex); + blackhole.consume(pattern); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void deserializeGranularityPathRegex(final Blackhole blackhole) throws IOException + { + for (int i = 0; i < numPatterns; i++) { + Pattern pattern = jsonMapper.readValue(granularityPathPatternBytes, Pattern.class); + blackhole.consume(pattern); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileUUIDRegex(final Blackhole blackhole) + { + for (int i = 0; i < numPatterns; i++) { + Pattern pattern = Pattern.compile(uuidRegex); + blackhole.consume(pattern); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void deserializeUUIDRegex(final Blackhole blackhole) throws IOException + { + for (int i = 0; i < numPatterns; i++) { + Pattern pattern = jsonMapper.readValue(uuidPatternBytes, Pattern.class); + blackhole.consume(pattern); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileUUIDRegexAndMatch(final Blackhole blackhole) + { + for (String uuid : uuids) { + Pattern pattern = Pattern.compile(uuidRegex); + Matcher matcher = pattern.matcher(uuid); + blackhole.consume(matcher.matches()); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void compileGranularityPathRegexAndMatch(final Blackhole blackhole) + { + for (String uuid : uuids) { + Pattern pattern = Pattern.compile(granularityPathRegex); + Matcher matcher = pattern.matcher(uuid); + blackhole.consume(matcher.matches()); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void precompileUUIDRegexAndMatch(final Blackhole blackhole) + { + for (String uuid : uuids) { + Matcher matcher = uuidPattern.matcher(uuid); + blackhole.consume(matcher.matches()); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + @OutputTimeUnit(TimeUnit.MICROSECONDS) + public void precompileGranularityPathRegexAndMatch(final Blackhole blackhole) + { + for (String uuid : uuids) { + Matcher matcher = granularityPathPattern.matcher(uuid); + blackhole.consume(matcher.matches()); + } + } +} + diff --git a/common/src/main/java/io/druid/metadata/MetadataCASUpdate.java b/common/src/main/java/io/druid/metadata/MetadataCASUpdate.java new file mode 100644 index 00000000000..93b63ae78df --- /dev/null +++ b/common/src/main/java/io/druid/metadata/MetadataCASUpdate.java @@ -0,0 +1,80 @@ +/* + * 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.metadata; + +/** + * Expresses a single compare-and-swap update for MetadataStorageConnector's compareAndSwap method + */ +public class MetadataCASUpdate +{ + private final String tableName; + private final String keyColumn; + private final String valueColumn; + private final String key; + private final byte[] oldValue; + private final byte[] newValue; + + public MetadataCASUpdate( + String tableName, + String keyColumn, + String valueColumn, + String key, + byte[] oldValue, + byte[] newValue + ) + { + this.tableName = tableName; + this.keyColumn = keyColumn; + this.valueColumn = valueColumn; + this.key = key; + this.oldValue = oldValue; + this.newValue = newValue; + } + + public String getTableName() + { + return tableName; + } + + public String getKeyColumn() + { + return keyColumn; + } + + public String getValueColumn() + { + return valueColumn; + } + + public String getKey() + { + return key; + } + + public byte[] getOldValue() + { + return oldValue; + } + + public byte[] getNewValue() + { + return newValue; + } +} diff --git a/common/src/main/java/io/druid/metadata/MetadataStorageConnector.java b/common/src/main/java/io/druid/metadata/MetadataStorageConnector.java index 246f231165d..a8da1827045 100644 --- a/common/src/main/java/io/druid/metadata/MetadataStorageConnector.java +++ b/common/src/main/java/io/druid/metadata/MetadataStorageConnector.java @@ -19,10 +19,15 @@ package io.druid.metadata; +import java.util.List; + /** */ public interface MetadataStorageConnector { + String CONFIG_TABLE_KEY_COLUMN = "name"; + String CONFIG_TABLE_VALUE_COLUMN = "payload"; + Void insertOrUpdate( String tableName, String keyColumn, @@ -38,6 +43,21 @@ public interface MetadataStorageConnector String key ); + /** + * Atomic compare-and-swap variant of insertOrUpdate(). + * + * @param updates Set of updates to be made. If compare checks succeed for all updates, perform all updates. + * If any compare check fails, reject all updates. + * @return true if updates were made, false otherwise + * @throws Exception + */ + default boolean compareAndSwap( + List updates + ) throws Exception + { + throw new UnsupportedOperationException("compareAndSwap is not implemented."); + } + void createDataSourceTable(); void createPendingSegmentsTable(); diff --git a/distribution/pom.xml b/distribution/pom.xml index f1dd51b1403..6b429994e1a 100644 --- a/distribution/pom.xml +++ b/distribution/pom.xml @@ -110,6 +110,8 @@ io.druid.extensions:druid-examples -c io.druid.extensions:simple-client-sslcontext + -c + io.druid.extensions:druid-basic-security ${druid.distribution.pulldeps.opts} diff --git a/docs/content/development/extensions-core/druid-basic-security.md b/docs/content/development/extensions-core/druid-basic-security.md new file mode 100644 index 00000000000..d89a1402502 --- /dev/null +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -0,0 +1,295 @@ +--- +layout: doc_page +--- + +# Druid Basic Security + +This extension adds: +- an Authenticator which supports [HTTP Basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) +- an Authorizer which implements basic role-based access control + +Make sure to [include](../../operations/including-extensions.html) `druid-basic-security` as an extension. + +Please see [Authentication and Authorization](../../configuration/auth.html) for more information on the extension interfaces being implemented. + +## Configuration + +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| + + +### Creating an Authenticator +``` +druid.auth.authenticatorChain=["MyBasicAuthenticator"] + +druid.auth.authenticator.MyBasicAuthenticator.type=basic +druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword=password1 +druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword=password2 +druid.auth.authenticator.MyBasicAuthenticator.authorizerName=MyBasicAuthorizer +``` + +To use the Basic authenticator, add an authenticator with type `basic` to the authenticatorChain. + +Configuration of the named authenticator is assigned through properties with the form: + +``` +druid.auth.authenticator.. +``` + +The configuration examples in the rest of this document will use "MyBasicAuthenticator" as the name of the authenticator being configured. + + +#### 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. 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| + +### Creating an Escalator + +``` +# Escalator +druid.escalator.type=basic +druid.escalator.internalClientUsername=druid_system +druid.escalator.internalClientPassword=password2 +druid.escalator.authorizerName=MyBasicAuthorizer +``` + +#### Properties +|Property|Description|Default|required| +|--------|-----------|-------|--------| +|`druid.escalator.internalClientUsername`|The escalator will use this username for requests made as the internal systerm user.|n/a|Yes| +|`druid.escalator.internalClientPassword`|The escalator will use this password for requests made as the internal system user.|n/a|Yes| +|`druid.escalator.authorizerName`|Authorizer that requests should be directed to.|n/a|Yes| + + +### Creating an Authorizer +``` +druid.auth.authorizers=["MyBasicAuthorizer"] + +druid.auth.authorizer.MyBasicAuthorizer.type=basic +``` + +To use the Basic authorizer, add an authenticator with type `basic` to the authorizers list. + +Configuration of the named authenticator is assigned through properties with the form: + +``` +druid.auth.authorizer.. +``` + +#### Properties +|Property|Description|Default|required| +|--------|-----------|-------|--------| +|`druid.auth.authorizer.MyBasicAuthorizer.enableCacheNotifications`|If true, the coordinator will notify Druid nodes whenever a configuration change to this Authorizer occurs, allowing them to immediately update their state without waiting for polling.|true|No| +|`druid.auth.authorizer.MyBasicAuthorizer.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No| + +## Usage + +### Coordinator Security API +To use these APIs, a user needs read/write permissions for the CONFIG resource type with name "security". + +#### Authentication API + +Root path: `/druid-ext/basic-security/authentication` + +Each API endpoint includes {authenticatorName}, specifying which Authenticator instance is being configured. + +##### User/Credential Management +`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users)` +Return a list of all user names. + +`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` +Return the name and credentials information of the user with name {userName} + +`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` +Create a new user with name {userName} + +`DELETE(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})` +Delete the user with name {userName} + +`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName}/credentials)` +Assign a password used for HTTP basic authentication for {userName} +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 + +Root path: `/druid-ext/basic-security/authorization` + +Each API endpoint includes {authorizerName}, specifying which Authorizer instance is being configured. + +##### User Creation/Deletion +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users)` +Return a list of all user names. + +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` +Return the name and role information of the user with name {userName} + +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` +Create a new user with name {userName} + +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})` +Delete the user with name {userName} + + +#### Role Creation/Deletion +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles)` +Return a list of all role names. + +`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` +Return name and permissions for the role named {roleName} + +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` +Create a new role with name {roleName}. +Content: username string + +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})` +Delete the role with name {roleName}. + + +#### Role Assignment +`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})` +Assign role {roleName} to user {userName}. + +`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})` +Unassign role {roleName} from user {userName} + + +#### 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": { + "name": "wiki.*", + "type": "DATASOURCE" + }, + "action": "READ" +}, +{ + "resource": { + "name": "wikiticker", + "type": "DATASOURCE" + }, + "action": "WRITE" +} +] +``` + +The "name" field for resources in the permission definitions are regexes used to match resource names during authorization checks. + +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 +If `druid.auth.authenticator..initialAdminPassword` is set, a default admin user named "admin" will be created, with the specified initial password. If this configuration is omitted, the "admin" user will not be created. + +If `druid.auth.authenticator..initialInternalClientPassword` is set, a default internal system user named "druid_system" will be created, with the specified initial password. If this configuration is omitted, the "druid_system" user will not be created. + + +### Authorizer + +Each Authorizer will always have a default "admin" and "druid_system" user with full privileges. + +## Defining permissions + +There are two action types in Druid: READ and WRITE + +There are three resource types in Druid: DATASOURCE, CONFIG, and STATE. + +### DATASOURCE +Resource names for this type are datasource names. Specifying a datasource permission allows the administrator to grant users access to specific datasources. + +### CONFIG +There are two possible resource names for the "CONFIG" resource type, "CONFIG" and "security". Granting a user access to CONFIG resources allows them to access the following endpoints. + +"CONFIG" resource name covers the following endpoints: + +|Endpoint|Node Type| +|--------|---------| +|`/druid/coordinator/v1/config`|coordinator| +|`/druid/indexer/v1/worker`|overlord| +|`/druid/indexer/v1/worker/history`|overlord| +|`/druid/worker/v1/disable`|middleManager| +|`/druid/worker/v1/enable`|middleManager| + +"security" resource name covers the following endpoint: + +|Endpoint|Node Type| +|--------|---------| +|`/druid/coordinator/v1/security`|coordinator| + +### STATE +There is only one possible resource name for the "STATE" config resource type, "STATE". Granting a user access to STATE resources allows them to access the following endpoints. + +"STATE" resource name covers the following endpoints: + +|Endpoint|Node Type| +|--------|---------| +|`/druid/coordinator/v1`|coordinator| +|`/druid/coordinator/v1/rules`|coordinator| +|`/druid/coordinator/v1/rules/history`|coordinator| +|`/druid/coordinator/v1/servers`|coordinator| +|`/druid/coordinator/v1/tiers`|coordinator| +|`/druid/broker/v1`|broker| +|`/druid/v2/candidates`|broker| +|`/druid/indexer/v1/leader`|overlord| +|`/druid/indexer/v1/isLeader`|overlord| +|`/druid/indexer/v1/action`|overlord| +|`/druid/indexer/v1/workers`|overlord| +|`/druid/indexer/v1/scaling`|overlord| +|`/druid/worker/v1/enabled`|middleManager| +|`/druid/worker/v1/tasks`|middleManager| +|`/druid/worker/v1/task/{taskid}/shutdown`|middleManager| +|`/druid/worker/v1//task/{taskid}/log`|middleManager| +|`/druid/historical/v1`|historical| +|`/druid-internal/v1/segments/`|historical| +|`/druid-internal/v1/segments/`|peon| +|`/druid-internal/v1/segments/`|realtime| +|`/status`|all nodes| + +## Configuration Propagation + +To prevent excessive load on the coordinator, the Authenticator and Authorizer user/role database state is cached on each Druid node. + +Each node will periodically poll the coordinator for the latest database state, controlled by the `druid.auth.basic.common.pollingPeriod` and `druid.auth.basic.common.maxRandomDelay` properties. + +When a configuration update occurs, the coordinator can optionally notify each node with the updated database state. This behavior is controlled by the `enableCacheNotifications` and `cacheNotificationTimeout` properties on Authenticators and Authorizers. + +Note that because of the caching, changes made to the user/role database may not be immediately reflected at each Druid node. + diff --git a/extensions-core/druid-basic-security/pom.xml b/extensions-core/druid-basic-security/pom.xml new file mode 100644 index 00000000000..417ba27f449 --- /dev/null +++ b/extensions-core/druid-basic-security/pom.xml @@ -0,0 +1,75 @@ + + + + + + 4.0.0 + + io.druid.extensions + druid-basic-security + druid-basic-security + druid-basic-security + + + io.druid + druid + 0.11.1-SNAPSHOT + ../../pom.xml + + + + + io.druid + druid-services + ${project.parent.version} + provided + + + io.druid + druid-server + ${project.parent.version} + provided + + + org.jdbi + jdbi + provided + + + + + junit + junit + test + + + org.easymock + easymock + test + + + io.druid + druid-server + ${project.parent.version} + test-jar + test + + + \ No newline at end of file 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 new file mode 100644 index 00000000000..c16314b0a65 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthCommonCacheConfig.java @@ -0,0 +1,80 @@ +/* + * 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.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +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; + + @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("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 + public long getPollingPeriod() + { + return pollingPeriod; + } + + @JsonProperty + 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 new file mode 100644 index 00000000000..6a972762696 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthDBConfig.java @@ -0,0 +1,71 @@ +/* + * 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; + +public class BasicAuthDBConfig +{ + public static final long DEFAULT_CACHE_NOTIFY_TIMEOUT_MS = 5000; + + private final String initialAdminPassword; + 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 int iterations + ) + { + this.initialAdminPassword = initialAdminPassword; + this.initialInternalClientPassword = initialInternalClientPassword; + this.enableCacheNotifications = enableCacheNotifications; + this.cacheNotificationTimeout = cacheNotificationTimeout; + this.iterations = iterations; + } + + public String getInitialAdminPassword() + { + return initialAdminPassword; + } + + public String getInitialInternalClientPassword() + { + return initialInternalClientPassword; + } + + public boolean isEnableCacheNotifications() + { + return enableCacheNotifications; + } + + 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 new file mode 100644 index 00000000000..9b1f92d1603 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java @@ -0,0 +1,235 @@ +/* + * 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.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 SecureRandom SECURE_RANDOM = new SecureRandom(); + public final static String ADMIN_NAME = "admin"; + public final static String INTERNAL_USER_NAME = "druid_system"; + + // 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 Base64.getEncoder().encodeToString(StringUtils.toUtf8(unencodedCreds)); + } + + public static byte[] hashPassword(final char[] password, final byte[] salt, final int iterations) + { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM); + SecretKey key = keyFactory.generateSecret( + new PBEKeySpec( + password, + salt, + iterations, + KEY_LENGTH + ) + ); + return key.getEncoded(); + } + catch (InvalidKeySpecException ikse) { + log.error("WTF? invalid keyspec"); + throw new RuntimeException("WTF? invalid keyspec", ikse); + } + catch (NoSuchAlgorithmException nsae) { + log.error("%s not supported on this system.", ALGORITHM); + throw new RuntimeException(StringUtils.format("%s not supported on this system.", ALGORITHM), nsae); + } + } + + public static byte[] generateSalt() + { + byte salt[] = new byte[SALT_LENGTH]; + SECURE_RANDOM.nextBytes(salt); + 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 { + return StringUtils.fromUtf8(Base64.getDecoder().decode(encodedUserSecret)); + } + catch (IllegalArgumentException iae) { + 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; + } + + public static byte[] serializeAuthenticatorUserMap( + ObjectMapper objectMapper, + Map userMap + ) + { + try { + return objectMapper.writeValueAsBytes(userMap); + } + catch (IOException ioe) { + throw new ISE(ioe, "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; + } + + public static byte[] serializeAuthorizerUserMap(ObjectMapper objectMapper, Map userMap) + { + try { + return objectMapper.writeValueAsBytes(userMap); + } + catch (IOException ioe) { + throw new ISE(ioe, "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(ioe, "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 new file mode 100644 index 00000000000..17ae325174d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java @@ -0,0 +1,39 @@ +/* + * 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 io.druid.java.util.common.StringUtils; + +/** + * 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 +{ + 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 new file mode 100644 index 00000000000..8e14d8770be --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java @@ -0,0 +1,183 @@ +/* + * 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.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Provides; +import com.google.inject.name.Names; +import io.druid.guice.Jerseys; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.LazySingleton; +import io.druid.guice.LifecycleModule; +import io.druid.initialization.DruidModule; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; +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.CoordinatorBasicAuthenticatorCacheNotifier; +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.endpoint.BasicAuthenticatorResource; +import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResourceHandler; +import io.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler; +import io.druid.security.basic.authentication.endpoint.DefaultBasicAuthenticatorResourceHandler; +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.CoordinatorBasicAuthorizerCacheNotifier; +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.endpoint.BasicAuthorizerResource; +import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResourceHandler; +import io.druid.security.basic.authorization.endpoint.CoordinatorBasicAuthorizerResourceHandler; +import io.druid.security.basic.authorization.endpoint.DefaultBasicAuthorizerResourceHandler; + +import java.util.List; + +public class BasicSecurityDruidModule implements DruidModule +{ + @Override + public void configure(Binder binder) + { + JsonConfigProvider.bind(binder, "druid.auth.basic.common", BasicAuthCommonCacheConfig.class); + + LifecycleModule.register(binder, BasicAuthenticatorMetadataStorageUpdater.class); + LifecycleModule.register(binder, BasicAuthorizerMetadataStorageUpdater.class); + LifecycleModule.register(binder, BasicAuthenticatorCacheManager.class); + LifecycleModule.register(binder, BasicAuthorizerCacheManager.class); + + Jerseys.addResource(binder, BasicAuthenticatorResource.class); + Jerseys.addResource(binder, BasicAuthorizerResource.class); + } + + @Provides @LazySingleton + public static BasicAuthenticatorMetadataStorageUpdater createAuthenticatorStorageUpdater(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class); + } else { + return null; + } + } + + @Provides @LazySingleton + public static BasicAuthenticatorCacheManager createAuthenticatorCacheManager(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(MetadataStoragePollingBasicAuthenticatorCacheManager.class); + } else { + return injector.getInstance(CoordinatorPollingBasicAuthenticatorCacheManager.class); + } + } + + @Provides @LazySingleton + public static BasicAuthenticatorResourceHandler createAuthenticatorResourceHandler(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthenticatorResourceHandler.class); + } else { + return injector.getInstance(DefaultBasicAuthenticatorResourceHandler.class); + } + } + + @Provides @LazySingleton + public static BasicAuthenticatorCacheNotifier createAuthenticatorCacheNotifier(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthenticatorCacheNotifier.class); + } else { + return null; + } + } + + @Provides @LazySingleton + public static BasicAuthorizerMetadataStorageUpdater createAuthorizerStorageUpdater(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthorizerMetadataStorageUpdater.class); + } else { + return null; + } + } + + @Provides @LazySingleton + public static BasicAuthorizerCacheManager createAuthorizerCacheManager(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(MetadataStoragePollingBasicAuthorizerCacheManager.class); + } else { + return injector.getInstance(CoordinatorPollingBasicAuthorizerCacheManager.class); + } + } + + @Provides @LazySingleton + public static BasicAuthorizerResourceHandler createAuthorizerResourceHandler(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthorizerResourceHandler.class); + } else { + return injector.getInstance(DefaultBasicAuthorizerResourceHandler.class); + } + } + + @Provides @LazySingleton + public static BasicAuthorizerCacheNotifier createAuthorizerCacheNotifier(final Injector injector) + { + if (isCoordinator(injector)) { + return injector.getInstance(CoordinatorBasicAuthorizerCacheNotifier.class); + } else { + return null; + } + } + + @Override + public List getJacksonModules() + { + return ImmutableList.of( + new SimpleModule("BasicDruidSecurity").registerSubtypes( + BasicHTTPAuthenticator.class, + BasicHTTPEscalator.class, + BasicRoleBasedAuthorizer.class + ) + ); + } + + private static boolean isCoordinator(Injector injector) + { + final String serviceName; + try { + serviceName = injector.getInstance(Key.get(String.class, Names.named("serviceName"))); + } + catch (Exception e) { + return false; + } + + return "druid/coordinator".equals(serviceName); + } +} 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 new file mode 100644 index 00000000000..733bcc50baf --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -0,0 +1,90 @@ +/* + * 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.collect.ImmutableList; +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ContainerRequest; +import io.druid.java.util.common.StringUtils; +import io.druid.server.http.security.AbstractResourceFilter; +import io.druid.server.security.Access; +import io.druid.server.security.AuthorizationUtils; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.Resource; +import io.druid.server.security.ResourceAction; +import io.druid.server.security.ResourceType; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.List; + +public class BasicSecurityResourceFilter extends AbstractResourceFilter +{ + 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 + ) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + final ResourceAction resourceAction = new ResourceAction( + new Resource(SECURITY_RESOURCE_NAME, ResourceType.CONFIG), + getAction(request) + ); + + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); + + if (!authResult.isAllowed()) { + throw new WebApplicationException( + Response.status(Response.Status.FORBIDDEN) + .entity(StringUtils.format("Access-Check-Result: %s", authResult.toString())) + .build() + ); + } + + return request; + } + + @Override + public boolean isApplicable(String requestPath) + { + for (String path : APPLICABLE_PATHS) { + if (requestPath.startsWith(path) && !requestPath.equals(path)) { + return true; + } + } + return false; + } +} 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 00000000000..4b7a557197c --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/CommonCacheNotifier.java @@ -0,0 +1,236 @@ +/* + * 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.Futures; +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.Pair; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.concurrent.Execs; +import io.druid.java.util.common.logger.Logger; +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.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +public class CommonCacheNotifier +{ + private static final EmittingLogger LOG = new EmittingLogger(CommonCacheNotifier.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 BlockingQueue> updateQueue; + private final Map itemConfigMap; + private final String baseUrl; + private final String callerName; + private final ExecutorService exec; + + public CommonCacheNotifier( + Map itemConfigMap, + DruidNodeDiscoveryProvider discoveryProvider, + HttpClient httpClient, + String baseUrl, + String callerName + ) + { + this.exec = Execs.scheduledSingleThreaded(StringUtils.format("%s-notifierThread-", callerName) + "%d"); + this.callerName = callerName; + this.updateQueue = new LinkedBlockingQueue<>(); + this.itemConfigMap = itemConfigMap; + this.discoveryProvider = discoveryProvider; + this.httpClient = httpClient; + this.baseUrl = baseUrl; + } + + public void start() + { + exec.submit( + () -> { + while (!Thread.interrupted()) { + try { + LOG.debug(callerName + ":Waiting for cache update notification"); + Pair update = updateQueue.take(); + String authorizer = update.lhs; + byte[] serializedMap = update.rhs; + BasicAuthDBConfig authorizerConfig = itemConfigMap.get(update.lhs); + if (!authorizerConfig.isEnableCacheNotifications()) { + continue; + } + + LOG.debug(callerName + ":Sending cache update notifications"); + // 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, + serializedMap + ); + + try { + List responses = Futures.allAsList(futures) + .get( + authorizerConfig.getCacheNotificationTimeout(), + TimeUnit.MILLISECONDS + ); + + for (StatusResponseHolder response : responses) { + LOG.debug(callerName + ":Got status: " + response.getStatus()); + } + } + catch (Exception e) { + LOG.makeAlert(e, callerName + ":Failed to get response for cache notification.").emit(); + } + + LOG.debug(callerName + ":Received responses for cache update notifications."); + } + catch (Throwable t) { + LOG.makeAlert(t, callerName + ":Error occured while handling updates for cachedUserMaps.").emit(); + } + } + } + ); + } + + public void stop() + { + exec.shutdownNow(); + } + + public void addUpdate(String updatedItemName, byte[] updatedItemData) + { + updateQueue.add( + new Pair<>(updatedItemName, updatedItemData) + ); + } + + 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) { + LOG.error(callerName + ":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 + { + protected static final Logger log = new Logger(ResponseHandler.class); + + @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 + log.error(e, "exceptionCaught in CommonCacheNotifier ResponseHandler."); + } + } +} 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 new file mode 100644 index 00000000000..b127687999b --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java @@ -0,0 +1,214 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.inject.Provider; +import io.druid.java.util.common.IAE; +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; +import io.druid.server.security.AuthConfig; +import io.druid.server.security.AuthenticationResult; +import io.druid.server.security.Authenticator; + +import javax.annotation.Nullable; +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Map; + +@JsonTypeName("basic") +public class BasicHTTPAuthenticator implements Authenticator +{ + private final Provider cacheManager; + private final String name; + private final String authorizerName; + private final BasicAuthDBConfig dbConfig; + + @JsonCreator + public BasicHTTPAuthenticator( + @JacksonInject Provider cacheManager, + @JsonProperty("name") String name, + @JsonProperty("authorizerName") String authorizerName, + @JsonProperty("initialAdminPassword") String initialAdminPassword, + @JsonProperty("initialInternalClientPassword") String initialInternalClientPassword, + @JsonProperty("enableCacheNotifications") Boolean enableCacheNotifications, + @JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout, + @JsonProperty("credentialIterations") Integer credentialIterations + ) + { + this.name = name; + this.authorizerName = authorizerName; + this.dbConfig = new BasicAuthDBConfig( + initialAdminPassword, + initialInternalClientPassword, + enableCacheNotifications == null ? true : enableCacheNotifications, + cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout, + credentialIterations == null ? BasicAuthUtils.DEFAULT_KEY_ITERATIONS : credentialIterations + ); + this.cacheManager = cacheManager; + } + + @Override + public Filter getFilter() + { + return new BasicHTTPAuthenticationFilter(); + } + + @Override + public String getAuthChallengeHeader() + { + return "Basic"; + } + + @Override + @Nullable + public AuthenticationResult authenticateJDBCContext(Map context) + { + String user = (String) context.get("user"); + String password = (String) context.get("password"); + + if (user == null || password == null) { + return null; + } + + if (checkCredentials(user, password.toCharArray())) { + return new AuthenticationResult(user, name, null); + } else { + return null; + } + } + + + @Override + public Class getFilterClass() + { + return BasicHTTPAuthenticationFilter.class; + } + + @Override + public Map getInitParameters() + { + return null; + } + + @Override + public String getPath() + { + return "/*"; + } + + @Override + public EnumSet getDispatcherType() + { + return null; + } + + public BasicAuthDBConfig getDbConfig() + { + return dbConfig; + } + + public class BasicHTTPAuthenticationFilter implements Filter + { + @Override + public void init(FilterConfig filterConfig) throws ServletException + { + + } + + @Override + public void doFilter( + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain + ) throws IOException, ServletException + { + HttpServletResponse httpResp = (HttpServletResponse) servletResponse; + String userSecret = BasicAuthUtils.getBasicUserSecretFromHttpReq((HttpServletRequest) servletRequest); + + if (userSecret == null) { + // Request didn't have HTTP Basic auth credentials, move on to the next filter + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + String[] splits = userSecret.split(":"); + if (splits.length != 2) { + httpResp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + String user = splits[0]; + char[] password = splits[1].toCharArray(); + + if (checkCredentials(user, password)) { + AuthenticationResult authenticationResult = new AuthenticationResult(user, authorizerName, null); + servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult); + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + @Override + public void destroy() + { + + } + } + + private boolean checkCredentials(String username, char[] password) + { + Map userMap = cacheManager.get().getUserMap(name); + if (userMap == null) { + throw new IAE("No authenticator found with prefix: [%s]", name); + } + + BasicAuthenticatorUser user = userMap.get(username); + if (user == null) { + return false; + } + BasicAuthenticatorCredentials credentials = user.getCredentials(); + if (credentials == null) { + return false; + } + + byte[] recalculatedHash = BasicAuthUtils.hashPassword( + password, + credentials.getSalt(), + credentials.getIterations() + ); + + return Arrays.equals(recalculatedHash, credentials.getHash()); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPEscalator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPEscalator.java new file mode 100644 index 00000000000..2eb73a23bf2 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPEscalator.java @@ -0,0 +1,116 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.base.Throwables; +import com.metamx.http.client.CredentialedHttpClient; +import com.metamx.http.client.HttpClient; +import com.metamx.http.client.auth.BasicCredentials; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.BasicAuthUtils; +import io.druid.server.security.AuthenticationResult; +import io.druid.server.security.Escalator; +import org.eclipse.jetty.client.api.Authentication; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.util.Attributes; +import org.jboss.netty.handler.codec.http.HttpHeaders; + +import java.net.URI; + +@JsonTypeName("basic") +public class BasicHTTPEscalator implements Escalator +{ + private final String internalClientUsername; + private final String internalClientPassword; + private final String authorizerName; + + @JsonCreator + public BasicHTTPEscalator( + @JsonProperty("authorizerName") String authorizerName, + @JsonProperty("internalClientUsername") String internalClientUsername, + @JsonProperty("internalClientPassword") String internalClientPassword + ) + { + this.authorizerName = authorizerName; + this.internalClientUsername = internalClientUsername; + this.internalClientPassword = internalClientPassword; + } + + @Override + public HttpClient createEscalatedClient(HttpClient baseClient) + { + return new CredentialedHttpClient( + new BasicCredentials(internalClientUsername, internalClientPassword), + baseClient + ); + } + + @Override + public org.eclipse.jetty.client.HttpClient createEscalatedJettyClient(org.eclipse.jetty.client.HttpClient baseClient) + { + baseClient.getAuthenticationStore().addAuthentication(new Authentication() + { + @Override + public boolean matches(String type, URI uri, String realm) + { + return true; + } + + @Override + public Result authenticate( + final Request request, ContentResponse response, Authentication.HeaderInfo headerInfo, Attributes context + ) + { + return new Result() + { + @Override + public URI getURI() + { + return request.getURI(); + } + + @Override + public void apply(Request request) + { + try { + final String unencodedCreds = StringUtils.format("%s:%s", internalClientUsername, internalClientPassword); + final String base64Creds = BasicAuthUtils.getEncodedCredentials(unencodedCreds); + request.getHeaders().add(HttpHeaders.Names.AUTHORIZATION, "Basic " + base64Creds); + } + catch (Throwable e) { + Throwables.propagate(e); + } + } + }; + } + }); + return baseClient; + } + + @Override + public AuthenticationResult createEscalatedAuthenticationResult() + { + return new AuthenticationResult(internalClientUsername, authorizerName, null); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BytesFullResponseHandler.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BytesFullResponseHandler.java new file mode 100644 index 00000000000..d8b65a5e8ff --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BytesFullResponseHandler.java @@ -0,0 +1,75 @@ +/* + * 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; + +import com.metamx.http.client.response.ClientResponse; +import com.metamx.http.client.response.FullResponseHolder; +import com.metamx.http.client.response.HttpResponseHandler; +import org.jboss.netty.handler.codec.http.HttpChunk; +import org.jboss.netty.handler.codec.http.HttpResponse; + +public class BytesFullResponseHandler implements HttpResponseHandler +{ + @Override + public ClientResponse handleResponse(HttpResponse response) + { + BytesFullResponseHolder holder = new BytesFullResponseHolder( + response.getStatus(), + response, + null + ); + + holder.addChunk(response.getContent().array()); + + return ClientResponse.unfinished( + holder + ); + } + + @Override + public ClientResponse handleChunk( + ClientResponse response, + HttpChunk chunk + ) + { + BytesFullResponseHolder holder = (BytesFullResponseHolder) response.getObj(); + + if (holder == null) { + return ClientResponse.finished(null); + } + + holder.addChunk(chunk.getContent().array()); + 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/BytesFullResponseHolder.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BytesFullResponseHolder.java new file mode 100644 index 00000000000..a273f503098 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BytesFullResponseHolder.java @@ -0,0 +1,63 @@ +/* + * 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; + +import com.metamx.http.client.response.FullResponseHolder; +import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseStatus; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class BytesFullResponseHolder extends FullResponseHolder +{ + private final List chunks; + + public BytesFullResponseHolder( + HttpResponseStatus status, + HttpResponse response, + StringBuilder builder + ) + { + super(status, response, builder); + this.chunks = new ArrayList<>(); + } + + public void addChunk(byte[] chunk) + { + chunks.add(chunk); + } + + public byte[] getBytes() + { + int size = 0; + for (byte[] chunk : chunks) { + size += chunk.length; + } + ByteBuffer buf = ByteBuffer.wrap(new byte[size]); + + for (byte[] chunk : chunks) { + buf.put(chunk); + } + + return buf.array(); + } +} 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 new file mode 100644 index 00000000000..d5742e10668 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheManager.java @@ -0,0 +1,47 @@ +/* + * 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.cache; + +import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; + +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 new file mode 100644 index 00000000000..b9b7901292c --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/BasicAuthenticatorCacheNotifier.java @@ -0,0 +1,34 @@ +/* + * 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.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 new file mode 100644 index 00000000000..2eff574cfd9 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorBasicAuthenticatorCacheNotifier.java @@ -0,0 +1,123 @@ +/* + * 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.cache; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.metamx.emitter.EmittingLogger; +import com.metamx.http.client.HttpClient; +import io.druid.concurrent.LifecycleLock; +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.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +import io.druid.security.basic.BasicAuthDBConfig; +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 java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@ManageLifecycle +public class CoordinatorBasicAuthenticatorCacheNotifier implements BasicAuthenticatorCacheNotifier +{ + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthenticatorCacheNotifier.class); + + private final LifecycleLock lifecycleLock = new LifecycleLock(); + private CommonCacheNotifier cacheNotifier; + + @Inject + public CoordinatorBasicAuthenticatorCacheNotifier( + AuthenticatorMapper authenticatorMapper, + DruidNodeDiscoveryProvider discoveryProvider, + @EscalatedClient HttpClient httpClient + ) + { + cacheNotifier = new CommonCacheNotifier( + initAuthenticatorConfigMap(authenticatorMapper), + discoveryProvider, + httpClient, + "/druid-ext/basic-security/authentication/listen/%s", + "CoordinatorBasicAuthenticatorCacheNotifier" + ); + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + try { + cacheNotifier.start(); + lifecycleLock.started(); + } + finally { + lifecycleLock.exitStart(); + } + } + + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + return; + } + try { + cacheNotifier.stop(); + } + finally { + lifecycleLock.exitStop(); + } + } + + @Override + public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap); + } + + private Map initAuthenticatorConfigMap(AuthenticatorMapper mapper) + { + Preconditions.checkNotNull(mapper); + Preconditions.checkNotNull(mapper.getAuthenticatorMap()); + + Map authenticatorConfigMap = new HashMap<>(); + + for (Map.Entry entry : mapper.getAuthenticatorMap().entrySet()) { + Authenticator authenticator = entry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + String authenticatorName = entry.getKey(); + BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator; + BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig(); + authenticatorConfigMap.put(authenticatorName, dbConfig); + } + } + + return authenticatorConfigMap; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java new file mode 100644 index 00000000000..178232e13df --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/CoordinatorPollingBasicAuthenticatorCacheManager.java @@ -0,0 +1,282 @@ +/* + * 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.cache; + +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; +import com.metamx.http.client.Request; +import io.druid.client.coordinator.Coordinator; +import io.druid.concurrent.LifecycleLock; +import io.druid.discovery.DruidLeaderClient; +import io.druid.guice.ManageLifecycle; +import io.druid.guice.annotations.Smile; +import io.druid.java.util.common.ISE; +import io.druid.java.util.common.RetryUtils; +import io.druid.java.util.common.StringUtils; +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.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 javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +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 CoordinatorPollingBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager +{ + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthenticatorCacheManager.class); + + private final ConcurrentHashMap> cachedUserMaps; + private final Set authenticatorPrefixes; + private final Injector injector; + private final ObjectMapper objectMapper; + private final LifecycleLock lifecycleLock = new LifecycleLock(); + private final DruidLeaderClient druidLeaderClient; + private final BasicAuthCommonCacheConfig commonCacheConfig; + private final ScheduledExecutorService exec; + + @Inject + 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; + this.cachedUserMaps = new ConcurrentHashMap<>(); + this.authenticatorPrefixes = new HashSet<>(); + this.druidLeaderClient = druidLeaderClient; + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + LOG.info("Starting DefaultBasicAuthenticatorCacheManager."); + + try { + initUserMaps(); + + 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); + if (userMap != null) { + cachedUserMaps.put(authenticatorPrefix, userMap); + } + } + LOG.debug("Scheduled cache poll is done"); + } + catch (Throwable t) { + LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); + } + } + ); + + lifecycleLock.started(); + LOG.info("Started DefaultBasicAuthenticatorCacheManager."); + } + finally { + lifecycleLock.exitStart(); + } + } + + @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) + { + LOG.debug("Received cache update for authenticator [%s].", authenticatorPrefix); + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + try { + cachedUserMaps.put( + authenticatorPrefix, + objectMapper.readValue( + serializedUserMap, + BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE + ) + ); + + if (commonCacheConfig.getCacheDirectory() != null) { + writeUserMapToDisk(authenticatorPrefix, serializedUserMap); + } + } + catch (Exception e) { + LOG.makeAlert(e, "WTF? Could not deserialize user map received from coordinator.").emit(); + } + } + + @Override + public Map getUserMap(String authenticatorPrefix) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + + return cachedUserMaps.get(authenticatorPrefix); + } + + @Nullable + private Map fetchUserMapFromCoordinator(String prefix, boolean isInit) + { + try { + return RetryUtils.retry( + () -> { + return tryFetchUserMapFromCoordinator(prefix); + }, + e -> true, + commonCacheConfig.getMaxSyncRetries() + ); + } + catch (Exception e) { + LOG.makeAlert(e, "Encountered exception while fetching user map for authenticator [%s]", prefix); + if (isInit) { + if (commonCacheConfig.getCacheDirectory() != null) { + try { + LOG.info("Attempting to load user map snapshot from disk."); + return loadUserMapFromDisk(prefix); + } + catch (Exception e2) { + e2.addSuppressed(e); + 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 IOException + { + 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 IOException + { + 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/db/%s/cachedSerializedUserMap", prefix) + ); + BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go( + req, + new BytesFullResponseHandler() + ); + byte[] userMapBytes = responseHolder.getBytes(); + Map userMap = objectMapper.readValue( + userMapBytes, + BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE + ); + if (userMap != null && commonCacheConfig.getCacheDirectory() != null) { + writeUserMapToDisk(prefix, userMapBytes); + } + return userMap; + } + + private void initUserMaps() + { + AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); + + if (authenticatorMapper == null || authenticatorMapper.getAuthenticatorMap() == null) { + return; + } + + for (Map.Entry entry : authenticatorMapper.getAuthenticatorMap().entrySet()) { + Authenticator authenticator = entry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + String authenticatorName = entry.getKey(); + 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/MetadataStoragePollingBasicAuthenticatorCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/MetadataStoragePollingBasicAuthenticatorCacheManager.java new file mode 100644 index 00000000000..b5b7eea5ccc --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/cache/MetadataStoragePollingBasicAuthenticatorCacheManager.java @@ -0,0 +1,59 @@ +/* + * 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.cache; + +import com.google.inject.Inject; +import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater; +import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser; + +import java.util.Map; + +/** + * 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(MetadataStoragePollingBasicAuthenticatorCacheManager.class); + + private final BasicAuthenticatorMetadataStorageUpdater storageUpdater; + + @Inject + public MetadataStoragePollingBasicAuthenticatorCacheManager( + BasicAuthenticatorMetadataStorageUpdater storageUpdater + ) + { + this.storageUpdater = storageUpdater; + + log.info("Starting coordinator basic authenticator cache manager."); + } + + @Override + public void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap) + { + } + + @Override + public Map getUserMap(String authenticatorPrefix) + { + return storageUpdater.getCachedUserMap(authenticatorPrefix); + } +} 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 new file mode 100644 index 00000000000..a5a9d59825c --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/BasicAuthenticatorMetadataStorageUpdater.java @@ -0,0 +1,48 @@ +/* + * 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.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, BasicAuthenticatorCredentialUpdate update); + + Map getCachedUserMap(String prefix); + + byte[] getCachedSerializedUserMap(String prefix); + + byte[] getCurrentUserMapBytes(String prefix); + + 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 new file mode 100644 index 00000000000..eacc0242237 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/db/updater/CoordinatorBasicAuthenticatorMetadataStorageUpdater.java @@ -0,0 +1,444 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.metamx.emitter.EmittingLogger; +import io.druid.common.config.ConfigManager; +import io.druid.concurrent.LifecycleLock; +import io.druid.guice.ManageLifecycle; +import io.druid.guice.annotations.Smile; +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.concurrent.ScheduledExecutors; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +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.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.util.Collections; +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 +public class CoordinatorBasicAuthenticatorMetadataStorageUpdater implements BasicAuthenticatorMetadataStorageUpdater +{ + private static final EmittingLogger LOG = + new EmittingLogger(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class); + private static final String USERS = "users"; + private static final long UPDATE_RETRY_DELAY = 1000; + + private final AuthenticatorMapper authenticatorMapper; + private final MetadataStorageConnector connector; + private final MetadataStorageTablesConfig connectorConfig; + private final BasicAuthCommonCacheConfig commonCacheConfig; + private final ObjectMapper objectMapper; + private final BasicAuthenticatorCacheNotifier cacheNotifier; + private final int numRetries = 5; + + private final Map cachedUserMaps; + private final Set authenticatorPrefixes; + private final LifecycleLock lifecycleLock = new LifecycleLock(); + + private final ScheduledExecutorService exec; + private volatile boolean stopped = false; + + @Inject + public CoordinatorBasicAuthenticatorMetadataStorageUpdater( + AuthenticatorMapper authenticatorMapper, + MetadataStorageConnector connector, + MetadataStorageTablesConfig connectorConfig, + BasicAuthCommonCacheConfig commonCacheConfig, + @Smile ObjectMapper objectMapper, + BasicAuthenticatorCacheNotifier cacheNotifier, + 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 ConcurrentHashMap<>(); + this.authenticatorPrefixes = new HashSet<>(); + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + if (authenticatorMapper == null || authenticatorMapper.getAuthenticatorMap() == null) { + return; + } + + try { + LOG.info("Starting CoordinatorBasicAuthenticatorMetadataStorageUpdater."); + for (Map.Entry entry : authenticatorMapper.getAuthenticatorMap().entrySet()) { + Authenticator authenticator = entry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + String authenticatorName = entry.getKey(); + authenticatorPrefixes.add(authenticatorName); + BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator; + BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig(); + byte[] userMapBytes = getCurrentUserMapBytes(authenticatorName); + 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(BasicAuthUtils.INTERNAL_USER_NAME)) { + createUserInternal(authenticatorName, BasicAuthUtils.INTERNAL_USER_NAME); + setUserCredentialsInternal( + authenticatorName, + BasicAuthUtils.INTERNAL_USER_NAME, + new BasicAuthenticatorCredentialUpdate( + dbConfig.getInitialInternalClientPassword(), + BasicAuthUtils.DEFAULT_KEY_ITERATIONS + ) + ); + } + } + } + + ScheduledExecutors.scheduleWithFixedDelay( + exec, + new Duration(commonCacheConfig.getPollingPeriod()), + new Duration(commonCacheConfig.getPollingPeriod()), + new Callable() + { + @Override + public ScheduledExecutors.Signal call() throws Exception + { + if (stopped) { + return ScheduledExecutors.Signal.STOP; + } + try { + LOG.debug("Scheduled db poll is running"); + for (String authenticatorPrefix : authenticatorPrefixes) { + + byte[] userMapBytes = getCurrentUserMapBytes(authenticatorPrefix); + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + userMapBytes + ); + if (userMapBytes != null) { + cachedUserMaps.put(authenticatorPrefix, new BasicAuthenticatorUserMapBundle(userMap, userMapBytes)); + } + } + LOG.debug("Scheduled db poll is done"); + } + catch (Throwable t) { + LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); + } + return ScheduledExecutors.Signal.REPEAT; + } + } + ); + + lifecycleLock.started(); + } + finally { + lifecycleLock.exitStart(); + } + } + + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + throw new ISE("can't stop."); + } + + LOG.info("CoordinatorBasicAuthenticatorMetadataStorageUpdater is stopping."); + stopped = true; + LOG.info("CoordinatorBasicAuthenticatorMetadataStorageUpdater is stopped."); + } + + @Override + public void createUser(String prefix, String userName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + createUserInternal(prefix, userName); + } + + @Override + public void deleteUser(String prefix, String userName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + deleteUserInternal(prefix, userName); + } + + @Override + public void setUserCredentials(String prefix, String userName, BasicAuthenticatorCredentialUpdate update) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + setUserCredentialsInternal(prefix, userName, update); + } + + @Override + public Map getCachedUserMap(String prefix) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + + BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix); + if (bundle == null) { + return null; + } else { + return bundle.getUserMap(); + } + } + + @Override + public byte[] getCachedSerializedUserMap(String prefix) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + + BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix); + if (bundle == null) { + return null; + } else { + return bundle.getSerializedUserMap(); + } + } + + @Override + public byte[] getCurrentUserMapBytes(String prefix) + { + return connector.lookup( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, USERS) + ); + } + + @Override + public void refreshAllNotification() + { + cachedUserMaps.forEach( + (authenticatorName, userMapBundle) -> { + cacheNotifier.addUpdate(authenticatorName, userMapBundle.getSerializedUserMap()); + } + ); + } + + private static String getPrefixedKeyColumn(String keyPrefix, String keyName) + { + return StringUtils.format("basic_authentication_%s_%s", keyPrefix, keyName); + } + + private boolean tryUpdateUserMap( + String prefix, + Map userMap, + byte[] oldValue, + byte[] newValue + ) + { + try { + 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) { + throw new RuntimeException(e); + } + } + + private void createUserInternal(String prefix, String userName) + { + int attempts = 0; + while (attempts < numRetries) { + if (createUserOnce(prefix, userName)) { + return; + } 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); + } + + private void deleteUserInternal(String prefix, String userName) + { + int attempts = 0; + while (attempts < numRetries) { + if (deleteUserOnce(prefix, userName)) { + return; + } 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, BasicAuthenticatorCredentialUpdate update) + { + 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)) { + return; + } 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); + } + + private boolean createUserOnce(String prefix, String userName) + { + byte[] oldValue = getCurrentUserMapBytes(prefix); + 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 = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap); + return tryUpdateUserMap(prefix, userMap, oldValue, newValue); + } + + private boolean deleteUserOnce(String prefix, String userName) + { + byte[] oldValue = getCurrentUserMapBytes(prefix); + 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 = 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 = 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 = 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/endpoint/BasicAuthenticatorResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java new file mode 100644 index 00000000000..3ff0c34b9e4 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java @@ -0,0 +1,235 @@ +/* + * 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.endpoint; + +import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes; +import com.google.inject.Inject; +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; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path("/druid-ext/basic-security/authentication") +@LazySingleton +public class BasicAuthenticatorResource +{ + private final BasicAuthenticatorResourceHandler handler; + + @Inject + public BasicAuthenticatorResource( + BasicAuthenticatorResourceHandler handler + ) + { + 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("/db/{authenticatorName}/users") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllUsers( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName + ) + { + return handler.getAllUsers(authenticatorName); + } + + /** + * @param req HTTP request + * @param userName Name of user to retrieve information about + * + * @return Name and credentials of the user with userName, 400 error response if user doesn't exist + */ + @GET + @Path("/db/{authenticatorName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getUser( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName, + @PathParam("userName") final String userName + ) + { + return handler.getUser(authenticatorName, userName); + } + + /** + * Create a new user with name userName + * + * @param req HTTP request + * @param userName Name to assign the new user + * + * @return OK response, or 400 error response if user already exists + */ + @POST + @Path("/db/{authenticatorName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response createUser( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName, + @PathParam("userName") String userName + ) + { + return handler.createUser(authenticatorName, userName); + } + + /** + * Delete a user + * + * @param req HTTP request + * @param userName Name of user to delete + * + * @return OK response, or 400 error response if user doesn't exist + */ + @DELETE + @Path("/db/{authenticatorName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteUser( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName, + @PathParam("userName") String userName + ) + { + return handler.deleteUser(authenticatorName, userName); + } + + /** + * Assign credentials for a user + * + * @param req HTTP request + * @param userName Name of user + * @param password Password to assign + * + * @return OK response, 400 error if user doesn't exist + */ + @POST + @Path("/db/{authenticatorName}/users/{userName}/credentials") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response updateUserCredentials( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName, + @PathParam("userName") String userName, + BasicAuthenticatorCredentialUpdate update + ) + { + return handler.updateUserCredentials(authenticatorName, userName, update); + } + + /** + * @param req HTTP request + * + * @return serialized user map + */ + @GET + @Path("/db/{authenticatorName}/cachedSerializedUserMap") + @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getCachedSerializedUserMap( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName + ) + { + return handler.getCachedSerializedUserMap(authenticatorName); + } + + /** + * Listen for update notifications for the auth storage + * + * @param req HTTP request + * @param userName Name to assign the new user + * + * @return OK response, or 400 error response if user already exists + */ + @POST + @Path("/listen/{authenticatorName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response authenticatorUpdateListener( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName, + byte[] serializedUserMap + ) + { + return handler.authenticatorUpdateListener(authenticatorName, serializedUserMap); + } +} 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 new file mode 100644 index 00000000000..de0d7dd5d7b --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/BasicAuthenticatorResourceHandler.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.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); + + Response createUser(String authenticatorName, String userName); + + Response deleteUser(String authenticatorName, String userName); + + 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 new file mode 100644 index 00000000000..738bd3e793c --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/CoordinatorBasicAuthenticatorResourceHandler.java @@ -0,0 +1,218 @@ +/* + * 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.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.Inject; +import io.druid.guice.annotations.Smile; +import io.druid.java.util.common.StringUtils; +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 final BasicAuthenticatorMetadataStorageUpdater storageUpdater; + private final Map authenticatorMap; + private final ObjectMapper objectMapper; + + @Inject + public CoordinatorBasicAuthenticatorResourceHandler( + BasicAuthenticatorMetadataStorageUpdater storageUpdater, + AuthenticatorMapper authenticatorMapper, + @Smile ObjectMapper objectMapper + ) + { + this.storageUpdater = storageUpdater; + this.objectMapper = objectMapper; + + this.authenticatorMap = Maps.newHashMap(); + for (Map.Entry authenticatorEntry : authenticatorMapper.getAuthenticatorMap().entrySet()) { + final String authenticatorName = authenticatorEntry.getKey(); + final Authenticator authenticator = authenticatorEntry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + authenticatorMap.put( + authenticatorName, + (BasicHTTPAuthenticator) authenticator + ); + } + } + } + + @Override + public Response getAllUsers( + final String authenticatorName + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authenticatorName) + ); + + return Response.ok(userMap.keySet()).build(); + } + + @Override + public Response getUser(String authenticatorName, String userName) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authenticatorName) + ); + + try { + BasicAuthenticatorUser user = userMap.get(userName); + if (user == null) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + return Response.ok(user).build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response createUser(String authenticatorName, String userName) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + storageUpdater.createUser(authenticatorName, userName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response deleteUser(String authenticatorName, String userName) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + storageUpdater.deleteUser(authenticatorName, userName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response updateUserCredentials(String authenticatorName, String userName, BasicAuthenticatorCredentialUpdate update) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + storageUpdater.setUserCredentials(authenticatorName, userName, update); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response getCachedSerializedUserMap(String authenticatorName) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(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) + .entity(ImmutableMap.of( + "error", + StringUtils.format("Basic authenticator with name [%s] does not exist.", authenticatorName) + )) + .build(); + } + + private static Response makeResponseForBasicSecurityDBResourceException(BasicSecurityDBResourceException bsre) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", bsre.getMessage() + )) + .build(); + } +} 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 new file mode 100644 index 00000000000..d1211bf5094 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/endpoint/DefaultBasicAuthenticatorResourceHandler.java @@ -0,0 +1,142 @@ +/* + * 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.endpoint; + +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.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 +{ + private static final Logger log = new Logger(DefaultBasicAuthenticatorResourceHandler.class); + private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build(); + + private final BasicAuthenticatorCacheManager cacheManager; + private final Map authenticatorMap; + + @Inject + public DefaultBasicAuthenticatorResourceHandler( + BasicAuthenticatorCacheManager cacheManager, + AuthenticatorMapper authenticatorMapper + ) + { + this.cacheManager = cacheManager; + + this.authenticatorMap = Maps.newHashMap(); + for (Map.Entry authenticatorEntry : authenticatorMapper.getAuthenticatorMap().entrySet()) { + final String authenticatorName = authenticatorEntry.getKey(); + final Authenticator authenticator = authenticatorEntry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + authenticatorMap.put( + authenticatorName, + (BasicHTTPAuthenticator) authenticator + ); + } + } + } + + @Override + public Response getAllUsers(String authenticatorName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response getUser(String authenticatorName, String userName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response createUser(String authenticatorName, String userName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response deleteUser(String authenticatorName, String userName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response updateUserCredentials( + String authenticatorName, + String userName, + BasicAuthenticatorCredentialUpdate update + ) + { + return NOT_FOUND_RESPONSE; + } + + @Override + 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) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + String errMsg = StringUtils.format("Received update for unknown authenticator[%s]", authenticatorName); + log.error(errMsg); + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format(errMsg) + )) + .build(); + } + + 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 00000000000..36bcb2d6e4d --- /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 new file mode 100644 index 00000000000..e6aea0f51d0 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorCredentials.java @@ -0,0 +1,104 @@ +/* + * 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 io.druid.security.basic.BasicAuthUtils; + +import java.util.Arrays; + +public class BasicAuthenticatorCredentials +{ + private final byte[] salt; + private final byte[] hash; + private final int iterations; + + @JsonCreator + public BasicAuthenticatorCredentials( + @JsonProperty("salt") byte[] salt, + @JsonProperty("hash") byte[] hash, + @JsonProperty("iterations") int iterations + ) + { + Preconditions.checkNotNull(salt); + Preconditions.checkNotNull(hash); + this.salt = salt; + this.hash = hash; + this.iterations = iterations; + } + + public BasicAuthenticatorCredentials(BasicAuthenticatorCredentialUpdate update) + { + this.iterations = update.getIterations(); + this.salt = BasicAuthUtils.generateSalt(); + this.hash = BasicAuthUtils.hashPassword(update.getPassword().toCharArray(), salt, iterations); + } + + @JsonProperty + public byte[] getSalt() + { + return salt; + } + + @JsonProperty + public byte[] getHash() + { + return hash; + } + + @JsonProperty + public int getIterations() + { + return iterations; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthenticatorCredentials that = (BasicAuthenticatorCredentials) o; + + if (getIterations() != that.getIterations()) { + return false; + } + if (!Arrays.equals(getSalt(), that.getSalt())) { + return false; + } + return Arrays.equals(getHash(), that.getHash()); + + } + + @Override + public int hashCode() + { + int result = Arrays.hashCode(getSalt()); + result = 31 * result + Arrays.hashCode(getHash()); + result = 31 * result + getIterations(); + return result; + } +} 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 new file mode 100644 index 00000000000..3a8bd563375 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/entity/BasicAuthenticatorUser.java @@ -0,0 +1,77 @@ +/* + * 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; + +public class BasicAuthenticatorUser +{ + private final String name; + private final BasicAuthenticatorCredentials credentials; + + @JsonCreator + public BasicAuthenticatorUser( + @JsonProperty("name") String name, + @JsonProperty("credentials") BasicAuthenticatorCredentials credentials + ) + { + this.name = name; + this.credentials = credentials; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public BasicAuthenticatorCredentials getCredentials() + { + return credentials; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthenticatorUser that = (BasicAuthenticatorUser) o; + + if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { + return false; + } + return getCredentials() != null ? getCredentials().equals(that.getCredentials()) : that.getCredentials() == null; + } + + @Override + public int hashCode() + { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (getCredentials() != null ? getCredentials().hashCode() : 0); + return result; + } +} 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 00000000000..59cc5405575 --- /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 new file mode 100644 index 00000000000..8dd55c27b25 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -0,0 +1,125 @@ +/* + * 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; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.druid.java.util.common.IAE; +import io.druid.security.basic.BasicAuthDBConfig; +import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager; +import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.server.security.Access; +import io.druid.server.security.Action; +import io.druid.server.security.AuthenticationResult; +import io.druid.server.security.Authorizer; +import io.druid.server.security.Resource; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@JsonTypeName("basic") +public class BasicRoleBasedAuthorizer implements Authorizer +{ + private final BasicAuthorizerCacheManager cacheManager; + private final String name; + private final BasicAuthDBConfig dbConfig; + + + @JsonCreator + public BasicRoleBasedAuthorizer( + @JacksonInject BasicAuthorizerCacheManager cacheManager, + @JsonProperty("name") String name, + @JsonProperty("enableCacheNotifications") Boolean enableCacheNotifications, + @JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout + ) + { + this.name = name; + this.cacheManager = cacheManager; + this.dbConfig = new BasicAuthDBConfig( + null, + null, + enableCacheNotifications == null ? true : enableCacheNotifications, + cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout, + 0 + ); + } + + @Override + public Access authorize( + AuthenticationResult authenticationResult, Resource resource, Action action + ) + { + if (authenticationResult == null) { + throw new IAE("WTF? authenticationResult should never be null."); + } + + Map userMap = cacheManager.getUserMap(name); + if (userMap == null) { + throw new IAE("Could not load userMap for authorizer [%s]", name); + } + + Map roleMap = cacheManager.getRoleMap(name); + if (roleMap == null) { + throw new IAE("Could not load roleMap for authorizer [%s]", name); + } + + BasicAuthorizerUser user = userMap.get(authenticationResult.getIdentity()); + if (user == null) { + return new Access(false); + } + + for (String roleName : user.getRoles()) { + BasicAuthorizerRole role = roleMap.get(roleName); + for (BasicAuthorizerPermission permission : role.getPermissions()) { + if (permissionCheck(resource, action, permission)) { + return new Access(true); + } + } + } + + return new Access(false); + } + + private boolean permissionCheck(Resource resource, Action action, BasicAuthorizerPermission permission) + { + if (action != permission.getResourceAction().getAction()) { + return false; + } + + Resource permissionResource = permission.getResourceAction().getResource(); + if (permissionResource.getType() != resource.getType()) { + return false; + } + + Pattern resourceNamePattern = permission.getResourceNamePattern(); + Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName()); + return resourceNameMatcher.matches(); + } + + public BasicAuthDBConfig getDbConfig() + { + return dbConfig; + } +} 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 new file mode 100644 index 00000000000..77eaecd0b67 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheManager.java @@ -0,0 +1,56 @@ +/* + * 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.cache; + +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; + +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 new file mode 100644 index 00000000000..95bd80759d4 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/BasicAuthorizerCacheNotifier.java @@ -0,0 +1,34 @@ +/* + * 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.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 new file mode 100644 index 00000000000..924107cf92a --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorBasicAuthorizerCacheNotifier.java @@ -0,0 +1,122 @@ +/* + * 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.cache; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import com.metamx.emitter.EmittingLogger; +import com.metamx.http.client.HttpClient; +import io.druid.concurrent.LifecycleLock; +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.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +import io.druid.security.basic.BasicAuthDBConfig; +import io.druid.security.basic.CommonCacheNotifier; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +import io.druid.server.security.Authorizer; +import io.druid.server.security.AuthorizerMapper; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@ManageLifecycle +public class CoordinatorBasicAuthorizerCacheNotifier implements BasicAuthorizerCacheNotifier +{ + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthorizerCacheNotifier.class); + + private final LifecycleLock lifecycleLock = new LifecycleLock(); + private CommonCacheNotifier cacheNotifier; + + @Inject + public CoordinatorBasicAuthorizerCacheNotifier( + AuthorizerMapper authorizerMapper, + DruidNodeDiscoveryProvider discoveryProvider, + @EscalatedClient HttpClient httpClient + ) + { + cacheNotifier = new CommonCacheNotifier( + getAuthorizerConfigMap(authorizerMapper), + discoveryProvider, + httpClient, + "/druid-ext/basic-security/authorization/listen/%s", + "CoordinatorBasicAuthorizerCacheNotifier" + ); + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + try { + cacheNotifier.start(); + lifecycleLock.started(); + } + finally { + lifecycleLock.exitStart(); + } + } + + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + return; + } + try { + cacheNotifier.stop(); + } + finally { + lifecycleLock.exitStop(); + } + } + + @Override + public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap); + } + + private Map getAuthorizerConfigMap(AuthorizerMapper mapper) + { + 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) { + String authorizerName = entry.getKey(); + BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer; + BasicAuthDBConfig dbConfig = basicRoleBasedAuthorizer.getDbConfig(); + authorizerConfigMap.put(authorizerName, dbConfig); + } + } + + return authorizerConfigMap; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java new file mode 100644 index 00000000000..a57d931c97f --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/CoordinatorPollingBasicAuthorizerCacheManager.java @@ -0,0 +1,293 @@ +/* + * 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.cache; + +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; +import com.metamx.http.client.Request; +import io.druid.client.coordinator.Coordinator; +import io.druid.concurrent.LifecycleLock; +import io.druid.discovery.DruidLeaderClient; +import io.druid.guice.ManageLifecycle; +import io.druid.guice.annotations.Smile; +import io.druid.java.util.common.ISE; +import io.druid.java.util.common.RetryUtils; +import io.druid.java.util.common.StringUtils; +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.authorization.BasicRoleBasedAuthorizer; +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 io.druid.server.security.Authorizer; +import io.druid.server.security.AuthorizerMapper; +import org.jboss.netty.handler.codec.http.HttpMethod; +import org.joda.time.Duration; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +@ManageLifecycle +public class CoordinatorPollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager +{ + private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthorizerCacheManager.class); + + private final ConcurrentHashMap> cachedUserMaps; + private final ConcurrentHashMap> cachedRoleMaps; + + private final Set authorizerPrefixes; + private final Injector injector; + private final ObjectMapper objectMapper; + private final LifecycleLock lifecycleLock = new LifecycleLock(); + private final DruidLeaderClient druidLeaderClient; + private final BasicAuthCommonCacheConfig commonCacheConfig; + private final ScheduledExecutorService exec; + + @Inject + 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; + this.cachedUserMaps = new ConcurrentHashMap<>(); + this.cachedRoleMaps = new ConcurrentHashMap<>(); + this.authorizerPrefixes = new HashSet<>(); + this.druidLeaderClient = druidLeaderClient; + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + LOG.info("Starting CoordinatorPollingBasicAuthorizerCacheManager."); + + try { + initUserMaps(); + + 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); + if (userAndRoleMap != null) { + cachedUserMaps.put(authorizerPrefix, userAndRoleMap.getUserMap()); + cachedRoleMaps.put(authorizerPrefix, userAndRoleMap.getRoleMap()); + } + } + LOG.debug("Scheduled cache poll is done"); + } + catch (Throwable t) { + LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); + } + } + ); + + lifecycleLock.started(); + 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) + { + LOG.debug("Received cache update for authorizer [%s].", authorizerPrefix); + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + try { + UserAndRoleMap userAndRoleMap = objectMapper.readValue( + serializedUserAndRoleMap, + 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 (Exception e) { + LOG.makeAlert(e, "WTF? Could not deserialize user/role map received from coordinator.").emit(); + } + } + + @Override + public Map getUserMap(String authorizerPrefix) + { + return cachedUserMaps.get(authorizerPrefix); + } + + @Override + public Map getRoleMap(String authorizerPrefix) + { + return cachedRoleMaps.get(authorizerPrefix); + } + + private String getUserRoleMapFilename(String prefix) + { + return StringUtils.format("%s.authorizer.cache", prefix); + } + + @Nullable + private UserAndRoleMap loadUserAndRoleMapFromDisk(String prefix) throws IOException + { + 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 IOException + { + 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( + () -> { + return tryFetchMapsFromCoordinator(prefix); + }, + e -> true, + commonCacheConfig.getMaxSyncRetries() + ); + } + catch (Exception e) { + 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) { + e2.addSuppressed(e); + LOG.makeAlert(e2, "Encountered exception while loading user-role map snapshot for authorizer [%s]", prefix); + } + } + } + return null; + } + } + + private UserAndRoleMap tryFetchMapsFromCoordinator( + String prefix + ) throws Exception + { + Request req = druidLeaderClient.makeRequest( + HttpMethod.GET, + StringUtils.format("/druid-ext/basic-security/authorization/db/%s/cachedSerializedUserMap", prefix) + ); + BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go( + req, + new BytesFullResponseHandler() + ); + byte[] userRoleMapBytes = responseHolder.getBytes(); + + UserAndRoleMap userAndRoleMap = objectMapper.readValue( + userRoleMapBytes, + BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE + ); + if (userAndRoleMap != null && commonCacheConfig.getCacheDirectory() != null) { + writeMapToDisk(prefix, userRoleMapBytes); + } + return userAndRoleMap; + } + + private void initUserMaps() + { + AuthorizerMapper authorizerMapper = injector.getInstance(AuthorizerMapper.class); + + if (authorizerMapper == null || authorizerMapper.getAuthorizerMap() == null) { + return; + } + + for (Map.Entry entry : authorizerMapper.getAuthorizerMap().entrySet()) { + Authorizer authorizer = entry.getValue(); + if (authorizer instanceof BasicRoleBasedAuthorizer) { + String authorizerName = entry.getKey(); + 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/MetadataStoragePollingBasicAuthorizerCacheManager.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/MetadataStoragePollingBasicAuthorizerCacheManager.java new file mode 100644 index 00000000000..36b1d7a3176 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/cache/MetadataStoragePollingBasicAuthorizerCacheManager.java @@ -0,0 +1,58 @@ +/* + * 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.cache; + +import com.google.inject.Inject; +import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; + +import java.util.Map; + +public class MetadataStoragePollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager +{ + private final BasicAuthorizerMetadataStorageUpdater storageUpdater; + + @Inject + public MetadataStoragePollingBasicAuthorizerCacheManager( + BasicAuthorizerMetadataStorageUpdater storageUpdater + ) + { + this.storageUpdater = storageUpdater; + } + + @Override + public void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap) + { + + } + + @Override + public Map getUserMap(String authorizerPrefix) + { + return storageUpdater.getCachedUserMap(authorizerPrefix); + } + + @Override + public Map getRoleMap(String authorizerPrefix) + { + return storageUpdater.getCachedRoleMap(authorizerPrefix); + } +} 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 new file mode 100644 index 00000000000..2cd148037fe --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/BasicAuthorizerMetadataStorageUpdater.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * 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); + + void deleteUser(String prefix, String userName); + + void createRole(String prefix, String roleName); + + void deleteRole(String prefix, String roleName); + + void assignRole(String prefix, String userName, String roleName); + + void unassignRole(String prefix, String userName, String roleName); + + void setPermissions(String prefix, String roleName, List permissions); + + Map getCachedUserMap(String prefix); + + Map getCachedRoleMap(String prefix); + + 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 new file mode 100644 index 00000000000..ca80d19712d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java @@ -0,0 +1,790 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.metamx.emitter.EmittingLogger; +import io.druid.common.config.ConfigManager; +import io.druid.concurrent.LifecycleLock; +import io.druid.guice.ManageLifecycle; +import io.druid.guice.annotations.Smile; +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.concurrent.ScheduledExecutors; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.lifecycle.LifecycleStop; +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.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; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.Resource; +import io.druid.server.security.ResourceAction; +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.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 +public class CoordinatorBasicAuthorizerMetadataStorageUpdater implements BasicAuthorizerMetadataStorageUpdater +{ + private static final EmittingLogger LOG = + new EmittingLogger(CoordinatorBasicAuthorizerMetadataStorageUpdater.class); + + private static final long UPDATE_RETRY_DELAY = 1000; + + private static final String USERS = "users"; + private static final String ROLES = "roles"; + + public static final List SUPERUSER_PERMISSIONS = makeSuperUserPermissions(); + + private final AuthorizerMapper authorizerMapper; + private final MetadataStorageConnector connector; + private final MetadataStorageTablesConfig connectorConfig; + private final BasicAuthorizerCacheNotifier cacheNotifier; + private final BasicAuthCommonCacheConfig commonCacheConfig; + private final ObjectMapper objectMapper; + private final int numRetries = 5; + + private final Map cachedUserMaps; + private final Map cachedRoleMaps; + + private final Set authorizerNames; + private final LifecycleLock lifecycleLock = new LifecycleLock(); + + private final ScheduledExecutorService exec; + private volatile boolean stopped = false; + + @Inject + public CoordinatorBasicAuthorizerMetadataStorageUpdater( + AuthorizerMapper authorizerMapper, + MetadataStorageConnector connector, + MetadataStorageTablesConfig connectorConfig, + BasicAuthCommonCacheConfig commonCacheConfig, + @Smile ObjectMapper objectMapper, + BasicAuthorizerCacheNotifier cacheNotifier, + 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 ConcurrentHashMap<>(); + this.cachedRoleMaps = new ConcurrentHashMap<>(); + this.authorizerNames = new HashSet<>(); + } + + @LifecycleStart + public void start() + { + if (!lifecycleLock.canStart()) { + throw new ISE("can't start."); + } + + if (authorizerMapper == null || authorizerMapper.getAuthorizerMap() == null) { + return; + } + + try { + LOG.info("Starting CoordinatorBasicAuthorizerMetadataStorageUpdater"); + for (Map.Entry entry : authorizerMapper.getAuthorizerMap().entrySet()) { + Authorizer authorizer = entry.getValue(); + if (authorizer instanceof BasicRoleBasedAuthorizer) { + String authorizerName = entry.getKey(); + authorizerNames.add(authorizerName); + BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer; + + byte[] userMapBytes = getCurrentUserMapBytes(authorizerName); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + userMapBytes + ); + cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes)); + + byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + roleMapBytes + ); + cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes)); + + initSuperusers(authorizerName, userMap, roleMap); + } + } + + ScheduledExecutors.scheduleWithFixedDelay( + exec, + new Duration(commonCacheConfig.getPollingPeriod()), + new Duration(commonCacheConfig.getPollingPeriod()), + new Callable() + { + @Override + public ScheduledExecutors.Signal call() throws Exception + { + if (stopped) { + return ScheduledExecutors.Signal.STOP; + } + try { + LOG.debug("Scheduled db poll is running"); + for (String authorizerName : authorizerNames) { + + byte[] userMapBytes = getCurrentUserMapBytes(authorizerName); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + userMapBytes + ); + if (userMapBytes != null) { + synchronized (cachedUserMaps) { + cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes)); + } + } + + byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + roleMapBytes + ); + if (roleMapBytes != null) { + synchronized (cachedUserMaps) { + cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes)); + } + } + } + LOG.debug("Scheduled db poll is done"); + } + catch (Throwable t) { + LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit(); + } + return ScheduledExecutors.Signal.REPEAT; + } + } + ); + + lifecycleLock.started(); + } + finally { + lifecycleLock.exitStart(); + } + } + + @LifecycleStop + public void stop() + { + if (!lifecycleLock.canStop()) { + throw new ISE("can't stop."); + } + + LOG.info("CoordinatorBasicAuthorizerMetadataStorageUpdater is stopping."); + stopped = true; + LOG.info("CoordinatorBasicAuthorizerMetadataStorageUpdater is stopped."); + } + + + private static String getPrefixedKeyColumn(String keyPrefix, String keyName) + { + return StringUtils.format("basic_authorization_%s_%s", keyPrefix, keyName); + } + + private boolean tryUpdateUserMap( + String prefix, + Map userMap, + byte[] oldUserMapValue, + byte[] newUserMapValue + ) + { + return tryUpdateUserAndRoleMap(prefix, userMap, oldUserMapValue, newUserMapValue, null, null, null); + } + + private boolean tryUpdateRoleMap( + String prefix, + Map roleMap, + byte[] oldRoleMapValue, + byte[] newRoleMapValue + ) + { + return tryUpdateUserAndRoleMap(prefix, null, null, null, roleMap, oldRoleMapValue, newRoleMapValue); + } + + private boolean tryUpdateUserAndRoleMap( + String prefix, + Map userMap, + byte[] oldUserMapValue, + byte[] newUserMapValue, + Map roleMap, + byte[] oldRoleMapValue, + byte[] newRoleMapValue + ) + { + try { + 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) { + cachedUserMaps.put(prefix, new BasicAuthorizerUserMapBundle(userMap, newUserMapValue)); + } + if (roleMap != null) { + cachedRoleMaps.put(prefix, new BasicAuthorizerRoleMapBundle(roleMap, newRoleMapValue)); + } + + byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(prefix); + cacheNotifier.addUpdate(prefix, serializedUserAndRoleMap); + + return true; + } else { + return false; + } + + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void createUser(String prefix, String userName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + createUserInternal(prefix, userName); + } + + @Override + public void deleteUser(String prefix, String userName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + deleteUserInternal(prefix, userName); + } + + @Override + public void createRole(String prefix, String roleName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + createRoleInternal(prefix, roleName); + } + + @Override + public void deleteRole(String prefix, String roleName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + deleteRoleInternal(prefix, roleName); + } + + @Override + public void assignRole(String prefix, String userName, String roleName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + assignRoleInternal(prefix, userName, roleName); + } + + @Override + public void unassignRole(String prefix, String userName, String roleName) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + unassignRoleInternal(prefix, userName, roleName); + } + + @Override + public void setPermissions( + String prefix, String roleName, List permissions + ) + { + Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); + setPermissionsInternal(prefix, roleName, permissions); + } + + @Override + @Nullable + public Map getCachedUserMap(String prefix) + { + BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix); + return userMapBundle == null ? null : userMapBundle.getUserMap(); + } + + @Override + @Nullable + public Map getCachedRoleMap(String prefix) + { + BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix); + return roleMapBundle == null ? null : roleMapBundle.getRoleMap(); + } + + @Override + public byte[] getCurrentUserMapBytes(String prefix) + { + return connector.lookup( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, USERS) + ); + } + + @Override + public byte[] getCurrentRoleMapBytes(String prefix) + { + return connector.lookup( + connectorConfig.getConfigTable(), + MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN, + MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN, + getPrefixedKeyColumn(prefix, ROLES) + ); + } + + @Override + public void refreshAllNotification() + { + authorizerNames.forEach( + (authorizerName) -> { + try { + byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(authorizerName); + cacheNotifier.addUpdate(authorizerName, serializedUserAndRoleMap); + } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + ); + } + + private byte[] getCurrentUserAndRoleMapSerialized(String prefix) throws IOException + { + BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix); + BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix); + + UserAndRoleMap userAndRoleMap = new UserAndRoleMap( + userMapBundle == null ? null : userMapBundle.getUserMap(), + roleMapBundle == null ? null : roleMapBundle.getRoleMap() + ); + + return objectMapper.writeValueAsBytes(userAndRoleMap); + } + + private void createUserInternal(String prefix, String userName) + { + int attempts = 0; + while (attempts < numRetries) { + if (createUserOnce(prefix, userName)) { + return; + } 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); + } + + private void deleteUserInternal(String prefix, String userName) + { + int attempts = 0; + while (attempts < numRetries) { + if (deleteUserOnce(prefix, userName)) { + return; + } 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 createRoleInternal(String prefix, String roleName) + { + int attempts = 0; + while (attempts < numRetries) { + if (createRoleOnce(prefix, roleName)) { + return; + } 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); + } + + private void deleteRoleInternal(String prefix, String roleName) + { + int attempts = 0; + while (attempts < numRetries) { + if (deleteRoleOnce(prefix, roleName)) { + return; + } 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); + } + + private void assignRoleInternal(String prefix, String userName, String roleName) + { + int attempts = 0; + while (attempts < numRetries) { + if (assignRoleOnce(prefix, userName, roleName)) { + return; + } 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); + } + + private void unassignRoleInternal(String prefix, String userName, String roleName) + { + int attempts = 0; + while (attempts < numRetries) { + if (unassignRoleOnce(prefix, userName, roleName)) { + return; + } 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); + } + + private void setPermissionsInternal(String prefix, String roleName, List permissions) + { + int attempts = 0; + while (attempts < numRetries) { + if (setPermissionsOnce(prefix, roleName, permissions)) { + return; + } else { + attempts++; + } + try { + Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY)); + } + catch (InterruptedException ie) { + throw new RuntimeException(ie); + } + } + throw new ISE("Could not set permissions for role[%s] due to concurrent update contention.", roleName); + } + + private boolean createUserOnce(String prefix, String userName) + { + byte[] oldValue = getCurrentUserMapBytes(prefix); + 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 = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); + return tryUpdateUserMap(prefix, userMap, oldValue, newValue); + } + + private boolean deleteUserOnce(String prefix, String userName) + { + byte[] oldValue = getCurrentUserMapBytes(prefix); + 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 = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); + return tryUpdateUserMap(prefix, userMap, oldValue, newValue); + } + + private boolean createRoleOnce(String prefix, String roleName) + { + byte[] oldValue = getCurrentRoleMapBytes(prefix); + 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 = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap); + return tryUpdateRoleMap(prefix, roleMap, oldValue, newValue); + } + + private boolean deleteRoleOnce(String prefix, String roleName) + { + byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); + if (roleMap.get(roleName) == null) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } else { + roleMap.remove(roleName); + } + + byte[] oldUserMapValue = getCurrentUserMapBytes(prefix); + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); + for (BasicAuthorizerUser user : userMap.values()) { + user.getRoles().remove(roleName); + } + byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap); + byte[] newRoleMapValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap); + + return tryUpdateUserAndRoleMap( + prefix, + userMap, oldUserMapValue, newUserMapValue, + roleMap, oldRoleMapValue, newRoleMapValue + ); + } + + private boolean assignRoleOnce(String prefix, String userName, String roleName) + { + byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); + 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 = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); + BasicAuthorizerUser user = userMap.get(userName); + if (userMap.get(userName) == null) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + if (user.getRoles().contains(roleName)) { + throw new BasicSecurityDBResourceException("User [%s] already has role [%s].", userName, roleName); + } + + user.getRoles().add(roleName); + 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( + prefix, + userMap, oldUserMapValue, newUserMapValue, + roleMap, oldRoleMapValue, oldRoleMapValue + ); + } + + private boolean unassignRoleOnce(String prefix, String userName, String roleName) + { + byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); + 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 = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + oldUserMapValue + ); + BasicAuthorizerUser user = userMap.get(userName); + if (userMap.get(userName) == null) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + if (!user.getRoles().contains(roleName)) { + throw new BasicSecurityDBResourceException("User [%s] does not have role [%s].", userName, roleName); + } + + user.getRoles().remove(roleName); + 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( + prefix, + userMap, oldUserMapValue, newUserMapValue, + roleMap, oldRoleMapValue, oldRoleMapValue + ); + } + + private boolean setPermissionsOnce(String prefix, String roleName, List permissions) + { + byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix); + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + oldRoleMapValue + ); + if (roleMap.get(roleName) == null) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + roleMap.put( + roleName, + new BasicAuthorizerRole(roleName, BasicAuthorizerPermission.makePermissionList(permissions)) + ); + byte[] newRoleMapValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap); + + return tryUpdateRoleMap(prefix, roleMap, oldRoleMapValue, newRoleMapValue); + } + + private void initSuperusers( + String authorizerName, + Map userMap, + Map roleMap + ) + { + if (!roleMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { + createRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME); + setPermissionsInternal(authorizerName, BasicAuthUtils.ADMIN_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(BasicAuthUtils.INTERNAL_USER_NAME)) { + createUserInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME); + assignRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, BasicAuthUtils.INTERNAL_USER_NAME); + } + + if (!userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { + createUserInternal(authorizerName, BasicAuthUtils.ADMIN_NAME); + assignRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.ADMIN_NAME); + } + } + + private static List makeSuperUserPermissions() + { + ResourceAction datasourceR = new ResourceAction( + new Resource(".*", ResourceType.DATASOURCE), + Action.READ + ); + + ResourceAction datasourceW = new ResourceAction( + new Resource(".*", ResourceType.DATASOURCE), + Action.WRITE + ); + + ResourceAction configR = new ResourceAction( + new Resource(".*", ResourceType.CONFIG), + Action.READ + ); + + ResourceAction configW = new ResourceAction( + new Resource(".*", ResourceType.CONFIG), + Action.WRITE + ); + + ResourceAction stateR = new ResourceAction( + new Resource(".*", ResourceType.STATE), + Action.READ + ); + + ResourceAction stateW = new ResourceAction( + new Resource(".*", ResourceType.STATE), + Action.WRITE + ); + + return Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); + } +} 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 new file mode 100644 index 00000000000..1951cfb1128 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java @@ -0,0 +1,373 @@ +/* + * 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.endpoint; + +import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes; +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ResourceFilters; +import io.druid.guice.LazySingleton; +import io.druid.security.basic.BasicSecurityResourceFilter; +import io.druid.server.security.ResourceAction; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +@Path("/druid-ext/basic-security/authorization") +@LazySingleton +public class BasicAuthorizerResource +{ + private BasicAuthorizerResourceHandler resourceHandler; + + @Inject + public BasicAuthorizerResource( + BasicAuthorizerResourceHandler resourceHandler + ) + { + 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("/db/{authorizerName}/users") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllUsers( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName + ) + { + return resourceHandler.getAllUsers(authorizerName); + } + + /** + * @param req HTTP request + * @param userName Name of user to retrieve information about + * + * @return Name, roles, and permissions of the user with userName, 400 error response if user doesn't exist + */ + @GET + @Path("/db/{authorizerName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getUser( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("userName") final String userName, + @QueryParam("full") String full + ) + { + return resourceHandler.getUser(authorizerName, userName, full != null); + } + + /** + * Create a new user with name userName + * + * @param req HTTP request + * @param userName Name to assign the new user + * + * @return OK response, or 400 error response if user already exists + */ + @POST + @Path("/db/{authorizerName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response createUser( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("userName") String userName + ) + { + return resourceHandler.createUser(authorizerName, userName); + } + + /** + * Delete a user + * + * @param req HTTP request + * @param userName Name of user to delete + * + * @return OK response, or 400 error response if user doesn't exist + */ + @DELETE + @Path("/db/{authorizerName}/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteUser( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("userName") String userName + ) + { + return resourceHandler.deleteUser(authorizerName, userName); + } + + /** + * @param req HTTP request + * + * @return List of all roles + */ + @GET + @Path("/db/{authorizerName}/roles") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllRoles( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName + ) + { + return resourceHandler.getAllRoles(authorizerName); + } + + /** + * Get info about a role + * + * @param req HTTP request + * @param roleName Name of role + * + * @return Role name, users with role, and permissions of role. 400 error if role doesn't exist. + */ + @GET + @Path("/db/{authorizerName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getRole( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("roleName") final String roleName, + @QueryParam("full") String full + ) + { + return resourceHandler.getRole(authorizerName, roleName, full != null); + } + + /** + * Create a new role. + * + * @param req HTTP request + * @param roleName Name of role + * + * @return OK response, 400 error if role already exists + */ + @POST + @Path("/db/{authorizerName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response createRole( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("roleName") final String roleName + ) + { + return resourceHandler.createRole(authorizerName, roleName); + } + + /** + * Delete a role. + * + * @param req HTTP request + * @param roleName Name of role + * + * @return OK response, 400 error if role doesn't exist. + */ + @DELETE + @Path("/db/{authorizerName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteRole( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("roleName") String roleName + ) + { + return resourceHandler.deleteRole(authorizerName, roleName); + } + + /** + * Assign a role to a user. + * + * @param req HTTP request + * @param userName Name of user + * @param roleName Name of role + * + * @return OK response. 400 error if user/role don't exist, or if user already has the role + */ + @POST + @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response assignRoleToUser( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("userName") String userName, + @PathParam("roleName") String roleName + ) + { + return resourceHandler.assignRoleToUser(authorizerName, userName, roleName); + } + + /** + * Remove a role from a user. + * + * @param req HTTP request + * @param userName Name of user + * @param roleName Name of role + * + * @return OK response. 400 error if user/role don't exist, or if user does not have the role. + */ + @DELETE + @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response unassignRoleFromUser( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("userName") String userName, + @PathParam("roleName") String roleName + ) + { + return resourceHandler.unassignRoleFromUser(authorizerName, userName, roleName); + } + + /** + * Set the permissions of a role. This replaces the previous permissions of the role. + * + * @param req HTTP request + * @param roleName Name of role + * @param resourceActions Permissions to set + * + * @return OK response. 400 error if role doesn't exist. + */ + @POST + @Path("/db/{authorizerName}/roles/{roleName}/permissions") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response setRolePermissions( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("roleName") String roleName, + List permissions + ) + { + return resourceHandler.setRolePermissions(authorizerName, roleName, permissions); + } + + /** + * @param req HTTP request + * + * @return serialized user map + */ + @GET + @Path("/db/{authorizerName}/cachedSerializedUserMap") + @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getCachedSerializedUserMap( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName + ) + { + return resourceHandler.getCachedMaps(authorizerName); + } + + + /** + * Listen for update notifications for the auth storage + * + * @param req HTTP request + * @param userName Name to assign the new user + * + * @return OK response, or 400 error response if user already exists + */ + @POST + @Path("/listen/{authorizerName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response authorizerUpdateListener( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + byte[] serializedUserAndRoleMap + ) + { + return resourceHandler.authorizerUpdateListener(authorizerName, serializedUserAndRoleMap); + } +} 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 new file mode 100644 index 00000000000..2186f8d86fb --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/BasicAuthorizerResourceHandler.java @@ -0,0 +1,66 @@ +/* + * 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.endpoint; + +import io.druid.server.security.ResourceAction; + +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); + + Response createUser(String authorizerName, String userName); + + Response deleteUser(String authorizerName, String userName); + + Response getAllRoles(String authorizerName); + + Response getRole(String authorizerName, String roleName, boolean isFull); + + Response createRole(String authorizerName, String roleName); + + Response deleteRole(String authorizerName, String roleName); + + Response assignRoleToUser(String authorizerName, String userName, String roleName); + + Response unassignRoleFromUser(String authorizerName, String userName, String roleName); + + Response setRolePermissions(String authorizerName, String roleName, List permissions); + + 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 new file mode 100644 index 00000000000..407b893952a --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/CoordinatorBasicAuthorizerResourceHandler.java @@ -0,0 +1,431 @@ +/* + * 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.endpoint; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.Inject; +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; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleFull; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUserFull; +import io.druid.security.basic.authorization.entity.UserAndRoleMap; +import io.druid.server.security.Authorizer; +import io.druid.server.security.AuthorizerMapper; +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; +import java.util.Set; + +public class CoordinatorBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler +{ + private static final Logger log = new Logger(CoordinatorBasicAuthorizerResourceHandler.class); + + private final BasicAuthorizerMetadataStorageUpdater storageUpdater; + private final Map authorizerMap; + private final ObjectMapper objectMapper; + + @Inject + public CoordinatorBasicAuthorizerResourceHandler( + BasicAuthorizerMetadataStorageUpdater storageUpdater, + AuthorizerMapper authorizerMapper, + @Smile ObjectMapper objectMapper + ) + { + this.storageUpdater = storageUpdater; + this.objectMapper = objectMapper; + + this.authorizerMap = Maps.newHashMap(); + for (Map.Entry authorizerEntry : authorizerMapper.getAuthorizerMap().entrySet()) { + final String authorizerName = authorizerEntry.getKey(); + final Authorizer authorizer = authorizerEntry.getValue(); + if (authorizer instanceof BasicRoleBasedAuthorizer) { + authorizerMap.put( + authorizerName, + (BasicRoleBasedAuthorizer) authorizer + ); + } + } + } + + @Override + public Response getAllUsers(String authorizerName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authorizerName) + ); + return Response.ok(userMap.keySet()).build(); + } + + @Override + public Response getUser(String authorizerName, String userName, boolean isFull) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + if (isFull) { + return getUserFull(authorizerName, userName); + } else { + return getUserSimple(authorizerName, userName); + } + } + + @Override + public Response createUser(String authorizerName, String userName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.createUser(authorizerName, userName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response deleteUser(String authorizerName, String userName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.deleteUser(authorizerName, userName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response getAllRoles(String authorizerName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + storageUpdater.getCurrentRoleMapBytes(authorizerName) + ); + + return Response.ok(roleMap.keySet()).build(); + } + + @Override + public Response getRole(String authorizerName, String roleName, boolean isFull) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + if (isFull) { + return getRoleFull(authorizerName, roleName); + } else { + return getRoleSimple(authorizerName, roleName); + } + } + + @Override + public Response createRole(String authorizerName, String roleName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.createRole(authorizerName, roleName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response deleteRole(String authorizerName, String roleName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.deleteRole(authorizerName, roleName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response assignRoleToUser(String authorizerName, String userName, String roleName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.assignRole(authorizerName, userName, roleName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response unassignRoleFromUser(String authorizerName, String userName, String roleName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.unassignRole(authorizerName, userName, roleName); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response setRolePermissions( + String authorizerName, String roleName, List permissions + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + storageUpdater.setPermissions(authorizerName, roleName, permissions); + return Response.ok().build(); + } + catch (BasicSecurityDBResourceException cfe) { + return makeResponseForBasicSecurityDBResourceException(cfe); + } + } + + @Override + public Response getCachedMaps(String authorizerName) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + UserAndRoleMap userAndRoleMap = new UserAndRoleMap( + storageUpdater.getCachedUserMap(authorizerName), + storageUpdater.getCachedRoleMap(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) + .entity(ImmutableMap.of( + "error", + StringUtils.format("Basic authorizer with name [%s] does not exist.", authorizerName) + )) + .build(); + } + + private static Response makeResponseForBasicSecurityDBResourceException(BasicSecurityDBResourceException bsre) + { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", bsre.getMessage() + )) + .build(); + } + + private Response getUserSimple(String authorizerName, String userName) + { + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authorizerName) + ); + + try { + BasicAuthorizerUser user = userMap.get(userName); + if (user == null) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + return Response.ok(user).build(); + } + catch (BasicSecurityDBResourceException e) { + return makeResponseForBasicSecurityDBResourceException(e); + } + } + + private Response getUserFull(String authorizerName, String userName) + { + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authorizerName) + ); + + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + storageUpdater.getCurrentRoleMapBytes(authorizerName) + ); + + try { + BasicAuthorizerUser user = userMap.get(userName); + if (user == null) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + Set roles = new HashSet<>(); + for (String roleName : user.getRoles()) { + BasicAuthorizerRole role = roleMap.get(roleName); + if (role == null) { + log.error("User [%s] had role [%s], but role was not found.", userName, roleName); + } else { + roles.add(role); + } + } + + BasicAuthorizerUserFull fullUser = new BasicAuthorizerUserFull(userName, roles); + return Response.ok(fullUser).build(); + } + catch (BasicSecurityDBResourceException e) { + return makeResponseForBasicSecurityDBResourceException(e); + } + } + + private Response getRoleSimple(String authorizerName, String roleName) + { + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + storageUpdater.getCurrentRoleMapBytes(authorizerName) + ); + + try { + BasicAuthorizerRole role = roleMap.get(roleName); + if (role == null) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + return Response.ok(role).build(); + } + catch (BasicSecurityDBResourceException e) { + return makeResponseForBasicSecurityDBResourceException(e); + } + } + + private Response getRoleFull(String authorizerName, String roleName) + { + Map roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + storageUpdater.getCurrentRoleMapBytes(authorizerName) + ); + + Map userMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + storageUpdater.getCurrentUserMapBytes(authorizerName) + ); + + Set users = new HashSet<>(); + for (BasicAuthorizerUser user : userMap.values()) { + if (user.getRoles().contains(roleName)) { + users.add(user.getName()); + } + } + + try { + BasicAuthorizerRole role = roleMap.get(roleName); + if (role == null) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + BasicAuthorizerRoleFull roleFull = new BasicAuthorizerRoleFull( + roleName, + users, + role.getPermissions() + ); + return Response.ok(roleFull).build(); + } + catch (BasicSecurityDBResourceException e) { + return makeResponseForBasicSecurityDBResourceException(e); + } + } +} 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 new file mode 100644 index 00000000000..2531bebe6d5 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/endpoint/DefaultBasicAuthorizerResourceHandler.java @@ -0,0 +1,178 @@ +/* + * 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.endpoint; + +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.authorization.BasicRoleBasedAuthorizer; +import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager; +import io.druid.server.security.Authorizer; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.ResourceAction; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DefaultBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler +{ + private static final Logger log = new Logger(DefaultBasicAuthorizerResourceHandler.class); + private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build(); + + private final BasicAuthorizerCacheManager cacheManager; + private final Map authorizerMap; + + @Inject + public DefaultBasicAuthorizerResourceHandler( + BasicAuthorizerCacheManager cacheManager, + AuthorizerMapper authorizerMapper + ) + { + this.cacheManager = cacheManager; + + this.authorizerMap = Maps.newHashMap(); + for (Map.Entry authorizerEntry : authorizerMapper.getAuthorizerMap().entrySet()) { + final String authorizerName = authorizerEntry.getKey(); + final Authorizer authorizer = authorizerEntry.getValue(); + if (authorizer instanceof BasicRoleBasedAuthorizer) { + authorizerMap.put( + authorizerName, + (BasicRoleBasedAuthorizer) authorizer + ); + } + } + } + + + @Override + public Response getAllUsers(String authorizerName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response getUser(String authorizerName, String userName, boolean isFull) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response createUser(String authorizerName, String userName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response deleteUser(String authorizerName, String userName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response getAllRoles(String authorizerName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response getRole(String authorizerName, String roleName, boolean isFull) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response createRole(String authorizerName, String roleName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response deleteRole(String authorizerName, String roleName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response assignRoleToUser(String authorizerName, String userName, String roleName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response unassignRoleFromUser(String authorizerName, String userName, String roleName) + { + return NOT_FOUND_RESPONSE; + } + + @Override + public Response setRolePermissions( + String authorizerName, String roleName, List permissions + ) + { + return NOT_FOUND_RESPONSE; + } + + @Override + 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) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + String errMsg = StringUtils.format("Received update for unknown authorizer[%s]", authorizerName); + log.error(errMsg); + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format(errMsg) + )) + .build(); + } + + 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 new file mode 100644 index 00000000000..643b84a98c3 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerPermission.java @@ -0,0 +1,122 @@ +/* + * 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 io.druid.security.basic.BasicSecurityDBResourceException; +import io.druid.server.security.ResourceAction; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public class BasicAuthorizerPermission +{ + private final ResourceAction resourceAction; + private final Pattern resourceNamePattern; + + @JsonCreator + public BasicAuthorizerPermission( + @JsonProperty("resourceAction") ResourceAction resourceAction, + @JsonProperty("resourceNamePattern") Pattern resourceNamePattern + ) + { + this.resourceAction = resourceAction; + this.resourceNamePattern = resourceNamePattern; + } + + public BasicAuthorizerPermission( + ResourceAction resourceAction + ) + { + this.resourceAction = resourceAction; + try { + this.resourceNamePattern = Pattern.compile(resourceAction.getResource().getName()); + } + catch (PatternSyntaxException pse) { + throw new BasicSecurityDBResourceException( + pse, + "Invalid permission, resource name regex[%s] does not compile.", + resourceAction.getResource().getName() + ); + } + } + + @JsonProperty + public ResourceAction getResourceAction() + { + return resourceAction; + } + + @JsonProperty + public Pattern getResourceNamePattern() + { + return resourceNamePattern; + } + + public static List makePermissionList(List resourceActions) + { + List permissions = new ArrayList<>(); + + if (resourceActions == null) { + return permissions; + } + + for (ResourceAction resourceAction : resourceActions) { + permissions.add(new BasicAuthorizerPermission(resourceAction)); + } + return permissions; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthorizerPermission that = (BasicAuthorizerPermission) o; + + if (getResourceAction() != null + ? !getResourceAction().equals(that.getResourceAction()) + : that.getResourceAction() != null) { + return false; + } + return getResourceNamePattern() != null + ? getResourceNamePattern().pattern().equals(that.getResourceNamePattern().pattern()) + : that.getResourceNamePattern() == null; + + } + + @Override + public int hashCode() + { + int result = getResourceAction() != null ? getResourceAction().hashCode() : 0; + result = 31 * result + (getResourceNamePattern().pattern() != null + ? getResourceNamePattern().pattern().hashCode() + : 0); + return result; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRole.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRole.java new file mode 100644 index 00000000000..7ee4bfd362d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRole.java @@ -0,0 +1,81 @@ +/* + * 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.ArrayList; +import java.util.List; + +public class BasicAuthorizerRole +{ + private final String name; + private final List permissions; + + @JsonCreator + public BasicAuthorizerRole( + @JsonProperty("name") String name, + @JsonProperty("permissions") List permissions + ) + { + this.name = name; + this.permissions = permissions == null ? new ArrayList<>() : permissions; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public List getPermissions() + { + return permissions; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthorizerRole role = (BasicAuthorizerRole) o; + + if (getName() != null ? !getName().equals(role.getName()) : role.getName() != null) { + return false; + } + return getPermissions() != null ? getPermissions().equals(role.getPermissions()) : role.getPermissions() == null; + + } + + @Override + public int hashCode() + { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (getPermissions() != null ? getPermissions().hashCode() : 0); + return result; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleFull.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleFull.java new file mode 100644 index 00000000000..4e3dfe794b3 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerRoleFull.java @@ -0,0 +1,95 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Set; + +public class BasicAuthorizerRoleFull +{ + private final String name; + private final Set users; + private final List permissions; + + @JsonCreator + public BasicAuthorizerRoleFull( + @JsonProperty("name") String name, + @JsonProperty("users") Set users, + @JsonProperty("permissions") List permissions + ) + { + this.name = name; + this.users = users; + this.permissions = permissions == null ? new ArrayList<>() : permissions; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public List getPermissions() + { + return permissions; + } + + @JsonProperty + public Set getUsers() + { + return users; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthorizerRoleFull that = (BasicAuthorizerRoleFull) o; + + if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { + return false; + } + if (getUsers() != null ? !getUsers().equals(that.getUsers()) : that.getUsers() != null) { + return false; + } + return getPermissions() != null ? getPermissions().equals(that.getPermissions()) : that.getPermissions() == null; + + } + + @Override + public int hashCode() + { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (getUsers() != null ? getUsers().hashCode() : 0); + result = 31 * result + (getPermissions() != null ? getPermissions().hashCode() : 0); + return result; + } +} 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 00000000000..9ea457a7f69 --- /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/BasicAuthorizerUser.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUser.java new file mode 100644 index 00000000000..8fdb4ede167 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUser.java @@ -0,0 +1,81 @@ +/* + * 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.HashSet; +import java.util.Set; + +public class BasicAuthorizerUser +{ + private final String name; + private final Set roles; + + @JsonCreator + public BasicAuthorizerUser( + @JsonProperty("name") String name, + @JsonProperty("roles") Set roles + ) + { + this.name = name; + this.roles = roles == null ? new HashSet<>() : roles; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Set getRoles() + { + return roles; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthorizerUser that = (BasicAuthorizerUser) o; + + if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { + return false; + } + return getRoles() != null ? getRoles().equals(that.getRoles()) : that.getRoles() == null; + + } + + @Override + public int hashCode() + { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (getRoles() != null ? getRoles().hashCode() : 0); + return result; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserFull.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserFull.java new file mode 100644 index 00000000000..074f825a784 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/BasicAuthorizerUserFull.java @@ -0,0 +1,81 @@ +/* + * 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.HashSet; +import java.util.Set; + +public class BasicAuthorizerUserFull +{ + private final String name; + private final Set roles; + + @JsonCreator + public BasicAuthorizerUserFull( + @JsonProperty("name") String name, + @JsonProperty("roles") Set roles + ) + { + this.name = name; + this.roles = roles == null ? new HashSet<>() : roles; + } + + @JsonProperty + public String getName() + { + return name; + } + + @JsonProperty + public Set getRoles() + { + return roles; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicAuthorizerUserFull that = (BasicAuthorizerUserFull) o; + + if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) { + return false; + } + return getRoles() != null ? getRoles().equals(that.getRoles()) : that.getRoles() == null; + + } + + @Override + public int hashCode() + { + int result = getName() != null ? getName().hashCode() : 0; + result = 31 * result + (getRoles() != null ? getRoles().hashCode() : 0); + return result; + } +} 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 00000000000..109c36ec07f --- /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/main/java/io/druid/security/basic/authorization/entity/UserAndRoleMap.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/UserAndRoleMap.java new file mode 100644 index 00000000000..e362c4d5caf --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/entity/UserAndRoleMap.java @@ -0,0 +1,56 @@ +/* + * 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 UserAndRoleMap +{ + @JsonProperty + private Map userMap; + + @JsonProperty + private Map roleMap; + + @JsonCreator + public UserAndRoleMap( + @JsonProperty("userMap") Map userMap, + @JsonProperty("roleMap") Map roleMap + ) + { + this.userMap = userMap; + this.roleMap = roleMap; + } + + @JsonProperty + public Map getUserMap() + { + return userMap; + } + + @JsonProperty + public Map getRoleMap() + { + return roleMap; + } +} diff --git a/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.initialization.DruidModule b/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.initialization.DruidModule new file mode 100644 index 00000000000..02de8e2e58f --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.initialization.DruidModule @@ -0,0 +1 @@ +io.druid.security.basic.BasicSecurityDruidModule \ No newline at end of file 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 new file mode 100644 index 00000000000..c453126f699 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java @@ -0,0 +1,39 @@ +/* + * 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; + +import io.druid.security.basic.BasicAuthUtils; +import org.junit.Assert; +import org.junit.Test; + +public class BasicAuthUtilsTest +{ + @Test + public void testHashPassword() + { + char[] password = "HELLO".toCharArray(); + int iterations = BasicAuthUtils.DEFAULT_KEY_ITERATIONS; + byte[] salt = BasicAuthUtils.generateSalt(); + byte[] hash = BasicAuthUtils.hashPassword(password, salt, iterations); + + Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length); + Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length); + } +} 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 new file mode 100644 index 00000000000..3ea3b11847d --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest.java @@ -0,0 +1,212 @@ +/* + * 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.authentication; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import io.druid.guice.GuiceInjectors; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.annotations.Self; +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.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; +import io.druid.server.security.AuthenticatorMapper; +import io.druid.server.security.Escalator; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Map; + +public class CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest +{ + private final static String AUTHENTICATOR_NAME = "test"; + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule(); + + 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(); + + updater = new CoordinatorBasicAuthenticatorMetadataStorageUpdater( + new AuthenticatorMapper( + ImmutableMap.of( + "test", + new BasicHTTPAuthenticator( + null, + "test", + "test", + null, + null, + null, + null, + null + ) + ) + ), + connector, + tablesConfig, + new BasicAuthCommonCacheConfig(null, null, null, null), + objectMapper, + new NoopBasicAuthenticatorCacheNotifier(), + null + ); + + updater.start(); + } + + @After + public void tearDown() throws Exception + { + updater.stop(); + } + + @Test + public void createUser() + { + updater.createUser(AUTHENTICATOR_NAME, "druid"); + Map expectedUserMap = ImmutableMap.of( + "druid", new BasicAuthenticatorUser("druid", null) + ); + Map actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); + Assert.assertEquals(expectedUserMap, actualUserMap); + + // create duplicate should fail + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] already exists."); + updater.createUser(AUTHENTICATOR_NAME, "druid"); + } + + @Test + public void deleteUser() + { + updater.createUser(AUTHENTICATOR_NAME, "druid"); + updater.deleteUser(AUTHENTICATOR_NAME, "druid"); + Map expectedUserMap = ImmutableMap.of(); + Map actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); + Assert.assertEquals(expectedUserMap, actualUserMap); + + // delete non-existent user should fail + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] does not exist."); + updater.deleteUser(AUTHENTICATOR_NAME, "druid"); + } + + @Test + public void setCredentials() + { + updater.createUser(AUTHENTICATOR_NAME, "druid"); + updater.setUserCredentials(AUTHENTICATOR_NAME, "druid", new BasicAuthenticatorCredentialUpdate("helloworld", null)); + + Map userMap = BasicAuthUtils.deserializeAuthenticatorUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME) + ); + BasicAuthenticatorCredentials credentials = userMap.get("druid").getCredentials(); + + byte[] recalculatedHash = BasicAuthUtils.hashPassword( + "helloworld".toCharArray(), + credentials.getSalt(), + credentials.getIterations() + ); + + Assert.assertArrayEquals(credentials.getHash(), recalculatedHash); + } + + private Injector setupInjector() + { + return Initialization.makeInjectorWithModules( + GuiceInjectors.makeStartupInjector(), + ImmutableList.of( + new Module() + { + @Override + public void configure(Binder binder) + { + JsonConfigProvider.bindInstance( + binder, + Key.get(DruidNode.class, Self.class), + new DruidNode("test", "localhost", null, null, true, false) + ); + + binder.bind(Escalator.class).toInstance( + new BasicHTTPEscalator(null, null, null) + ); + + binder.bind(AuthenticatorMapper.class).toInstance( + new AuthenticatorMapper( + ImmutableMap.of( + "test", + new BasicHTTPAuthenticator( + null, + "test", + "test", + 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 new file mode 100644 index 00000000000..059a4816530 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/CoordinatorBasicAuthenticatorResourceTest.java @@ -0,0 +1,323 @@ +/* + * 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.authentication; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; +import io.druid.guice.GuiceInjectors; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.annotations.Self; +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.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; +import io.druid.server.security.AuthenticatorMapper; +import io.druid.server.security.Escalator; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import java.util.Map; +import java.util.Set; + +public class CoordinatorBasicAuthenticatorResourceTest +{ + private final static String AUTHENTICATOR_NAME = "test"; + private final static String AUTHENTICATOR_NAME2 = "test2"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule(); + + private TestDerbyConnector connector; + private MetadataStorageTablesConfig tablesConfig; + private BasicAuthenticatorResource resource; + private CoordinatorBasicAuthenticatorMetadataStorageUpdater storageUpdater; + private HttpServletRequest req; + + @Before + public void setUp() throws Exception + { + req = EasyMock.createStrictMock(HttpServletRequest.class); + + connector = derbyConnectorRule.getConnector(); + tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); + connector.createConfigTable(); + + ObjectMapper objectMapper = new ObjectMapper(new SmileFactory()); + + AuthenticatorMapper authenticatorMapper = new AuthenticatorMapper( + ImmutableMap.of( + AUTHENTICATOR_NAME, + new BasicHTTPAuthenticator( + null, + AUTHENTICATOR_NAME, + "test", + "druid", + "druid", + null, + null, + null + ), + AUTHENTICATOR_NAME2, + new BasicHTTPAuthenticator( + null, + AUTHENTICATOR_NAME2, + "test", + "druid", + "druid", + null, + null, + null + ) + ) + ); + + storageUpdater = new CoordinatorBasicAuthenticatorMetadataStorageUpdater( + authenticatorMapper, + connector, + tablesConfig, + new BasicAuthCommonCacheConfig(null, null, null, null), + objectMapper, + new NoopBasicAuthenticatorCacheNotifier(), + null + ); + + resource = new BasicAuthenticatorResource( + new CoordinatorBasicAuthenticatorResourceHandler( + storageUpdater, + authenticatorMapper, + objectMapper + ) + ); + + storageUpdater.start(); + } + + @After + public void tearDown() throws Exception + { + storageUpdater.stop(); + } + + @Test + public void testInvalidAuthenticator() + { + Response response = resource.getAllUsers(req, "invalidName"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals( + errorMapWithMsg("Basic authenticator with name [invalidName] does not exist."), + response.getEntity() + ); + } + + @Test + public void testGetAllUsers() + { + Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + 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( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid", + "druid2", + "druid3" + ); + + response = resource.getAllUsers(req, AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + } + + @Test + public void testSeparateDatabaseTables() + { + Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + 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"); + + resource.createUser(req, AUTHENTICATOR_NAME2, "druid4"); + resource.createUser(req, AUTHENTICATOR_NAME2, "druid5"); + resource.createUser(req, AUTHENTICATOR_NAME2, "druid6"); + + Set expectedUsers = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid", + "druid2", + "druid3" + ); + + Set expectedUsers2 = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid4", + "druid5", + "druid6" + ); + + response = resource.getAllUsers(req, AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + + response = resource.getAllUsers(req, AUTHENTICATOR_NAME2); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers2, response.getEntity()); + } + + @Test + public void testCreateDeleteUser() + { + Response response = resource.createUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + BasicAuthenticatorUser expectedUser = new BasicAuthenticatorUser("druid", null); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = resource.getUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + @Test + public void testUserCredentials() + { + Response response = resource.createUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.updateUserCredentials( + req, + AUTHENTICATOR_NAME, + "druid", + new BasicAuthenticatorCredentialUpdate("helloworld", null) + ); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + BasicAuthenticatorUser actualUser = (BasicAuthenticatorUser) response.getEntity(); + Assert.assertEquals("druid", actualUser.getName()); + BasicAuthenticatorCredentials credentials = actualUser.getCredentials(); + + byte[] salt = credentials.getSalt(); + byte[] hash = credentials.getHash(); + int iterations = credentials.getIterations(); + Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length); + Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length); + Assert.assertEquals(BasicAuthUtils.DEFAULT_KEY_ITERATIONS, iterations); + + byte[] recalculatedHash = BasicAuthUtils.hashPassword( + "helloworld".toCharArray(), + salt, + iterations + ); + Assert.assertArrayEquals(recalculatedHash, hash); + + response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + 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()); + } + + private static Map errorMapWithMsg(String errorMsg) + { + return ImmutableMap.of("error", errorMsg); + } + + private Injector setupInjector() + { + return Initialization.makeInjectorWithModules( + GuiceInjectors.makeStartupInjector(), + ImmutableList.of( + new Module() + { + @Override + public void configure(Binder binder) + { + JsonConfigProvider.bindInstance( + binder, + Key.get(DruidNode.class, Self.class), + new DruidNode("test", "localhost", null, null, true, false) + ); + + binder.bind(Escalator.class).toInstance( + new BasicHTTPEscalator(null, null, null) + ); + } + } + ) + ); + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java new file mode 100644 index 00000000000..22809cc8d4c --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authentication/NoopBasicAuthenticatorCacheNotifier.java @@ -0,0 +1,31 @@ +/* + * 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.authentication; + +import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier; + +public class NoopBasicAuthenticatorCacheNotifier implements BasicAuthenticatorCacheNotifier +{ + @Override + public void addUpdate(String updatedAuthenticatorPrefix, byte[] updatedUserMap) + { + + } +} 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 new file mode 100644 index 00000000000..6132753c684 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/BasicRoleBasedAuthorizerTest.java @@ -0,0 +1,135 @@ +/* + * 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.authorization; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import io.druid.metadata.MetadataStorageTablesConfig; +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.MetadataStoragePollingBasicAuthorizerCacheManager; +import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater; +import io.druid.server.security.Access; +import io.druid.server.security.Action; +import io.druid.server.security.AuthenticationResult; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.Resource; +import io.druid.server.security.ResourceAction; +import io.druid.server.security.ResourceType; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.List; + +public class BasicRoleBasedAuthorizerTest +{ + private static final String AUTHORIZER_NAME = "test"; + + @Rule + public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule(); + + private BasicRoleBasedAuthorizer authorizer; + private TestDerbyConnector connector; + private MetadataStorageTablesConfig tablesConfig; + private CoordinatorBasicAuthorizerMetadataStorageUpdater updater; + + @Before + public void setUp() throws Exception + { + connector = derbyConnectorRule.getConnector(); + tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); + connector.createConfigTable(); + + updater = new CoordinatorBasicAuthorizerMetadataStorageUpdater( + new AuthorizerMapper( + ImmutableMap.of( + AUTHORIZER_NAME, + new BasicRoleBasedAuthorizer( + null, + AUTHORIZER_NAME, + null, + null + ) + ) + ), + connector, + tablesConfig, + new BasicAuthCommonCacheConfig(null, null, null, null), + new ObjectMapper(new SmileFactory()), + new NoopBasicAuthorizerCacheNotifier(), + null + ); + + updater.start(); + + authorizer = new BasicRoleBasedAuthorizer( + new MetadataStoragePollingBasicAuthorizerCacheManager( + updater + ), + AUTHORIZER_NAME, + null, + null + ); + } + + @After + public void tearDown() throws Exception + { + } + + @Test + public void testAuth() + { + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole"); + + List permissions = Lists.newArrayList( + new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ) + ); + + updater.setPermissions(AUTHORIZER_NAME, "druidRole", permissions); + + AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null); + + Access access = authorizer.authorize( + authenticationResult, + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ); + Assert.assertTrue(access.isAllowed()); + + access = authorizer.authorize( + authenticationResult, + new Resource("wrongResource", ResourceType.DATASOURCE), + Action.WRITE + ); + Assert.assertFalse(access.isAllowed()); + } +} 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 new file mode 100644 index 00000000000..d1f6b1fb17e --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java @@ -0,0 +1,377 @@ +/* + * 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.authorization; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +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.authorization.BasicRoleBasedAuthorizer; +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; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.server.security.Action; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.Resource; +import io.druid.server.security.ResourceAction; +import io.druid.server.security.ResourceType; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.List; +import java.util.Map; + +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.INTERNAL_USER_NAME)) + ); + + private final static Map BASE_ROLE_MAP = ImmutableMap.of( + BasicAuthUtils.ADMIN_NAME, + new BasicAuthorizerRole( + BasicAuthUtils.ADMIN_NAME, + BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) + ), + BasicAuthUtils.INTERNAL_USER_NAME, + new BasicAuthorizerRole( + BasicAuthUtils.INTERNAL_USER_NAME, + BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) + ) + ); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule(); + + 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(); + + updater = new CoordinatorBasicAuthorizerMetadataStorageUpdater( + new AuthorizerMapper( + ImmutableMap.of( + AUTHORIZER_NAME, + new BasicRoleBasedAuthorizer( + null, + AUTHORIZER_NAME, + null, + null + ) + ) + ), + connector, + tablesConfig, + new BasicAuthCommonCacheConfig(null, null, null, null), + objectMapper, + new NoopBasicAuthorizerCacheNotifier(), + null + ); + + updater.start(); + } + + // user tests + @Test + 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 = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + Assert.assertEquals(expectedUserMap, actualUserMap); + + updater.deleteUser(AUTHORIZER_NAME, "druid"); + expectedUserMap.remove("druid"); + actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + Assert.assertEquals(expectedUserMap, actualUserMap); + } + + @Test + public void testDeleteNonExistentUser() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] does not exist."); + updater.deleteUser(AUTHORIZER_NAME, "druid"); + } + + @Test + public void testCreateDuplicateUser() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] already exists."); + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createUser(AUTHORIZER_NAME, "druid"); + } + + // role tests + @Test + 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 = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + + updater.deleteRole(AUTHORIZER_NAME, "druid"); + expectedRoleMap.remove("druid"); + actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + } + + @Test + public void testDeleteNonExistentRole() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("Role [druid] does not exist."); + updater.deleteRole(AUTHORIZER_NAME, "druid"); + } + + @Test + public void testCreateDuplicateRole() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("Role [druid] already exists."); + updater.createRole(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druid"); + } + + // role and user tests + @Test + public void testAddAndRemoveRole() throws Exception + { + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole"); + + Map expectedUserMap = Maps.newHashMap(BASE_USER_MAP); + expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole"))); + + Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); + expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); + + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + + Assert.assertEquals(expectedUserMap, actualUserMap); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + + updater.unassignRole(AUTHORIZER_NAME, "druid", "druidRole"); + expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of())); + actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + + Assert.assertEquals(expectedUserMap, actualUserMap); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + } + + @Test + public void testAddRoleToNonExistentUser() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [nonUser] does not exist."); + updater.createRole(AUTHORIZER_NAME, "druid"); + updater.assignRole(AUTHORIZER_NAME, "nonUser", "druid"); + } + + @Test + public void testAddNonexistentRoleToUser() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("Role [nonRole] does not exist."); + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.assignRole(AUTHORIZER_NAME, "druid", "nonRole"); + } + + @Test + public void testAddExistingRoleToUserFails() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] already has role [druidRole]."); + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole"); + updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole"); + } + + @Test + public void testUnassignInvalidRoleAssignmentFails() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("User [druid] does not have role [druidRole]."); + + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + + Map expectedUserMap = Maps.newHashMap(BASE_USER_MAP); + expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of())); + + Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); + expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); + + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + + Assert.assertEquals(expectedUserMap, actualUserMap); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + + updater.unassignRole(AUTHORIZER_NAME, "druid", "druidRole"); + } + + // role and permission tests + @Test + public void testSetRolePermissions() throws Exception + { + updater.createUser(AUTHORIZER_NAME, "druid"); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole"); + + List permsToAdd = ImmutableList.of( + new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ) + ); + + updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd); + + Map expectedUserMap = Maps.newHashMap(BASE_USER_MAP); + expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole"))); + + Map expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP); + expectedRoleMap.put( + "druidRole", + new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(permsToAdd)) + ); + + Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( + objectMapper, + updater.getCurrentUserMapBytes(AUTHORIZER_NAME) + ); + + Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + + Assert.assertEquals(expectedUserMap, actualUserMap); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + + updater.setPermissions(AUTHORIZER_NAME, "druidRole", null); + expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", null)); + actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( + objectMapper, + updater.getCurrentRoleMapBytes(AUTHORIZER_NAME) + ); + + Assert.assertEquals(expectedUserMap, actualUserMap); + Assert.assertEquals(expectedRoleMap, actualRoleMap); + } + + @Test + public void testAddPermissionToNonExistentRole() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("Role [druidRole] does not exist."); + + List permsToAdd = ImmutableList.of( + new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ) + ); + + updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd); + } + + @Test + public void testAddBadPermission() throws Exception + { + expectedException.expect(BasicSecurityDBResourceException.class); + expectedException.expectMessage("Invalid permission, resource name regex[??????????] does not compile."); + updater.createRole(AUTHORIZER_NAME, "druidRole"); + + List permsToAdd = ImmutableList.of( + new ResourceAction( + new Resource("??????????", ResourceType.DATASOURCE), + Action.WRITE + ) + ); + + updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd); + } +} 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 new file mode 100644 index 00000000000..763b1f7d5f9 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java @@ -0,0 +1,588 @@ +/* + * 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.authorization; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +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.authorization.BasicRoleBasedAuthorizer; +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; +import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRole; +import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleFull; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUser; +import io.druid.security.basic.authorization.entity.BasicAuthorizerUserFull; +import io.druid.server.security.Action; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.Resource; +import io.druid.server.security.ResourceAction; +import io.druid.server.security.ResourceType; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CoordinatorBasicAuthorizerResourceTest +{ + private final static String AUTHORIZER_NAME = "test"; + private final static String AUTHORIZER_NAME2 = "test2"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule(); + + private TestDerbyConnector connector; + private MetadataStorageTablesConfig tablesConfig; + private BasicAuthorizerResource resource; + private CoordinatorBasicAuthorizerMetadataStorageUpdater storageUpdater; + private HttpServletRequest req; + + @Before + public void setUp() throws Exception + { + req = EasyMock.createStrictMock(HttpServletRequest.class); + + connector = derbyConnectorRule.getConnector(); + tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); + connector.createConfigTable(); + + AuthorizerMapper authorizerMapper = new AuthorizerMapper( + ImmutableMap.of( + AUTHORIZER_NAME, + new BasicRoleBasedAuthorizer( + null, + AUTHORIZER_NAME, + null, + null + ), + AUTHORIZER_NAME2, + new BasicRoleBasedAuthorizer( + null, + AUTHORIZER_NAME2, + null, + null + ) + ) + ); + + storageUpdater = new CoordinatorBasicAuthorizerMetadataStorageUpdater( + authorizerMapper, + connector, + tablesConfig, + new BasicAuthCommonCacheConfig(null, null, null, null), + new ObjectMapper(new SmileFactory()), + new NoopBasicAuthorizerCacheNotifier(), + null + ); + + resource = new BasicAuthorizerResource( + new CoordinatorBasicAuthorizerResourceHandler( + storageUpdater, + authorizerMapper, + new ObjectMapper(new SmileFactory()) + ) + ); + + storageUpdater.start(); + } + + @After + public void tearDown() throws Exception + { + storageUpdater.stop(); + } + + @Test + public void testSeparateDatabaseTables() + { + Response response = resource.getAllUsers(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals( + ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), + response.getEntity() + ); + + resource.createUser(req, AUTHORIZER_NAME, "druid"); + resource.createUser(req, AUTHORIZER_NAME, "druid2"); + resource.createUser(req, AUTHORIZER_NAME, "druid3"); + + resource.createUser(req, AUTHORIZER_NAME2, "druid4"); + resource.createUser(req, AUTHORIZER_NAME2, "druid5"); + resource.createUser(req, AUTHORIZER_NAME2, "druid6"); + + Set expectedUsers = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid", + "druid2", + "druid3" + ); + + Set expectedUsers2 = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid4", + "druid5", + "druid6" + ); + + response = resource.getAllUsers(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + + response = resource.getAllUsers(req, AUTHORIZER_NAME2); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers2, response.getEntity()); + } + + @Test + public void testInvalidAuthorizer() + { + Response response = resource.getAllUsers(req, "invalidName"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals( + errorMapWithMsg("Basic authorizer with name [invalidName] does not exist."), + response.getEntity() + ); + } + + @Test + public void testGetAllUsers() + { + Response response = resource.getAllUsers(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals( + ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), + response.getEntity() + ); + + resource.createUser(req, AUTHORIZER_NAME, "druid"); + resource.createUser(req, AUTHORIZER_NAME, "druid2"); + resource.createUser(req, AUTHORIZER_NAME, "druid3"); + + Set expectedUsers = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid", + "druid2", + "druid3" + ); + + response = resource.getAllUsers(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + } + + @Test + public void testGetAllRoles() + { + Response response = resource.getAllRoles(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals( + ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), + response.getEntity() + ); + + resource.createRole(req, AUTHORIZER_NAME, "druid"); + resource.createRole(req, AUTHORIZER_NAME, "druid2"); + resource.createRole(req, AUTHORIZER_NAME, "druid3"); + + Set expectedRoles = ImmutableSet.of( + BasicAuthUtils.ADMIN_NAME, + BasicAuthUtils.INTERNAL_USER_NAME, + "druid", + "druid2", + "druid3" + ); + + response = resource.getAllRoles(req, AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoles, response.getEntity()); + } + + @Test + public void testCreateDeleteUser() + { + Response response = resource.createUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(200, response.getStatus()); + + BasicAuthorizerUser expectedUser = new BasicAuthorizerUser( + "druid", + ImmutableSet.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.deleteUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + @Test + public void testCreateDeleteRole() + { + Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + + BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of()); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); + } + + @Test + public void testRoleAssignment() throws Exception + { + Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(200, response.getStatus()); + + BasicAuthorizerUser expectedUser = new BasicAuthorizerUser( + "druid", + ImmutableSet.of("druidRole") + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of()); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(200, response.getStatus()); + expectedUser = new BasicAuthorizerUser( + "druid", + ImmutableSet.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRole, response.getEntity()); + } + + @Test + public void testDeleteAssignedRole() + { + Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(200, response.getStatus()); + BasicAuthorizerUser expectedUser = new BasicAuthorizerUser( + "druid", + ImmutableSet.of("druidRole") + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid2", null); + Assert.assertEquals(200, response.getStatus()); + BasicAuthorizerUser expectedUser2 = new BasicAuthorizerUser( + "druid2", + ImmutableSet.of("druidRole") + ); + Assert.assertEquals(expectedUser2, response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of()); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", null); + Assert.assertEquals(200, response.getStatus()); + expectedUser = new BasicAuthorizerUser( + "druid", + ImmutableSet.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid2", null); + Assert.assertEquals(200, response.getStatus()); + expectedUser2 = new BasicAuthorizerUser( + "druid2", + ImmutableSet.of() + ); + Assert.assertEquals(expectedUser2, response.getEntity()); + } + + @Test + public void testRolesAndPerms() + { + Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + List perms = ImmutableList.of( + new ResourceAction(new Resource("A", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("B", ResourceType.DATASOURCE), Action.WRITE), + new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE) + ); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "wrongRole", perms); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [wrongRole] does not exist."), response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms)); + Assert.assertEquals(expectedRole, response.getEntity()); + + List newPerms = ImmutableList.of( + new ResourceAction(new Resource("D", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("B", ResourceType.DATASOURCE), Action.WRITE), + new ResourceAction(new Resource("F", ResourceType.CONFIG), Action.WRITE) + ); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", newPerms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(newPerms)); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null); + Assert.assertEquals(200, response.getStatus()); + expectedRole = new BasicAuthorizerRole("druidRole", null); + Assert.assertEquals(expectedRole, response.getEntity()); + } + + @Test + public void testUsersRolesAndPerms() + { + Response response = resource.createUser(req, AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createRole(req, AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createRole(req, AUTHORIZER_NAME, "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + List perms = ImmutableList.of( + new ResourceAction(new Resource("A", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("B", ResourceType.DATASOURCE), Action.WRITE), + new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE) + ); + + List perms2 = ImmutableList.of( + new ResourceAction(new Resource("D", ResourceType.STATE), Action.READ), + new ResourceAction(new Resource("E", ResourceType.DATASOURCE), Action.WRITE), + new ResourceAction(new Resource("F", ResourceType.CONFIG), Action.WRITE) + ); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole2", perms2); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms)); + BasicAuthorizerRole expectedRole2 = new BasicAuthorizerRole("druidRole2", BasicAuthorizerPermission.makePermissionList(perms2)); + Set expectedRoles = Sets.newHashSet(expectedRole, expectedRole2); + + BasicAuthorizerUserFull expectedUserFull = new BasicAuthorizerUserFull("druid", expectedRoles); + response = resource.getUser(req, AUTHORIZER_NAME, "druid", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull, response.getEntity()); + + BasicAuthorizerUserFull expectedUserFull2 = new BasicAuthorizerUserFull("druid2", expectedRoles); + response = resource.getUser(req, AUTHORIZER_NAME, "druid2", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull2, response.getEntity()); + + Set expectedUserSet = Sets.newHashSet("druid", "druid2"); + BasicAuthorizerRoleFull expectedRoleFull = new BasicAuthorizerRoleFull( + "druidRole", + expectedUserSet, + BasicAuthorizerPermission.makePermissionList(perms) + ); + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoleFull, response.getEntity()); + + BasicAuthorizerRoleFull expectedRoleFull2 = new BasicAuthorizerRoleFull( + "druidRole2", + expectedUserSet, + BasicAuthorizerPermission.makePermissionList(perms2) + ); + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole2", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoleFull2, response.getEntity()); + + + perms = ImmutableList.of( + new ResourceAction(new Resource("A", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE) + ); + + perms2 = ImmutableList.of( + new ResourceAction(new Resource("E", ResourceType.DATASOURCE), Action.WRITE) + ); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole2", perms2); + Assert.assertEquals(200, response.getStatus()); + + expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms)); + expectedRole2 = new BasicAuthorizerRole("druidRole2", BasicAuthorizerPermission.makePermissionList(perms2)); + expectedRoles = Sets.newHashSet(expectedRole, expectedRole2); + expectedUserFull = new BasicAuthorizerUserFull("druid", expectedRoles); + expectedUserFull2 = new BasicAuthorizerUserFull("druid2", expectedRoles); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull, response.getEntity()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid2", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull2, response.getEntity()); + + response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + + expectedUserFull = new BasicAuthorizerUserFull("druid", Sets.newHashSet(expectedRole2)); + expectedUserFull2 = new BasicAuthorizerUserFull("druid2", Sets.newHashSet(expectedRole)); + expectedRoleFull = new BasicAuthorizerRoleFull( + "druidRole", + Sets.newHashSet("druid2"), + BasicAuthorizerPermission.makePermissionList(perms) + ); + expectedRoleFull2 = new BasicAuthorizerRoleFull( + "druidRole2", + Sets.newHashSet("druid"), + BasicAuthorizerPermission.makePermissionList(perms2) + ); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull, response.getEntity()); + + response = resource.getUser(req, AUTHORIZER_NAME, "druid2", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUserFull2, response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoleFull, response.getEntity()); + + response = resource.getRole(req, AUTHORIZER_NAME, "druidRole2", ""); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoleFull2, response.getEntity()); + } + + private static Map errorMapWithMsg(String errorMsg) + { + return ImmutableMap.of("error", errorMsg); + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java new file mode 100644 index 00000000000..ca37a214c82 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/authorization/NoopBasicAuthorizerCacheNotifier.java @@ -0,0 +1,31 @@ +/* + * 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.authorization; + +import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier; + +public class NoopBasicAuthorizerCacheNotifier implements BasicAuthorizerCacheNotifier +{ + @Override + public void addUpdate(String authorizerPrefix, byte[] userAndRoleMap) + { + + } +} diff --git a/extensions-core/lookups-cached-single/src/main/java/io/druid/server/lookup/cache/polling/PollingCache.java b/extensions-core/lookups-cached-single/src/main/java/io/druid/server/lookup/cache/polling/PollingCache.java index f9ac3414fea..589911e7413 100644 --- a/extensions-core/lookups-cached-single/src/main/java/io/druid/server/lookup/cache/polling/PollingCache.java +++ b/extensions-core/lookups-cached-single/src/main/java/io/druid/server/lookup/cache/polling/PollingCache.java @@ -41,5 +41,4 @@ public interface PollingCache * close and clean the resources used by the cache */ void close(); - } diff --git a/integration-tests/docker/broker.conf b/integration-tests/docker/broker.conf index 894e0fe15d5..e7e744d5db8 100644 --- a/integration-tests/docker/broker.conf +++ b/integration-tests/docker/broker.conf @@ -21,6 +21,19 @@ command=java -Ddruid.broker.cache.populateCache=true -Ddruid.cache.type=local -Ddruid.cache.sizeInBytes=40000000 + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server broker redirect_stderr=true diff --git a/integration-tests/docker/coordinator.conf b/integration-tests/docker/coordinator.conf index afe5a90c65b..41ed94889ac 100644 --- a/integration-tests/docker/coordinator.conf +++ b/integration-tests/docker/coordinator.conf @@ -15,6 +15,19 @@ command=java -Ddruid.metadata.storage.connector.password=diurd -Ddruid.zk.service.host=druid-zookeeper-kafka -Ddruid.coordinator.startDelay=PT5S + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server coordinator redirect_stderr=true diff --git a/integration-tests/docker/historical.conf b/integration-tests/docker/historical.conf index 2038ed0108a..58cbd84e241 100644 --- a/integration-tests/docker/historical.conf +++ b/integration-tests/docker/historical.conf @@ -19,6 +19,19 @@ command=java -Ddruid.server.http.numThreads=100 -Ddruid.segmentCache.locations="[{\"path\":\"/shared/druid/indexCache\",\"maxSize\":5000000000}]" -Ddruid.server.maxSize=5000000000 + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server historical redirect_stderr=true diff --git a/integration-tests/docker/middlemanager.conf b/integration-tests/docker/middlemanager.conf index 092302ef63d..2ca1560fb2c 100644 --- a/integration-tests/docker/middlemanager.conf +++ b/integration-tests/docker/middlemanager.conf @@ -22,6 +22,19 @@ command=java -Ddruid.worker.ip=%(ENV_HOST_IP)s -Ddruid.selectors.indexing.serviceName=druid/overlord -Ddruid.indexer.task.chathandler.type=announce + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server middleManager redirect_stderr=true diff --git a/integration-tests/docker/overlord.conf b/integration-tests/docker/overlord.conf index 8577bf1fe60..1b15400170f 100644 --- a/integration-tests/docker/overlord.conf +++ b/integration-tests/docker/overlord.conf @@ -17,6 +17,19 @@ command=java -Ddruid.indexer.storage.type=metadata -Ddruid.indexer.logs.directory=/shared/tasklogs -Ddruid.indexer.runner.type=remote + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server overlord redirect_stderr=true diff --git a/integration-tests/docker/router.conf b/integration-tests/docker/router.conf index 0336ba15434..3222c18db36 100644 --- a/integration-tests/docker/router.conf +++ b/integration-tests/docker/router.conf @@ -10,6 +10,19 @@ command=java -Ddruid.host=%(ENV_HOST_IP)s -Ddruid.zk.service.host=druid-zookeeper-kafka -Ddruid.server.http.numThreads=100 + -Ddruid.lookup.numLookupLoadingThreads=1 + -Ddruid.auth.authenticatorChain="[\"basic\"]" + -Ddruid.auth.authenticator.basic.type=basic + -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 + -Ddruid.escalator.authorizerName=basic + -Ddruid.auth.authorizers="[\"basic\"]" + -Ddruid.auth.authorizer.basic.type=basic -cp /shared/docker/lib/* io.druid.cli.Main server router redirect_stderr=true diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 29df7a4d7f0..7d3f72ce812 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -81,6 +81,11 @@ + + io.druid.extensions + druid-basic-security + ${project.parent.version} + io.druid druid-services diff --git a/integration-tests/run_cluster.sh b/integration-tests/run_cluster.sh index eb261447175..b824abf94d4 100755 --- a/integration-tests/run_cluster.sh +++ b/integration-tests/run_cluster.sh @@ -40,7 +40,7 @@ docker run -d --privileged --name druid-overlord -p 8090:8090 -v $SHARED_DIR:/sh docker run -d --privileged --name druid-coordinator -p 8081:8081 -v $SHARED_DIR:/shared -v $DOCKERDIR/coordinator.conf:$SUPERVISORDIR/coordinator.conf --link druid-overlord:druid-overlord --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster # Start Historical -docker run -d --privileged --name druid-historical -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster +docker run -d --privileged --name druid-historical -p 8083:8083 -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster # Start Middlemanger docker run -d --privileged --name druid-middlemanager -p 8091:8091 -p 8100:8100 -p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -v $RESOURCEDIR:/resources -v $SHARED_DIR:/shared -v $DOCKERDIR/middlemanager.conf:$SUPERVISORDIR/middlemanager.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-overlord:druid-overlord druid/cluster diff --git a/integration-tests/src/main/java/io/druid/testing/ConfigFileConfigProvider.java b/integration-tests/src/main/java/io/druid/testing/ConfigFileConfigProvider.java index fdad0860fe9..afa8a4da558 100644 --- a/integration-tests/src/main/java/io/druid/testing/ConfigFileConfigProvider.java +++ b/integration-tests/src/main/java/io/druid/testing/ConfigFileConfigProvider.java @@ -138,6 +138,12 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide return brokerUrl; } + @Override + public String getHistoricalUrl() + { + return historicalUrl; + } + @Override public String getMiddleManagerHost() { diff --git a/integration-tests/src/main/java/io/druid/testing/DockerConfigProvider.java b/integration-tests/src/main/java/io/druid/testing/DockerConfigProvider.java index 8cea6bc9205..3763039fd6f 100644 --- a/integration-tests/src/main/java/io/druid/testing/DockerConfigProvider.java +++ b/integration-tests/src/main/java/io/druid/testing/DockerConfigProvider.java @@ -64,6 +64,12 @@ public class DockerConfigProvider implements IntegrationTestingConfigProvider return "http://" + dockerIp + ":8082"; } + @Override + public String getHistoricalUrl() + { + return "http://" + dockerIp + ":8083"; + } + @Override public String getMiddleManagerHost() { diff --git a/integration-tests/src/main/java/io/druid/testing/IntegrationTestingConfig.java b/integration-tests/src/main/java/io/druid/testing/IntegrationTestingConfig.java index 70384bd1066..dc33381b91e 100644 --- a/integration-tests/src/main/java/io/druid/testing/IntegrationTestingConfig.java +++ b/integration-tests/src/main/java/io/druid/testing/IntegrationTestingConfig.java @@ -31,6 +31,8 @@ public interface IntegrationTestingConfig String getBrokerUrl(); + String getHistoricalUrl(); + String getMiddleManagerHost(); String getZookeeperHosts(); diff --git a/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java b/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java index 2dfb9460c7e..b2cb8b00c13 100644 --- a/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java +++ b/integration-tests/src/main/java/io/druid/testing/guice/DruidTestModule.java @@ -72,7 +72,7 @@ public class DruidTestModule implements Module if (config.getUsername() != null) { return new CredentialedHttpClient(new BasicCredentials(config.getUsername(), config.getPassword()), delegate); } else { - return delegate; + return new CredentialedHttpClient(new BasicCredentials("admin", "priest"), delegate); } } diff --git a/integration-tests/src/main/java/org/testng/DruidTestRunnerFactory.java b/integration-tests/src/main/java/org/testng/DruidTestRunnerFactory.java index 8b7f38757df..bece170369d 100644 --- a/integration-tests/src/main/java/org/testng/DruidTestRunnerFactory.java +++ b/integration-tests/src/main/java/org/testng/DruidTestRunnerFactory.java @@ -122,7 +122,7 @@ public class DruidTestRunnerFactory implements ITestRunnerFactory () -> { try { StatusResponseHolder response = client.go( - new Request(HttpMethod.GET, new URL(StringUtils.format("%s/status", host))), + new Request(HttpMethod.GET, new URL(StringUtils.format("%s/status/health", host))), handler ).get(); 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 new file mode 100644 index 00000000000..d03ce64b346 --- /dev/null +++ b/integration-tests/src/test/java/io/druid/tests/security/ITBasicAuthConfigurationTest.java @@ -0,0 +1,290 @@ +/* + * 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.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; +import com.google.inject.Inject; +import com.metamx.http.client.CredentialedHttpClient; +import com.metamx.http.client.HttpClient; +import com.metamx.http.client.Request; +import com.metamx.http.client.auth.BasicCredentials; +import com.metamx.http.client.response.StatusResponseHandler; +import com.metamx.http.client.response.StatusResponseHolder; +import io.druid.guice.annotations.Client; +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; +import io.druid.server.security.ResourceType; +import io.druid.testing.IntegrationTestingConfig; +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; + +import javax.ws.rs.core.MediaType; +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; + + @Inject + ObjectMapper jsonMapper; + + @Inject + @Client + HttpClient httpClient; + + StatusResponseHandler responseHandler = new StatusResponseHandler(Charsets.UTF_8); + + @Test + public void testAuthConfiguration() throws Exception + { + HttpClient adminClient = new CredentialedHttpClient( + new BasicCredentials("admin", "priest"), + httpClient + ); + + HttpClient internalSystemClient = new CredentialedHttpClient( + new BasicCredentials("druid_system", "warlock"), + httpClient + ); + + HttpClient newUserClient = new CredentialedHttpClient( + new BasicCredentials("druid", "helloworld"), + httpClient + ); + + // check that admin works + checkNodeAccess(adminClient); + + // check that internal user works + checkNodeAccess(internalSystemClient); + + // create a new user that can read /status + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid", + null + ); + + makeRequest( + adminClient, + HttpMethod.POST, + 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/db/basic/users/druid", + null + ); + + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/roles/druidrole", + null + ); + + makeRequest( + adminClient, + HttpMethod.POST, + config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid/roles/druidrole", + null + ); + + List permissions = Arrays.asList( + new ResourceAction( + new Resource(".*", ResourceType.STATE), + Action.READ + ) + ); + byte[] permissionsBytes = jsonMapper.writeValueAsBytes(permissions); + makeRequest( + adminClient, + HttpMethod.POST, + 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) + { + makeRequest(httpClient, HttpMethod.GET, config.getCoordinatorUrl() + "/status", null); + makeRequest(httpClient, HttpMethod.GET, config.getIndexerUrl() + "/status", null); + makeRequest(httpClient, HttpMethod.GET, config.getBrokerUrl() + "/status", null); + makeRequest(httpClient, HttpMethod.GET, config.getHistoricalUrl() + "/status", null); + 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 { + Request request = new Request(method, new URL(url)); + if (content != null) { + request.setContent(MediaType.APPLICATION_JSON, content); + } + int retryCount = 0; + + StatusResponseHolder response; + + while (true) { + response = httpClient.go( + request, + responseHandler + ).get(); + + if (!response.getStatus().equals(HttpResponseStatus.OK)) { + String errMsg = StringUtils.format( + "Error while making request to url[%s] status[%s] content[%s]", + url, + response.getStatus(), + response.getContent() + ); + // it can take time for the auth config to propagate, so we retry + if (retryCount > 4) { + throw new ISE(errMsg); + } else { + LOG.error(errMsg); + LOG.error("retrying in 3000ms, retryCount: " + retryCount); + retryCount++; + Thread.sleep(3000); + } + } else { + break; + } + } + return response; + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/pom.xml b/pom.xml index 00860684b67..94ee5243697 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,6 @@ Need to update Druid to use Jackson 2.6+ --> 1.10.77 2.5.5 - 3.4.10 @@ -121,6 +120,7 @@ extensions-core/lookups-cached-single extensions-core/s3-extensions extensions-core/simple-client-sslcontext + extensions-core/druid-basic-security extensions-contrib/azure-extensions extensions-contrib/cassandra-storage diff --git a/server/src/main/java/io/druid/discovery/DruidLeaderClient.java b/server/src/main/java/io/druid/discovery/DruidLeaderClient.java index 9b5b201fe39..3955f9329b8 100644 --- a/server/src/main/java/io/druid/discovery/DruidLeaderClient.java +++ b/server/src/main/java/io/druid/discovery/DruidLeaderClient.java @@ -26,6 +26,7 @@ import com.metamx.http.client.HttpClient; import com.metamx.http.client.Request; import com.metamx.http.client.response.FullResponseHandler; import com.metamx.http.client.response.FullResponseHolder; +import com.metamx.http.client.response.HttpResponseHandler; import io.druid.client.selector.Server; import io.druid.concurrent.LifecycleLock; import io.druid.curator.discovery.ServerDiscoverySelector; @@ -127,10 +128,18 @@ public class DruidLeaderClient return new Request(httpMethod, new URL(StringUtils.format("%s%s", getCurrentKnownLeader(true), urlPath))); } + public FullResponseHolder go(Request request) throws IOException, InterruptedException + { + return go(request, new FullResponseHandler(Charsets.UTF_8)); + } + /** * Executes a Request object aimed at the leader. Throws IOException if the leader cannot be located. */ - public FullResponseHolder go(Request request) throws IOException, InterruptedException + public FullResponseHolder go( + Request request, + HttpResponseHandler responseHandler + ) throws IOException, InterruptedException { Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS)); for (int counter = 0; counter < MAX_RETRIES; counter++) { @@ -139,7 +148,7 @@ public class DruidLeaderClient try { try { - fullResponseHolder = httpClient.go(request, new FullResponseHandler(Charsets.UTF_8)).get(); + fullResponseHolder = httpClient.go(request, responseHandler).get(); } catch (ExecutionException e) { // Unwrap IOExceptions and ChannelExceptions, re-throw others diff --git a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java index d30231de3d5..e648e928f6e 100644 --- a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java +++ b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java @@ -44,6 +44,8 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLRecoverableException; import java.sql.SQLTransientException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.Callable; @@ -446,6 +448,78 @@ public abstract class SQLMetadataConnector implements MetadataStorageConnector ); } + @Override + public boolean compareAndSwap( + List updates + ) throws Exception + { + return getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Boolean inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + List currentValues = new ArrayList(); + + // Compare + for (MetadataCASUpdate update : updates) { + byte[] currentValue = handle + .createQuery( + StringUtils.format( + "SELECT %1$s FROM %2$s WHERE %3$s = :key", + update.getValueColumn(), + update.getTableName(), + update.getKeyColumn() + ) + ) + .bind("key", update.getKey()) + .map(ByteArrayMapper.FIRST) + .first(); + + if (!Arrays.equals(currentValue, update.getOldValue())) { + return false; + } + currentValues.add(currentValue); + } + + // Swap + for (int i = 0; i < updates.size(); i++) { + MetadataCASUpdate update = updates.get(i); + byte[] currentValue = currentValues.get(i); + + if (currentValue == null) { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (%2$s, %3$s) VALUES (:key, :value)", + update.getTableName(), + update.getKeyColumn(), + update.getValueColumn() + ) + ) + .bind("key", update.getKey()) + .bind("value", update.getNewValue()) + .execute(); + } else { + handle.createStatement( + StringUtils.format( + "UPDATE %1$s SET %3$s=:value WHERE %2$s=:key", + update.getTableName(), + update.getKeyColumn(), + update.getValueColumn() + ) + ) + .bind("key", update.getKey()) + .bind("value", update.getNewValue()) + .execute(); + } + } + + return true; + } + } + ); + } + public abstract DBI getDBI(); @Override diff --git a/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java b/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java index 852b7e4a310..5760d344e0c 100644 --- a/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java +++ b/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java @@ -37,6 +37,11 @@ public class AuthenticatorMapper this.authenticatorMap = authenticatorMap; } + public Map getAuthenticatorMap() + { + return authenticatorMap; + } + public List getAuthenticatorChain() { return Lists.newArrayList(authenticatorMap.values()); diff --git a/server/src/main/java/io/druid/server/security/AuthorizerMapper.java b/server/src/main/java/io/druid/server/security/AuthorizerMapper.java index 2c029aafe03..9a2052ca6f0 100644 --- a/server/src/main/java/io/druid/server/security/AuthorizerMapper.java +++ b/server/src/main/java/io/druid/server/security/AuthorizerMapper.java @@ -39,4 +39,9 @@ public class AuthorizerMapper { return authorizerMap.get(name); } + + public Map getAuthorizerMap() + { + return authorizerMap; + } } diff --git a/server/src/main/java/io/druid/server/security/ResourceAction.java b/server/src/main/java/io/druid/server/security/ResourceAction.java index 240f9280562..3a7641b4415 100644 --- a/server/src/main/java/io/druid/server/security/ResourceAction.java +++ b/server/src/main/java/io/druid/server/security/ResourceAction.java @@ -21,6 +21,7 @@ package io.druid.server.security; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.druid.java.util.common.StringUtils; public class ResourceAction { @@ -75,4 +76,10 @@ public class ResourceAction result = 31 * result + getAction().hashCode(); return result; } + + @Override + public String toString() + { + return StringUtils.format("{%s,%s}", resource, action); + } } diff --git a/services/src/main/java/io/druid/cli/CliOverlord.java b/services/src/main/java/io/druid/cli/CliOverlord.java index 7bba5f4e2b2..c958bb59d7a 100644 --- a/services/src/main/java/io/druid/cli/CliOverlord.java +++ b/services/src/main/java/io/druid/cli/CliOverlord.java @@ -348,6 +348,8 @@ public class CliOverlord extends ServerRunnable // Can't use /* here because of Guice and Jetty static content conflicts root.addFilter(GuiceFilter.class, "/druid/*", null); + root.addFilter(GuiceFilter.class, "/druid-ext/*", null); + HandlerList handlerList = new HandlerList(); handlerList.setHandlers( new Handler[]{ diff --git a/services/src/main/java/io/druid/cli/CoordinatorJettyServerInitializer.java b/services/src/main/java/io/druid/cli/CoordinatorJettyServerInitializer.java index 1174bf4b9b9..f47644f1347 100644 --- a/services/src/main/java/io/druid/cli/CoordinatorJettyServerInitializer.java +++ b/services/src/main/java/io/druid/cli/CoordinatorJettyServerInitializer.java @@ -144,6 +144,8 @@ class CoordinatorJettyServerInitializer implements JettyServerInitializer if (beOverlord) { root.addFilter(GuiceFilter.class, "/druid/indexer/*", null); } + root.addFilter(GuiceFilter.class, "/druid-ext/*", null); + // this will be removed in the next major release root.addFilter(GuiceFilter.class, "/coordinator/*", null); diff --git a/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java b/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java index 3ea97097a13..fc00ca644cb 100644 --- a/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java +++ b/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java @@ -109,10 +109,10 @@ public class RouterJettyServerInitializer implements JettyServerInitializer jsonMapper ); - // Can't use '/*' here because of Guice conflicts with AsyncQueryForwardingServlet path root.addFilter(GuiceFilter.class, "/status/*", null); root.addFilter(GuiceFilter.class, "/druid/router/*", null); + root.addFilter(GuiceFilter.class, "/druid-ext/*", null); final HandlerList handlerList = new HandlerList(); handlerList.setHandlers(