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 000000000000..7f081d754875 --- /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/distribution/pom.xml b/distribution/pom.xml index f1dd51b1403b..6b429994e1a5 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 000000000000..17a15c819fc0 --- /dev/null +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -0,0 +1,219 @@ +--- +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. + + +## Configuration + + +### Properties +|Property|Description|Default|required| +|--------|-----------|-------|--------| +|`druid.auth.basic.initialAdminPassword`|Password to assign when Druid automatically creates the default admin account. See [Default user accounts](#default-user-accounts) for more information.|"druid"|No| +|`druid.auth.basic.initialInternalClientPassword`|Password to assign when Druid automatically creates the default admin account. See [Default user accounts](#default-user-accounts) for more information.|"druid"|No| +|`druid.auth.basic.permissionCacheSize`|Resource names are used as regexes in permissions. Compiled regex Pattern objects are cached by the Basic authorizer. This property controls how many cached Pattern objects are stored.|5000|No| + +### Creating an Authenticator +``` +druid.auth.authenticatorChain=["MyBasicAuthenticator"] + +druid.auth.authenticator.MyBasicAuthenticator.type=basic +``` + +To use the Basic authenticator, add an authenticator with type `basic` to the authenticatorChain. The example above uses the name "MyBasicAuthenticator" for the Authenticator. + +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. + +Only one instance of a "basic" type authenticator should be created and used, multiple "basic" authenticator instances are not supported. + +#### Properties +|Property|Description|Default|required| +|--------|-----------|-------|--------| +|`druid.auth.authenticator.MyBasicAuthenticator.internalClientUsername`| Username for the internal system user, used for internal node communication|N/A|Yes| +|`druid.auth.authenticator.MyBasicAuthenticator.internalClientPassword`| Password for the internal system user, used for internal node communication|N/A|Yes| +|`druid.auth.authenticator.MyBasicAuthenticator.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. The example above uses the name "MyBasicAuthorizer" for the Authorizer. + +Configuration of the named authenticator is assigned through properties with the form: + +``` +druid.auth.authorizer.. +``` + +The Basic authorizer has no additional configuration properties at this time. + +Only one instance of a "basic" type authorizer should be created and used, multiple "basic" authorizer instances are not supported. + + +## Usage + + +### Coordinator Security API +To use these APIs, a user needs read/write permissions for the CONFIG resource type with name "security". + +Root path: `/druid/coordinator/v1/security` + +#### User Management +`GET(/users)` +Return a list of all user names. + +`GET(/users/{userName})` +Return the name, roles, permissions of the user named {userName} + +`POST(/users/{userName})` +Create a new user with name {userName} + +`DELETE(/users/{userName})` +Delete the user with name {userName} + + +#### User Credentials +`GET(/credentials/{userName})` +Return the salt/hash/iterations info used for HTTP basic authentication for {userName} + +`POST(/credentials/{userName})` +Assign a password used for HTTP basic authentication for {userName} +Content: password string + + +#### Role Creation/Deletion +`GET(/roles)` +Return a list of all role names. + +`GET(/roles/{roleName})` +Return name and permissions for the role named {roleName} + +`POST(/roles/{roleName})` +Create a new role with name {roleName}. +Content: username string + +`DELETE(/roles/{roleName})` +Delete the role with name {roleName}. + + +#### Role Assignment +`POST(/users/{userName}/roles/{roleName})` +Assign role {roleName} to user {userName}. + +`DELETE(/users/{userName}/roles/{roleName})` +Unassign role {roleName} from user {userName} + + +#### Permissions +`POST(/roles/{roleName}/permissions)` +Create a new permissions and assign them to role named {roleName}. +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. + +`DELETE(/permissions/{permId})` +Delete the permission with ID {permId}. Permission IDs are available from the output of individual user/role GET endpoints. + +## Default user accounts + +By default, an administrator account with full privileges is created with credentials `admin/druid`. The password assigned at account creation can be overridden by setting the `druid.auth.basic.initialAdminPassword` property. + +A default internal system user account with full privileges, meant for internal communications between Druid services, is also created with credentials `druid_system/druid`. The password assigned at account creation can be overridden by setting the `druid.auth.basic.initialInternalClientPassword` property. + +The values for `druid.authenticator..internalClientUsername` and `druid.authenticator..internalClientPassword` must match the credentials of the internal system user account. + +Cluster administrators should change the default passwords for these accounts before exposing a cluster to users. + +## 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| diff --git a/extensions-core/druid-basic-security/pom.xml b/extensions-core/druid-basic-security/pom.xml new file mode 100644 index 000000000000..75f8c4de1201 --- /dev/null +++ b/extensions-core/druid-basic-security/pom.xml @@ -0,0 +1,78 @@ + + + + + + 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 + + + mysql + mysql-connector-java + ${mysql.version} + + + org.postgresql + postgresql + ${postgresql.version} + + + org.jdbi + jdbi + provided + + + + + junit + junit + test + + + org.easymock + easymock + test + + + \ No newline at end of file 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 000000000000..952b2743e6b0 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java @@ -0,0 +1,103 @@ +/* + * 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; +import io.druid.java.util.common.logger.Logger; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.servlet.http.HttpServletRequest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +public class BasicAuthUtils +{ + private static final Logger log = new Logger(BasicAuthUtils.class); + private static final Base64.Encoder ENCODER = Base64.getEncoder(); + private static final Base64.Decoder DECODER = Base64.getDecoder(); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + + public static int SALT_LENGTH = 32; + public static int KEY_ITERATIONS = 10000; + public static int KEY_LENGTH = 512; + public static String ALGORITHM = "PBKDF2WithHmacSHA512"; + + public static String getEncodedCredentials(final String unencodedCreds) + { + return ENCODER.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(ikse); + } + catch (NoSuchAlgorithmException nsae) { + log.error("%s not supported on this system.", ALGORITHM); + throw new RuntimeException(nsae); + } + } + + public static byte[] generateSalt() + { + byte salt[] = new byte[SALT_LENGTH]; + SECURE_RANDOM.nextBytes(salt); + return salt; + } + + public static String getBasicUserSecretFromHttpReq(HttpServletRequest httpReq) + { + try { + String authHeader = httpReq.getHeader("Authorization"); + + if (authHeader == null) { + return null; + } + + if (!authHeader.substring(0, 6).equals("Basic ")) { + return null; + } + + String encodedUserSecret = authHeader.substring(6); + return StringUtils.fromUtf8(DECODER.decode(encodedUserSecret)); + } + catch (Exception e) { + return null; + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthenticatorResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthenticatorResource.java new file mode 100644 index 000000000000..a4539600e97d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthenticatorResource.java @@ -0,0 +1,260 @@ +/* + * 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.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ResourceFilters; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; +import io.druid.security.basic.db.BasicAuthenticatorStorageConnector; +import io.druid.server.security.Authenticator; +import io.druid.server.security.AuthenticatorMapper; +import org.skife.jdbi.v2.exceptions.CallbackFailedException; + +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; +import java.util.List; +import java.util.Map; + +/** + * Configuration resource for authenticator users and credentials. + */ +@Path("/druid/coordinator/v1/security/authentication") +public class BasicAuthenticatorResource +{ + private final BasicAuthenticatorStorageConnector dbConnector; + private final Map authenticatorMap; + + @Inject + public BasicAuthenticatorResource( + BasicAuthenticatorStorageConnector dbConnector, + AuthenticatorMapper authenticatorMapper + ) + { + this.dbConnector = dbConnector; + + 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); + } + } + } + + /** + * @param req HTTP request + * + * @return List of all users + */ + @GET + @Path("/{authenticatorName}/users") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllUsers( + @Context HttpServletRequest req, + @PathParam("authenticatorName") final String authenticatorName + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + List> users = dbConnector.getAllUsers(authenticator.getDBPrefix()); + return Response.ok(users).build(); + } + + /** + * @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("/{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 + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + Map user = dbConnector.getUser(authenticator.getDBPrefix(), userName); + Map credentials = dbConnector.getUserCredentials(authenticator.getDBPrefix(), userName); + + Map userInfo = Maps.newHashMap(); + userInfo.put("user", user); + if (credentials != null) { + userInfo.put("credentials", credentials); + } + return Response.ok(userInfo).build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + dbConnector.createUser(authenticator.getDBPrefix(), userName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + dbConnector.deleteUser(authenticator.getDBPrefix(), userName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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, + String password + ) + { + final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName); + if (authenticator == null) { + return makeResponseForAuthenticatorNotFound(authenticatorName); + } + + try { + dbConnector.setUserCredentials(authenticator.getDBPrefix(), userName, password.toCharArray()); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + 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 makeResponseForCallbackFailedException(CallbackFailedException cfe) + { + Throwable cause = cfe.getCause(); + if (cause instanceof BasicSecurityDBResourceException) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", cause.getMessage() + )) + .build(); + } else { + throw cfe; + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java new file mode 100644 index 000000000000..0342c5419b0e --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java @@ -0,0 +1,501 @@ +/* + * 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.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ResourceFilters; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +import io.druid.security.basic.db.BasicAuthorizerStorageConnector; +import io.druid.server.security.Authorizer; +import io.druid.server.security.AuthorizerMapper; +import io.druid.server.security.ResourceAction; +import org.skife.jdbi.v2.exceptions.CallbackFailedException; + +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; +import java.util.List; +import java.util.Map; + +/** + * Configuration resource for authorizer users/roles/permissions + */ +@Path("/druid/coordinator/v1/security/authorization") +public class BasicAuthorizerResource +{ + private final BasicAuthorizerStorageConnector dbConnector; + private final Map authorizerMap; + + @Inject + public BasicAuthorizerResource( + BasicAuthorizerStorageConnector dbConnector, + AuthorizerMapper authorizerMapper + ) + { + this.dbConnector = dbConnector; + 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)); + } + } + } + + /** + * @param req HTTP request + * + * @return List of all users + */ + @GET + @Path("/{authorizerName}/users") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllUsers( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + List> users = dbConnector.getAllUsers(authorizer.getDBPrefix()); + return Response.ok(users).build(); + } + + /** + * @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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + Map user = dbConnector.getUser(authorizer.getDBPrefix(), userName); + List> roles = dbConnector.getRolesForUser(authorizer.getDBPrefix(), userName); + List> permissions = dbConnector.getPermissionsForUser(authorizer.getDBPrefix(), userName); + + Map userInfo = ImmutableMap.of( + "user", user, + "roles", roles, + "permissions", permissions + ); + + return Response.ok(userInfo).build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.createUser(authorizer.getDBPrefix(), userName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.deleteUser(authorizer.getDBPrefix(), userName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * @param req HTTP request + * + * @return List of all roles + */ + @GET + @Path("/{authorizerName}/roles") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllRoles( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + List> roles = dbConnector.getAllRoles(authorizer.getDBPrefix()); + return Response.ok(roles).build(); + } + + /** + * 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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + Map role = dbConnector.getRole(authorizer.getDBPrefix(), roleName); + List> users = dbConnector.getUsersWithRole(authorizer.getDBPrefix(), roleName); + List> permissions = dbConnector.getPermissionsForRole(authorizer.getDBPrefix(), roleName); + + Map roleInfo = ImmutableMap.of( + "role", role, + "users", users, + "permissions", permissions + ); + + return Response.ok(roleInfo).build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * Create a new role. + * + * @param req HTTP request + * @param roleName Name of role + * + * @return OK response, 400 error if role already exists + */ + @POST + @Path("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.createRole(authorizer.getDBPrefix(), roleName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * Delete a role. + * + * @param req HTTP request + * @param roleName Name of role + * + * @return OK response, 400 error if role doesn't exist. + */ + @DELETE + @Path("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.deleteRole(authorizer.getDBPrefix(), roleName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.assignRole(authorizer.getDBPrefix(), userName, roleName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * 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("/{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 + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.unassignRole(authorizer.getDBPrefix(), userName, roleName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * Add permissions to a role. + * + * @param req HTTP request + * @param roleName Name of role + * @param resourceActions Permissions to add + * + * @return OK response. 400 error if role doesn't exist. + */ + @POST + @Path("/{authorizerName}/roles/{roleName}/permissions") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response addPermissionsToRole( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("roleName") String roleName, + List resourceActions + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + for (ResourceAction resourceAction : resourceActions) { + dbConnector.addPermission(authorizer.getDBPrefix(), roleName, resourceAction); + } + + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + /** + * Delete a permission. + * + * @param req HTTP request + * @param permId ID of permission to delete + * + * @return OK response. 400 error if permission doesn't exist. + */ + @DELETE + @Path("/{authorizerName}/permissions/{permId}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deletePermission( + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName, + @PathParam("permId") Integer permId + ) + { + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + try { + dbConnector.deletePermission(authorizer.getDBPrefix(), permId); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } + } + + 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 makeResponseForCallbackFailedException(CallbackFailedException cfe) + { + Throwable cause = cfe.getCause(); + if (cause instanceof BasicSecurityDBResourceException) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", cause.getMessage() + )) + .build(); + } else { + throw cfe; + } + } +} 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 000000000000..6b95308739d5 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.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; + +import io.druid.java.util.common.StringUtils; + +/** + * Throw this in the BasicSecurityStorageConnectors for invalid resource accesses 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)); + } +} 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 000000000000..484d56e5b5a7 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java @@ -0,0 +1,145 @@ +/* + * 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.Key; +import io.druid.guice.Jerseys; +import io.druid.guice.ManageLifecycle; +import io.druid.guice.PolyBind; +import io.druid.initialization.DruidModule; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +import io.druid.security.basic.db.BasicAuthenticatorStorageConnector; +import io.druid.security.basic.db.BasicAuthorizerStorageConnector; +import io.druid.security.basic.db.SQLBasicAuthenticatorStorageConnector; +import io.druid.security.basic.db.SQLBasicAuthorizerStorageConnector; +import io.druid.security.basic.db.derby.DerbySQLBasicAuthenticatorStorageConnector; +import io.druid.security.basic.db.derby.DerbySQLBasicAuthorizerStorageConnector; +import io.druid.security.basic.db.mysql.MySQLBasicAuthenticatorStorageConnector; +import io.druid.security.basic.db.mysql.MySQLBasicAuthorizerStorageConnector; +import io.druid.security.basic.db.postgres.PostgreSQLBasicAuthenticatorStorageConnector; +import io.druid.security.basic.db.postgres.PostgreSQLBasicAuthorizerStorageConnector; + +import java.util.List; + +public class BasicSecurityDruidModule implements DruidModule +{ + public final String STORAGE_CONNECTOR_TYPE_PROPERTY = "druid.metadata.storage.type"; + + @Override + public void configure(Binder binder) + { + // authentication + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(BasicAuthenticatorStorageConnector.class), null, "derby" + ); + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(SQLBasicAuthenticatorStorageConnector.class), null, "derby" + ); + + PolyBind.optionBinder(binder, Key.get(BasicAuthenticatorStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthenticatorStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicAuthenticatorStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthenticatorStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicAuthenticatorStorageConnector.class)) + .addBinding("postgresql") + .to(PostgreSQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthenticatorStorageConnector.class)) + .addBinding("postgresql") + .to(PostgreSQLBasicAuthenticatorStorageConnector.class) + .in(ManageLifecycle.class); + + Jerseys.addResource(binder, BasicAuthenticatorResource.class); + + + // authorization + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(BasicAuthorizerStorageConnector.class), null, "derby" + ); + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(SQLBasicAuthorizerStorageConnector.class), null, "derby" + ); + + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) + .addBinding("postgresql") + .to(PostgreSQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) + .addBinding("postgresql") + .to(PostgreSQLBasicAuthorizerStorageConnector.class) + .in(ManageLifecycle.class); + + Jerseys.addResource(binder, BasicAuthorizerResource.class); + } + + @Override + public List getJacksonModules() + { + return ImmutableList.of( + new SimpleModule("BasicDruidSecurity").registerSubtypes( + BasicHTTPAuthenticator.class, + BasicRoleBasedAuthorizer.class + ) + ); + } +} 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 000000000000..adb3b0cf8f4e --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -0,0 +1,87 @@ +/* + * 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 applicablePaths = ImmutableList.of( + "druid/coordinator/v1/security/*" + ); + + @Inject + public BasicSecurityResourceFilter( + AuthorizerMapper authorizerMapper + ) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + final ResourceAction resourceAction = new ResourceAction( + new Resource("security", 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 : applicablePaths) { + 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/authentication/BasicHTTPAuthenticator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java new file mode 100644 index 000000000000..6cab70cf60b8 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java @@ -0,0 +1,274 @@ +/* + * 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.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.security.basic.db.BasicAuthDBConfig; +import io.druid.security.basic.db.BasicAuthenticatorStorageConnector; +import io.druid.server.security.AuthConfig; +import io.druid.server.security.AuthenticationResult; +import io.druid.server.security.Authenticator; +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 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.net.URI; +import java.util.EnumSet; +import java.util.Map; + +@JsonTypeName("basic") +public class BasicHTTPAuthenticator implements Authenticator +{ + private final BasicAuthenticatorStorageConnector dbConnector; + private final String internalClientUsername; + private final String internalClientPassword; + private final String authorizerName; + private final BasicAuthDBConfig dbConfig; + + @JsonCreator + public BasicHTTPAuthenticator( + @JacksonInject BasicAuthenticatorStorageConnector dbConnector, + @JsonProperty("dbPrefix") String dbPrefix, + @JsonProperty("initialAdminPassword") String initialAdminPassword, + @JsonProperty("initialInternalClientPassword") String initialInternalClientPassword, + @JsonProperty("internalClientUsername") String internalClientUsername, + @JsonProperty("internalClientPassword") String internalClientPassword, + @JsonProperty("authorizerName") String authorizerName + ) + { + this.internalClientUsername = internalClientUsername; + this.internalClientPassword = internalClientPassword; + this.authorizerName = authorizerName; + this.dbConfig = new BasicAuthDBConfig(dbPrefix, initialAdminPassword, initialInternalClientPassword); + this.dbConnector = dbConnector; + } + + /** + * constructor for unit tests + */ + public BasicHTTPAuthenticator( + BasicAuthenticatorStorageConnector dbConnector, + String dbPrefix, + String internalClientUsername, + String internalClientPassword, + String authorizerName + ) + { + this.dbConnector = dbConnector; + this.internalClientUsername = internalClientUsername; + this.internalClientPassword = internalClientPassword; + this.authorizerName = authorizerName; + this.dbConfig = new BasicAuthDBConfig(dbPrefix, null, null); + } + + @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 (dbConnector.checkCredentials(dbConfig.getDbPrefix(), user, password.toCharArray())) { + return new AuthenticationResult(user, authorizerName, null); + } else { + return null; + } + } + + @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); + } + + @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 BasicAuthenticatorStorageConnector getDbConnector() + { + return dbConnector; + } + + public BasicAuthDBConfig getDbConfig() + { + return dbConfig; + } + + public String getDBPrefix() + { + return dbConfig.getDbPrefix(); + } + + 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 (dbConnector.checkCredentials(dbConfig.getDbPrefix(), user, password)) { + AuthenticationResult authenticationResult = new AuthenticationResult(user, authorizerName, null); + servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult); + filterChain.doFilter(servletRequest, servletResponse); + } else { + httpResp.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + @Override + public void destroy() + { + + } + } +} 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 000000000000..7845555eedb3 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.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.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 com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import io.druid.java.util.common.IAE; +import io.druid.security.basic.db.BasicAuthDBConfig; +import io.druid.security.basic.db.BasicAuthorizerStorageConnector; +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 io.druid.server.security.ResourceAction; + +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@JsonTypeName("basic") +public class BasicRoleBasedAuthorizer implements Authorizer +{ + private static final int DEFAULT_PERMISSION_CACHE_SIZE = 5000; + private final BasicAuthorizerStorageConnector dbConnector; + private final LoadingCache permissionPatternCache; + private final int permissionCacheSize; + private final BasicAuthDBConfig dbConfig; + + @JsonCreator + public BasicRoleBasedAuthorizer( + @JacksonInject BasicAuthorizerStorageConnector dbConnector, + @JsonProperty("dbPrefix") String dbPrefix, + @JsonProperty("permissionCacheSize") Integer permissionCacheSize + ) + { + this.dbConfig = new BasicAuthDBConfig(dbPrefix, null, null); + this.dbConnector = dbConnector; + this.permissionCacheSize = permissionCacheSize == null ? DEFAULT_PERMISSION_CACHE_SIZE : permissionCacheSize; + this.permissionPatternCache = Caffeine.newBuilder() + .maximumSize(this.permissionCacheSize) + .build(regexStr -> Pattern.compile(regexStr)); + } + + /** + * constructor for unit tests + */ + public BasicRoleBasedAuthorizer( + BasicAuthorizerStorageConnector dbConnector, + String dbPrefix, + int permissionCacheSize + ) + { + this.dbConnector = dbConnector; + this.permissionCacheSize = permissionCacheSize; + this.permissionPatternCache = Caffeine.newBuilder() + .maximumSize(this.permissionCacheSize) + .build(regexStr -> Pattern.compile(regexStr)); + this.dbConfig = new BasicAuthDBConfig(dbPrefix, null, null); + } + + @Override + public Access authorize( + AuthenticationResult authenticationResult, Resource resource, Action action + ) + { + if (authenticationResult == null) { + throw new IAE("WTF? authenticationResult should never be null."); + } + + List> permissions = dbConnector.getPermissionsForUser(dbConfig.getDbPrefix(), authenticationResult.getIdentity()); + + for (Map permission : permissions) { + if (permissionCheck(resource, action, permission)) { + return new Access(true); + } + } + + return new Access(false); + } + + public BasicAuthorizerStorageConnector getDbConnector() + { + return dbConnector; + } + + public String getDBPrefix() + { + return dbConfig.getDbPrefix(); + } + + private boolean permissionCheck(Resource resource, Action action, Map permission) + { + ResourceAction permissionResourceAction = (ResourceAction) permission.get("resourceAction"); + if (action != permissionResourceAction.getAction()) { + return false; + } + + Resource permissionResource = permissionResourceAction.getResource(); + if (permissionResource.getType() != resource.getType()) { + return false; + } + + String permissionResourceName = permissionResource.getName(); + + Pattern resourceNamePattern = permissionPatternCache.get(permissionResourceName); + Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName()); + return resourceNameMatcher.matches(); + } + + public int getPermissionCacheSize() + { + return permissionCacheSize; + } + + public BasicAuthDBConfig getDbConfig() + { + return dbConfig; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.java new file mode 100644 index 000000000000..81a96e814c3b --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.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.db; + +public class BasicAuthDBConfig +{ + private final String dbPrefix; + private final String initialAdminPassword; + private final String initialInternalClientPassword; + + public BasicAuthDBConfig( + final String dbPrefix, + final String initialAdminPassword, + final String initialInternalClientPassword + ) + { + this.dbPrefix = dbPrefix; + this.initialAdminPassword = initialAdminPassword; + this.initialInternalClientPassword = initialInternalClientPassword; + } + + public String getDbPrefix() + { + return dbPrefix; + } + + public String getInitialAdminPassword() + { + return initialAdminPassword; + } + + public String getInitialInternalClientPassword() + { + return initialInternalClientPassword; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..bd685437edac --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java @@ -0,0 +1,44 @@ +/* + * 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.db; + +import java.util.List; +import java.util.Map; + +public interface BasicAuthenticatorStorageConnector +{ + void createUser(String dbPrefix, String userName); + + void deleteUser(String dbPrefix, String userName); + + List> getAllUsers(String dbPrefix); + + Map getUser(String dbPrefix, String userName); + + void setUserCredentials(String dbPrefix, String userName, char[] password); + + boolean checkCredentials(String dbPrefix, String userName, char[] password); + + Map getUserCredentials(String dbPrefix, String userName); + + void createUserTable(String dbPrefix); + + void createUserCredentialsTable(String dbPrefix); +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthorizerStorageConnector.java new file mode 100644 index 000000000000..bda353f0e50d --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthorizerStorageConnector.java @@ -0,0 +1,68 @@ +/* + * 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.db; + +import io.druid.server.security.ResourceAction; + +import java.util.List; +import java.util.Map; + +public interface BasicAuthorizerStorageConnector +{ + void createUser(String dbPrefix, String userName); + + void deleteUser(String dbPrefix, String userName); + + void createRole(String dbPrefix, String roleName); + + void deleteRole(String dbPrefix, String roleName); + + void addPermission(String dbPrefix, String roleName, ResourceAction resourceAction); + + void deletePermission(String dbPrefix, int permissionId); + + void assignRole(String dbPrefix, String userName, String roleName); + + void unassignRole(String dbPrefix, String userName, String roleName); + + List> getAllUsers(String dbPrefix); + + List> getAllRoles(String dbPrefix); + + Map getUser(String dbPrefix, String userName); + + Map getRole(String dbPrefix, String roleName); + + List> getRolesForUser(String dbPrefix, String userName); + + List> getUsersWithRole(String dbPrefix, String roleName); + + List> getPermissionsForRole(String dbPrefix, String roleName); + + List> getPermissionsForUser(String dbPrefix, String userName); + + void createRoleTable(String dbPrefix); + + void createUserTable(String dbPrefix); + + void createPermissionTable(String dbPrefix); + + void createUserRoleTable(String dbPrefix); +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..c8d3dbc2eb51 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthenticatorStorageConnector.java @@ -0,0 +1,493 @@ +/* + * 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.db; + +import com.google.common.base.Predicate; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.metadata.BaseSQLMetadataConnector; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.BasicAuthUtils; +import io.druid.security.basic.BasicSecurityDBResourceException; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; +import io.druid.server.security.Authenticator; +import io.druid.server.security.AuthenticatorMapper; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.TransactionCallback; +import org.skife.jdbi.v2.TransactionStatus; +import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.skife.jdbi.v2.util.IntegerMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public abstract class SQLBasicAuthenticatorStorageConnector + extends BaseSQLMetadataConnector + implements BasicAuthenticatorStorageConnector +{ + public static final String USERS = "users"; + public static final String USER_CREDENTIALS = "user_credentials"; + public static final List TABLE_NAMES = Lists.newArrayList( + USER_CREDENTIALS, + USERS + ); + + private static final String DEFAULT_ADMIN_NAME = "admin"; + private static final String DEFAULT_SYSTEM_USER_NAME = "druid_system"; + + private final Supplier config; + private final UserCredentialsMapper credsMapper; + private final Injector injector; + + @Inject + public SQLBasicAuthenticatorStorageConnector( + Supplier config, + Injector injector + ) + { + this.config = config; + this.injector = injector; + this.credsMapper = new UserCredentialsMapper(); + this.shouldRetry = new Predicate() + { + @Override + public boolean apply(Throwable e) + { + return isTransientException(e); + } + }; + } + + @LifecycleStart + public void start() + { + AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class); + + for (Map.Entry entry : authenticatorMapper.getAuthenticatorMap().entrySet()) { + Authenticator authenticator = entry.getValue(); + if (authenticator instanceof BasicHTTPAuthenticator) { + String authenticatorName = entry.getKey(); + BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator; + BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig(); + + retryTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + if (tableExists(handle, getPrefixedTableName(dbConfig.getDbPrefix(), USERS))) { + return null; + } + + createUserTable(dbConfig.getDbPrefix()); + createUserCredentialsTable(dbConfig.getDbPrefix()); + + makeDefaultSuperuser( + dbConfig.getDbPrefix(), + DEFAULT_ADMIN_NAME, + dbConfig.getInitialAdminPassword() + ); + + makeDefaultSuperuser( + dbConfig.getDbPrefix(), + DEFAULT_SYSTEM_USER_NAME, + dbConfig.getInitialInternalClientPassword() + ); + + return null; + } + }, + 3, + 10 + ); + } + } + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createUserCredentialsTable(String dbPrefix) + { + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + credentialsTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name VARCHAR(255) NOT NULL, \n" + + " salt VARBINARY(32) NOT NULL, \n" + + " hash VARBINARY(64) NOT NULL, \n" + + " iterations INTEGER NOT NULL, \n" + + " PRIMARY KEY (user_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + credentialsTableName, + userTableName + ) + ) + ); + } + + @Override + public void createUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getUserCount(handle, dbPrefix, userName); + if (count != 0) { + throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); + } + + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (name) VALUES (:user_name)", userTableName + ) + ) + .bind("user_name", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public void deleteUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getUserCount(handle, dbPrefix, userName); + if (count == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE name = :userName", userTableName + ) + ) + .bind("userName", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public List> getAllUsers(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s", userTableName) + ) + .list(); + } + } + ); + } + + @Override + public Map getUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s where name = :userName", userTableName) + ) + .bind("userName", userName) + .first(); + } + } + ); + } + + @Override + public Map getUserCredentials(String dbPrefix, String userName) + { + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s where user_name = :userName", credentialsTableName) + ) + .map(credsMapper) + .bind("userName", userName) + .first(); + } + } + ); + } + + @Override + public void setUserCredentials(String dbPrefix, String userName, char[] password) + { + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + Map existingMapping = handle + .createQuery( + StringUtils.format( + "SELECT user_name FROM %1$s WHERE user_name = :userName", + credentialsTableName + ) + ) + .bind("userName", userName) + .first(); + + int iterations = BasicAuthUtils.KEY_ITERATIONS; + byte[] salt = BasicAuthUtils.generateSalt(); + byte[] hash = BasicAuthUtils.hashPassword(password, salt, iterations); + + if (existingMapping == null) { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (user_name, salt, hash, iterations) " + + "VALUES (:userName, :salt, :hash, :iterations)", + credentialsTableName + ) + ) + .bind("userName", userName) + .bind("salt", salt) + .bind("hash", hash) + .bind("iterations", iterations) + .execute(); + } else { + handle.createStatement( + StringUtils.format( + "UPDATE %1$s SET " + + "salt = :salt, " + + "hash = :hash, " + + "iterations = :iterations " + + "WHERE user_name = :userName", + credentialsTableName + ) + ) + .bind("userName", userName) + .bind("salt", salt) + .bind("hash", hash) + .bind("iterations", iterations) + .execute(); + } + + return null; + } + } + ); + } + + @Override + public boolean checkCredentials(String dbPrefix, String userName, char[] password) + { + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + return getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Boolean inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + Map credentials = handle + .createQuery( + StringUtils.format( + "SELECT * FROM %1$s WHERE user_name = :userName", + credentialsTableName + ) + ) + .bind("userName", userName) + .map(credsMapper) + .first(); + + if (credentials == null) { + return false; + } + + byte[] dbSalt = (byte[]) credentials.get("salt"); + byte[] dbHash = (byte[]) credentials.get("hash"); + int iterations = (int) credentials.get("iterations"); + + byte[] hash = BasicAuthUtils.hashPassword(password, dbSalt, iterations); + + return Arrays.equals(dbHash, hash); + } + } + ); + } + + public List getTableNamesForPrefix(String dbPrefix) + { + return ImmutableList.of( + getPrefixedTableName(dbPrefix, USER_CREDENTIALS), + getPrefixedTableName(dbPrefix, USERS) + ); + } + + public MetadataStorageConnectorConfig getConfig() + { + return config.get(); + } + + public String getValidationQuery() + { + return "SELECT 1"; + } + + protected BasicDataSource getDatasource() + { + MetadataStorageConnectorConfig connectorConfig = getConfig(); + + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUsername(connectorConfig.getUser()); + dataSource.setPassword(connectorConfig.getPassword()); + String uri = connectorConfig.getConnectURI(); + dataSource.setUrl(uri); + + dataSource.setValidationQuery(getValidationQuery()); + dataSource.setTestOnBorrow(true); + + return dataSource; + } + + protected static String getPrefixedTableName(String dbPrefix, String baseTableName) + { + return StringUtils.format("basic_authentication_%s_%s", dbPrefix, baseTableName); + } + + private void makeDefaultSuperuser(String dbPrefix, String username, String password) + { + if (getUser(dbPrefix, username) != null) { + return; + } + + createUser(dbPrefix, username); + setUserCredentials(dbPrefix, username, password.toCharArray()); + } + + private int getUserCount(Handle handle, String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", userTableName, "name") + ) + .bind("key", userName) + .map(IntegerMapper.FIRST) + .first(); + } + + private static class UserCredentialsMapper implements ResultSetMapper> + { + @Override + public Map map(int index, ResultSet resultSet, StatementContext context) + throws SQLException + { + String user_name = resultSet.getString("user_name"); + byte[] salt = resultSet.getBytes("salt"); + byte[] hash = resultSet.getBytes("hash"); + int iterations = resultSet.getInt("iterations"); + return ImmutableMap.of( + "user_name", user_name, + "salt", salt, + "hash", hash, + "iterations", iterations + ); + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java new file mode 100644 index 000000000000..3d13ef14d209 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java @@ -0,0 +1,915 @@ +/* + * 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.db; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Predicate; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.java.util.common.IAE; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.metadata.BaseSQLMetadataConnector; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.BasicSecurityDBResourceException; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +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.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.TransactionCallback; +import org.skife.jdbi.v2.TransactionStatus; +import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.skife.jdbi.v2.util.IntegerMapper; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +public abstract class SQLBasicAuthorizerStorageConnector + extends BaseSQLMetadataConnector + implements BasicAuthorizerStorageConnector +{ + public static final String USERS = "users"; + public static final String PERMISSIONS = "permissions"; + public static final String ROLES = "roles"; + public static final String USER_ROLES = "user_roles"; + + private static final String DEFAULT_ADMIN_NAME = "admin"; + private static final String DEFAULT_ADMIN_ROLE = "admin"; + + private static final String DEFAULT_SYSTEM_USER_NAME = "druid_system"; + private static final String DEFAULT_SYSTEM_USER_ROLE = "druid_system"; + + private final Supplier config; + private final ObjectMapper jsonMapper; + private final PermissionsMapper permMapper; + private final Injector injector; + + @Inject + public SQLBasicAuthorizerStorageConnector( + Supplier config, + Injector injector, + ObjectMapper jsonMapper + ) + { + this.config = config; + this.injector = injector; + this.jsonMapper = jsonMapper; + this.permMapper = new PermissionsMapper(); + this.shouldRetry = new Predicate() + { + @Override + public boolean apply(Throwable e) + { + return isTransientException(e); + } + }; + } + + @LifecycleStart + public void start() + { + final AuthorizerMapper authorizerMapper = injector.getInstance(AuthorizerMapper.class); + + for (Map.Entry entry : authorizerMapper.getAuthorizerMap().entrySet()) { + Authorizer authorizer = entry.getValue(); + if (authorizer instanceof BasicRoleBasedAuthorizer) { + String authorizerName = entry.getKey(); + BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer; + BasicAuthDBConfig dbConfig = basicRoleBasedAuthorizer.getDbConfig(); + + retryTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + if (tableExists(handle, getPrefixedTableName(dbConfig.getDbPrefix(), USERS))) { + return null; + } + + createUserTable(dbConfig.getDbPrefix()); + createRoleTable(dbConfig.getDbPrefix()); + createPermissionTable(dbConfig.getDbPrefix()); + createUserRoleTable(dbConfig.getDbPrefix()); + + makeDefaultSuperuser( + dbConfig.getDbPrefix(), + DEFAULT_ADMIN_NAME, + DEFAULT_ADMIN_ROLE + ); + + makeDefaultSuperuser( + dbConfig.getDbPrefix(), + DEFAULT_SYSTEM_USER_NAME, + DEFAULT_SYSTEM_USER_ROLE + ); + + return null; + } + }, + 3, + 10 + ); + } + } + } + + @Override + public void createRoleTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + createTable( + roleTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + roleTableName + ) + ) + ); + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createPermissionTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + createTable( + permissionTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " id INTEGER NOT NULL,\n" + + " resource_json VARCHAR(255) NOT NULL,\n" + + " role_name INTEGER NOT NULL, \n" + + " PRIMARY KEY (id),\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + permissionTableName, + roleTableName + ) + ) + ); + } + + @Override + public void createUserRoleTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + createTable( + userRoleTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name VARCHAR(255) NOT NULL,\n" + + " role_name VARCHAR(255) NOT NULL, \n" + + " FOREIGN KEY (user_name) REFERENCES %2$s(name) ON DELETE CASCADE,\n" + + " FOREIGN KEY (role_name) REFERENCES %3$s(name) ON DELETE CASCADE\n" + + ")", + userRoleTableName, + userTableName, + roleTableName + ) + ) + ); + } + + @Override + public void createUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getUserCount(handle, dbPrefix, userName); + if (count != 0) { + throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); + } + + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (name) VALUES (:user_name)", userTableName + ) + ) + .bind("user_name", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public void deleteUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getUserCount(handle, dbPrefix, userName); + if (count == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE name = :userName", userTableName + ) + ) + .bind("userName", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public void createRole(String dbPrefix, String roleName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getRoleCount(handle, dbPrefix, roleName); + if (count != 0) { + throw new BasicSecurityDBResourceException("Role [%s] already exists.", roleName); + } + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (name) VALUES (:roleName)", roleTableName + ) + ) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void deleteRole(String dbPrefix, String roleName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int count = getRoleCount(handle, dbPrefix, roleName); + if (count == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE name = :roleName", roleTableName + ) + ) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void addPermission(String dbPrefix, String roleName, ResourceAction resourceAction) + { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int roleCount = getRoleCount(handle, dbPrefix, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + // make sure the resource regex compiles + try { + Pattern pattern = Pattern.compile(resourceAction.getResource().getName()); + } + catch (PatternSyntaxException pse) { + throw new BasicSecurityDBResourceException( + "Invalid permission, resource name regex[%s] does not compile.", + resourceAction.getResource().getName() + ); + } + + try { + byte[] serializedResourceAction = jsonMapper.writeValueAsBytes(resourceAction); + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (resource_json, role_name) VALUES (:resourceJson, :roleName)", + permissionTableName + ) + ) + .bind("resourceJson", serializedResourceAction) + .bind("roleName", roleName) + .execute(); + + return null; + } + catch (JsonProcessingException jpe) { + throw new IAE(jpe, "Could not serialize resourceAction [%s].", resourceAction); + } + } + } + ); + } + + @Override + public void deletePermission(String dbPrefix, int permissionId) + { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int permCount = getPermissionCount(handle, dbPrefix, permissionId); + if (permCount == 0) { + throw new BasicSecurityDBResourceException("Permission with id [%s] does not exist.", permissionId); + } + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE id = :permissionId", permissionTableName + ) + ) + .bind("permissionId", permissionId) + .execute(); + return null; + } + } + ); + } + + @Override + public void assignRole(String dbPrefix, String userName, String roleName) + { + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); + + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + int userRoleMappingCount = getUserRoleMappingCount(handle, dbPrefix, userName, roleName); + if (userRoleMappingCount != 0) { + throw new BasicSecurityDBResourceException("User [%s] already has role [%s].", userName, roleName); + } + + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (user_name, role_name) VALUES (:userName, :roleName)", userRoleTableName + ) + ) + .bind("userName", userName) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void unassignRole(String dbPrefix, String userName, String roleName) + { + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); + + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + int userRoleMappingCount = getUserRoleMappingCount(handle, dbPrefix, userName, roleName); + if (userRoleMappingCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not have role [%s].", userName, roleName); + } + + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE user_name = :userName AND role_name = :roleName", userRoleTableName + ) + ) + .bind("userName", userName) + .bind("roleName", roleName) + .execute(); + + return null; + } + } + ); + } + + @Override + public List> getAllUsers(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s", userTableName) + ) + .list(); + } + } + ); + } + + @Override + public List> getAllRoles(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s", roleTableName) + ) + .list(); + } + } + ); + } + + @Override + public Map getUser(String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s where name = :userName", userTableName) + ) + .bind("userName", userName) + .first(); + } + } + ); + } + + @Override + public Map getRole(String dbPrefix, String roleName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM %1$s where name = :roleName", roleTableName) + ) + .bind("roleName", roleName) + .first(); + } + } + ); + } + + @Override + public List> getRolesForUser(String dbPrefix, String userName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + return handle + .createQuery( + StringUtils.format( + "SELECT %1$s.name\n" + + "FROM %1$s\n" + + "JOIN %2$s\n" + + " ON %2$s.role_name = %1$s.name\n" + + "WHERE %2$s.user_name = :userName", + roleTableName, + userRoleTableName + ) + ) + .bind("userName", userName) + .list(); + } + } + ); + } + + @Override + public List> getUsersWithRole(String dbPrefix, String roleName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + int roleCount = getRoleCount(handle, dbPrefix, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + return handle + .createQuery( + StringUtils.format( + "SELECT %1$s.name\n" + + "FROM %1$s\n" + + "JOIN %2$s\n" + + " ON %2$s.user_name = %1$s.name\n" + + "WHERE %2$s.role_name = :roleName", + userTableName, + userRoleTableName + ) + ) + .bind("roleName", roleName) + .list(); + } + } + ); + } + + public List getTableNames(String dbPrefix) + { + return ImmutableList.of( + getPrefixedTableName(dbPrefix, USER_ROLES), + getPrefixedTableName(dbPrefix, PERMISSIONS), + getPrefixedTableName(dbPrefix, ROLES), + getPrefixedTableName(dbPrefix, USERS) + ); + } + + private class PermissionsMapper implements ResultSetMapper> + { + @Override + public Map map(int index, ResultSet resultSet, StatementContext context) + throws SQLException + { + int id = resultSet.getInt("id"); + byte[] resourceJson = resultSet.getBytes("resource_json"); + try { + final ResourceAction resourceAction = jsonMapper.readValue(resourceJson, ResourceAction.class); + return ImmutableMap.of( + "id", id, + "resourceAction", resourceAction + ); + } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + } + + @Override + public List> getPermissionsForRole(String dbPrefix, String roleName) + { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + int roleCount = getRoleCount(handle, dbPrefix, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + return handle + .createQuery( + StringUtils.format( + "SELECT %1$s.id, %1$s.resource_json\n" + + "FROM %1$s\n" + + "WHERE %1$s.role_name = :roleName", + permissionTableName + ) + ) + .map(permMapper) + .bind("roleName", roleName) + .list(); + } + } + ); + } + + @Override + public List> getPermissionsForUser(String dbPrefix, String userName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + int userCount = getUserCount(handle, dbPrefix, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + return handle + .createQuery( + StringUtils.format( + "SELECT %1$s.id, %1$s.resource_json, %2$s.name\n" + + "FROM %1$s\n" + + "JOIN %2$s\n" + + " ON %1$s.role_name = %2$s.name\n" + + "JOIN %3$s\n" + + " ON %3$s.role_name = %2$s.name\n" + + "WHERE %3$s.user_name = :userName", + permissionTableName, + roleTableName, + userRoleTableName + ) + ) + .map(permMapper) + .bind("userName", userName) + .list(); + } + } + ); + } + + public MetadataStorageConnectorConfig getConfig() + { + return config.get(); + } + + public String getValidationQuery() + { + return "SELECT 1"; + } + + protected BasicDataSource getDatasource() + { + MetadataStorageConnectorConfig connectorConfig = getConfig(); + + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUsername(connectorConfig.getUser()); + dataSource.setPassword(connectorConfig.getPassword()); + String uri = connectorConfig.getConnectURI(); + dataSource.setUrl(uri); + + dataSource.setValidationQuery(getValidationQuery()); + dataSource.setTestOnBorrow(true); + + return dataSource; + } + + protected static String getPrefixedTableName(String dbPrefix, String baseTableName) + { + return StringUtils.format("basic_authorization_%s_%s", dbPrefix, baseTableName); + } + + private int getUserCount(Handle handle, String dbPrefix, String userName) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", userTableName, "name") + ) + .bind("key", userName) + .map(IntegerMapper.FIRST) + .first(); + } + + private int getRoleCount(Handle handle, String dbPrefix, String roleName) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", roleTableName, "name") + ) + .bind("key", roleName) + .map(IntegerMapper.FIRST) + .first(); + } + + private int getPermissionCount(Handle handle, String dbPrefix, int permissionId) + { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", permissionTableName, "id") + ) + .bind("key", permissionId) + .map(IntegerMapper.FIRST) + .first(); + } + + private int getUserRoleMappingCount(Handle handle, String dbPrefix, String userName, String roleName) + { + final String userRoleTableName = getPrefixedTableName(dbPrefix, USER_ROLES); + + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :userkey AND %3$s = :rolekey", + userRoleTableName, + "user_name", + "role_name" + ) + ) + .bind("userkey", userName) + .bind("rolekey", roleName) + .map(IntegerMapper.FIRST) + .first(); + } + + private void makeDefaultSuperuser(String dbPrefix, String username, String role) + { + if (getUser(dbPrefix, username) != null) { + return; + } + + createUser(dbPrefix, username); + createRole(dbPrefix, role); + assignRole(dbPrefix, username, role); + + 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 + ); + + List resActs = Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); + + for (ResourceAction resAct : resActs) { + addPermission(dbPrefix, role, resAct); + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..58e51998ce89 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthenticatorStorageConnector.java @@ -0,0 +1,147 @@ +/* + * 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.db.derby; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.guice.ManageLifecycle; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorage; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthenticatorStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +@ManageLifecycle +public class DerbySQLBasicAuthenticatorStorageConnector extends SQLBasicAuthenticatorStorageConnector +{ + private static final Logger log = new Logger(DerbySQLBasicAuthenticatorStorageConnector.class); + + private final DBI dbi; + private final MetadataStorage storage; + + @Inject + public DerbySQLBasicAuthenticatorStorageConnector( + MetadataStorage storage, + Supplier config, + Injector injector + ) + { + super(config, injector); + + final BasicDataSource datasource = getDatasource(); + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("org.apache.derby.jdbc.ClientDriver"); + + this.dbi = new DBI(datasource); + this.storage = storage; + log.info("Derby connector instantiated with metadata storage [%s].", this.storage.getClass().getName()); + } + + public DerbySQLBasicAuthenticatorStorageConnector( + MetadataStorage storage, + Supplier config, + Injector injector, + DBI dbi + ) + { + super(config, injector); + this.dbi = dbi; + this.storage = storage; + } + + @Override + @LifecycleStart + public void start() + { + storage.start(); + super.start(); + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createUserCredentialsTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + createTable( + credentialsTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name VARCHAR(255) NOT NULL, \n" + + " salt BLOB(32) NOT NULL, \n" + + " hash BLOB(64) NOT NULL, \n" + + " iterations INTEGER NOT NULL, \n" + + " PRIMARY KEY (user_name), \n" + + " FOREIGN KEY (user_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + credentialsTableName, + userTableName + ) + ) + ); + } + + @Override + public boolean tableExists(Handle handle, String tableName) + { + return !handle.createQuery("select * from SYS.SYSTABLES where tablename = :tableName") + .bind("tableName", StringUtils.toUpperCase(tableName)) + .list() + .isEmpty(); + } + + @Override + public String getValidationQuery() + { + return "VALUES 1"; + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java new file mode 100644 index 000000000000..aba4c0eaef65 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java @@ -0,0 +1,168 @@ +/* + * 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.db.derby; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.guice.ManageLifecycle; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.lifecycle.LifecycleStart; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorage; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthorizerStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +@ManageLifecycle +public class DerbySQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector +{ + private static final Logger log = new Logger(DerbySQLBasicAuthorizerStorageConnector.class); + + private final DBI dbi; + private final MetadataStorage storage; + + @Inject + public DerbySQLBasicAuthorizerStorageConnector( + MetadataStorage storage, + Supplier config, + Injector injector, + ObjectMapper jsonMapper + ) + { + super(config, injector, jsonMapper); + + final BasicDataSource datasource = getDatasource(); + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("org.apache.derby.jdbc.ClientDriver"); + + this.dbi = new DBI(datasource); + this.storage = storage; + log.info("Derby connector instantiated with metadata storage [%s].", this.storage.getClass().getName()); + } + + public DerbySQLBasicAuthorizerStorageConnector( + MetadataStorage storage, + Supplier config, + Injector injector, + ObjectMapper jsonMapper, + DBI dbi + ) + { + super(config, injector, jsonMapper); + this.dbi = dbi; + this.storage = storage; + } + + @Override + @LifecycleStart + public void start() + { + storage.start(); + super.start(); + } + + @Override + public void createRoleTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + createTable( + roleTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name)\n" + + ")", + roleTableName + ) + ) + ); + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createPermissionTable(String dbPrefix) + { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + createTable( + permissionTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),\n" + + " resource_json BLOB(1024) NOT NULL,\n" + + " role_name VARCHAR(255) NOT NULL, \n" + + " PRIMARY KEY (id),\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + permissionTableName, + roleTableName + ) + ) + ); + } + + @Override + public boolean tableExists(Handle handle, String tableName) + { + return !handle.createQuery("select * from SYS.SYSTABLES where tablename = :tableName") + .bind("tableName", StringUtils.toUpperCase(tableName)) + .list() + .isEmpty(); + } + + @Override + public String getValidationQuery() + { + return "VALUES 1"; + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..22dcfd8b6727 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthenticatorStorageConnector.java @@ -0,0 +1,134 @@ +/* + * 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.db.mysql; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +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.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthenticatorStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.util.BooleanMapper; + +public class MySQLBasicAuthenticatorStorageConnector extends SQLBasicAuthenticatorStorageConnector +{ + private static final Logger log = new Logger(MySQLBasicAuthenticatorStorageConnector.class); + + private final DBI dbi; + + @Inject + public MySQLBasicAuthenticatorStorageConnector( + Supplier config, + Injector injector + ) + { + super(config, injector); + + final BasicDataSource datasource = getDatasource(); + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("com.mysql.jdbc.Driver"); + + // use double-quotes for quoting columns, so we can write SQL that works with most databases + datasource.setConnectionInitSqls(ImmutableList.of("SET sql_mode='ANSI_QUOTES'")); + + this.dbi = new DBI(datasource); + log.info("Configured MySQL as security storage"); + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createUserCredentialsTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + createTable( + credentialsTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name VARCHAR(255) NOT NULL, \n" + + " salt BLOB(32) NOT NULL, \n" + + " hash BLOB(64) NOT NULL, \n" + + " iterations INTEGER NOT NULL, \n" + + " PRIMARY KEY (user_name), \n" + + " FOREIGN KEY (user_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + credentialsTableName, + userTableName + ) + ) + ); + } + + + @Override + public boolean tableExists(Handle handle, String tableName) + { + // ensure database defaults to utf8, otherwise bail + boolean isUtf8 = handle + .createQuery("SELECT @@character_set_database = 'utf8'") + .map(BooleanMapper.FIRST) + .first(); + + if (!isUtf8) { + throw new ISE( + "Database default character set is not UTF-8." + System.lineSeparator() + + " Druid requires its MySQL database to be created using UTF-8 as default character set." + ); + } + + return !handle.createQuery("SHOW tables LIKE :tableName") + .bind("tableName", tableName) + .list() + .isEmpty(); + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java new file mode 100644 index 000000000000..31a33d672ce4 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java @@ -0,0 +1,154 @@ +/* + * 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.db.mysql; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +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.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthorizerStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.util.BooleanMapper; + +public class MySQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector +{ + private static final Logger log = new Logger(MySQLBasicAuthorizerStorageConnector.class); + + private final DBI dbi; + + @Inject + public MySQLBasicAuthorizerStorageConnector( + Supplier config, + Injector injector, + ObjectMapper jsonMapper + ) + { + super(config, injector, jsonMapper); + + final BasicDataSource datasource = getDatasource(); + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("com.mysql.jdbc.Driver"); + + // use double-quotes for quoting columns, so we can write SQL that works with most databases + datasource.setConnectionInitSqls(ImmutableList.of("SET sql_mode='ANSI_QUOTES'")); + + this.dbi = new DBI(datasource); + log.info("Configured MySQL as security storage"); + } + + @Override + public void createRoleTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + + createTable( + roleTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + roleTableName + ) + ) + ); + } + + @Override + public void createUserTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + + createTable( + userTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + userTableName + ) + ) + ); + } + + @Override + public void createPermissionTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + createTable( + permissionTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " id INTEGER NOT NULL AUTO_INCREMENT,\n" + + " resource_json BLOB(1024) NOT NULL,\n" + + " role_name VARCHAR(255) NOT NULL, \n" + + " PRIMARY KEY (id),\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + permissionTableName, + roleTableName + ) + ) + ); + } + + @Override + public boolean tableExists(Handle handle, String tableName) + { + // ensure database defaults to utf8, otherwise bail + boolean isUtf8 = handle + .createQuery("SELECT @@character_set_database = 'utf8'") + .map(BooleanMapper.FIRST) + .first(); + + if (!isUtf8) { + throw new ISE( + "Database default character set is not UTF-8." + System.lineSeparator() + + " Druid requires its MySQL database to be created using UTF-8 as default character set." + ); + } + + return !handle.createQuery("SHOW tables LIKE :tableName") + .bind("tableName", tableName) + .list() + .isEmpty(); + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..818571b1ca7b --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthenticatorStorageConnector.java @@ -0,0 +1,102 @@ +/* + * 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.db.postgres; + +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthenticatorStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.util.StringMapper; + +public class PostgreSQLBasicAuthenticatorStorageConnector extends SQLBasicAuthenticatorStorageConnector +{ + private static final Logger log = new Logger(PostgreSQLBasicAuthenticatorStorageConnector.class); + + private final DBI dbi; + + @Inject + public PostgreSQLBasicAuthenticatorStorageConnector( + Supplier config, + Injector injector + ) + { + super(config, injector); + + final BasicDataSource datasource = getDatasource(); + // PostgreSQL driver is classloader isolated as part of the extension + // so we need to help JDBC find the driver + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("org.postgresql.Driver"); + + this.dbi = new DBI(datasource); + + log.info("Configured PostgreSQL as security storage"); + } + + @Override + public boolean tableExists(final Handle handle, final String tableName) + { + return !handle.createQuery( + "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename ILIKE :tableName" + ) + .bind("tableName", tableName) + .map(StringMapper.FIRST) + .list() + .isEmpty(); + } + + @Override + public DBI getDBI() + { + return dbi; + } + + @Override + public void createUserCredentialsTable(String dbPrefix) + { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + final String credentialsTableName = getPrefixedTableName(dbPrefix, USER_CREDENTIALS); + + createTable( + credentialsTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name VARCHAR(255) NOT NULL, \n" + + " salt BYTEA NOT NULL, \n" + + " hash BYTEA NOT NULL, \n" + + " iterations INTEGER NOT NULL, \n" + + " PRIMARY KEY (user_name),\n" + + " FOREIGN KEY (user_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + credentialsTableName, + userTableName + ) + ) + ); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java new file mode 100644 index 000000000000..8e8a596dd712 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java @@ -0,0 +1,103 @@ +/* + * 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.db.postgres; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Supplier; +import com.google.common.collect.ImmutableList; +import com.google.inject.Inject; +import com.google.inject.Injector; +import io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.SQLBasicAuthorizerStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.util.StringMapper; + +public class PostgreSQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector +{ + private static final Logger log = new Logger(PostgreSQLBasicAuthorizerStorageConnector.class); + + private final DBI dbi; + + @Inject + public PostgreSQLBasicAuthorizerStorageConnector( + Supplier config, + Injector injector, + ObjectMapper jsonMapper + ) + { + super(config, injector, jsonMapper); + + final BasicDataSource datasource = getDatasource(); + // PostgreSQL driver is classloader isolated as part of the extension + // so we need to help JDBC find the driver + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("org.postgresql.Driver"); + + this.dbi = new DBI(datasource); + + log.info("Configured PostgreSQL as security storage"); + } + + @Override + public boolean tableExists(final Handle handle, final String tableName) + { + return !handle.createQuery( + "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'public' AND tablename ILIKE :tableName" + ) + .bind("tableName", tableName) + .map(StringMapper.FIRST) + .list() + .isEmpty(); + } + + @Override + public DBI getDBI() + { + return dbi; + } + + @Override + public void createPermissionTable(String dbPrefix) + { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + + createTable( + permissionTableName, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " id SERIAL NOT NULL,\n" + + " resource_json BYTEA NOT NULL,\n" + + " role_name VARCHAR(255) NOT NULL, \n" + + " PRIMARY KEY (id),\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + + ")", + permissionTableName, + roleTableName + ) + ) + ); + } +} 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 000000000000..02de8e2e58f1 --- /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 000000000000..5c0afedf7395 --- /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.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/BasicRoleBasedAuthorizerTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicRoleBasedAuthorizerTest.java new file mode 100644 index 000000000000..456ca3d638c1 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicRoleBasedAuthorizerTest.java @@ -0,0 +1,156 @@ +/* + * 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.java.util.common.StringUtils; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +import io.druid.security.db.TestDerbyAuthorizerStorageConnector; +import io.druid.server.security.Access; +import io.druid.server.security.Action; +import io.druid.server.security.AuthenticationResult; +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 org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.tweak.HandleCallback; + +public class BasicRoleBasedAuthorizerTest +{ + private static final String TEST_DB_PREFIX = "test"; + + @Rule + public final TestDerbyAuthorizerStorageConnector.DerbyConnectorRule derbyConnectorRule = + new TestDerbyAuthorizerStorageConnector.DerbyConnectorRule(TEST_DB_PREFIX); + + private BasicRoleBasedAuthorizer authorizer; + private TestDerbyAuthorizerStorageConnector connector; + + @Before + public void setUp() throws Exception + { + connector = derbyConnectorRule.getConnector(); + createAllTables(); + authorizer = new BasicRoleBasedAuthorizer(connector, TEST_DB_PREFIX, 5000); + } + + @Test + public void testAuth() + { + connector.createUser(TEST_DB_PREFIX, "druid"); + connector.createRole(TEST_DB_PREFIX, "druidRole"); + connector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + + ResourceAction permission = new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ); + connector.addPermission(TEST_DB_PREFIX, "druidRole", permission); + + 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()); + } + + @Test + public void testMorePermissionsThanCacheSize() + { + connector.createUser(TEST_DB_PREFIX, "druid"); + connector.createRole(TEST_DB_PREFIX, "druidRole"); + connector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + + for (int i = 0; i < authorizer.getPermissionCacheSize() + 50; i++) { + ResourceAction permission = new ResourceAction( + new Resource("testResource-" + i, ResourceType.DATASOURCE), + Action.WRITE + ); + connector.addPermission(TEST_DB_PREFIX, "druidRole", permission); + } + + AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null); + + Access access = authorizer.authorize( + authenticationResult, + new Resource("testResource-300", ResourceType.DATASOURCE), + Action.WRITE + ); + Assert.assertTrue(access.isAllowed()); + + access = authorizer.authorize( + authenticationResult, + new Resource("matchesNothing", ResourceType.DATASOURCE), + Action.WRITE + ); + Assert.assertFalse(access.isAllowed()); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + private void createAllTables() + { + connector.createUserTable(TEST_DB_PREFIX); + connector.createRoleTable(TEST_DB_PREFIX); + connector.createPermissionTable(TEST_DB_PREFIX); + connector.createUserRoleTable(TEST_DB_PREFIX); + } + + private void dropAllTables() + { + for (String table : connector.getTableNames(TEST_DB_PREFIX)) { + dropTable(table); + } + } + + private void dropTable(final String tableName) + { + connector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthenticatorResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthenticatorResourceTest.java new file mode 100644 index 000000000000..ebeefd4b9a68 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthenticatorResourceTest.java @@ -0,0 +1,264 @@ +/* + * 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.db; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.BasicAuthUtils; +import io.druid.security.basic.BasicAuthenticatorResource; +import io.druid.security.basic.authentication.BasicHTTPAuthenticator; +import io.druid.server.security.AllowAllAuthenticator; +import io.druid.server.security.AuthenticatorMapper; +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 org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; + +public class BasicAuthenticatorResourceTest +{ + private static final String BASIC_AUTHENTICATOR_NAME = "basic"; + private static final String BASIC_AUTHENTICATOR_NAME2 = "basic2"; + + private BasicAuthenticatorResource resource; + private HttpServletRequest req; + private TestDerbyAuthenticatorStorageConnector connector; + + @Rule + public final TestDerbyAuthenticatorStorageConnector.DerbyConnectorRule derbyConnectorRule = + new TestDerbyAuthenticatorStorageConnector.DerbyConnectorRule("test"); + + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() throws Exception + { + req = EasyMock.createStrictMock(HttpServletRequest.class); + connector = derbyConnectorRule.getConnector(); + + AuthenticatorMapper mapper = new AuthenticatorMapper( + ImmutableMap.of( + BASIC_AUTHENTICATOR_NAME, new BasicHTTPAuthenticator(connector, BASIC_AUTHENTICATOR_NAME, "druid", "druid", "druid"), + BASIC_AUTHENTICATOR_NAME2, new BasicHTTPAuthenticator(connector, BASIC_AUTHENTICATOR_NAME2, "druid", "druid", "druid"), + "allowAll", new AllowAllAuthenticator() + ), + "basic" + ); + + createAllTables(); + resource = new BasicAuthenticatorResource(connector, mapper); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testSeparateDatabaseTables() + { + Response response = resource.getAllUsers(req, BASIC_AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid2"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid3"); + + resource.createUser(req, BASIC_AUTHENTICATOR_NAME2, "druid4"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME2, "druid5"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME2, "druid6"); + + List> expectedUsers = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + List> expectedUsers2 = ImmutableList.of( + ImmutableMap.of("name", "druid4"), + ImmutableMap.of("name", "druid5"), + ImmutableMap.of("name", "druid6") + ); + + response = resource.getAllUsers(req, BASIC_AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + + response = resource.getAllUsers(req, BASIC_AUTHENTICATOR_NAME2); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers2, response.getEntity()); + } + + @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, BASIC_AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid2"); + resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid3"); + + List> expectedUsers = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + + response = resource.getAllUsers(req, BASIC_AUTHENTICATOR_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + } + + @Test + public void testCreateDeleteUser() + { + Response response = resource.createUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid") + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.deleteUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = resource.getUser(req, BASIC_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, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.updateUserCredentials(req, BASIC_AUTHENTICATOR_NAME, "druid", "helloworld"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map ent = (Map) response.getEntity(); + Map credentials = (Map) ent.get("credentials"); + Assert.assertEquals( + ImmutableMap.of("name", "druid"), + ent.get("user") + ); + + byte[] salt = (byte[]) credentials.get("salt"); + byte[] hash = (byte[]) credentials.get("hash"); + int iterations = (Integer) credentials.get("iterations"); + Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length); + Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length); + Assert.assertEquals(BasicAuthUtils.KEY_ITERATIONS, iterations); + + byte[] recalculatedHash = BasicAuthUtils.hashPassword( + "helloworld".toCharArray(), + salt, + iterations + ); + Assert.assertArrayEquals(recalculatedHash, hash); + + response = resource.deleteUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHENTICATOR_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = resource.updateUserCredentials(req, BASIC_AUTHENTICATOR_NAME, "druid", "helloworld"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + private void createAllTables() + { + connector.createUserTable(BASIC_AUTHENTICATOR_NAME); + connector.createUserCredentialsTable(BASIC_AUTHENTICATOR_NAME); + + connector.createUserTable(BASIC_AUTHENTICATOR_NAME2); + connector.createUserCredentialsTable(BASIC_AUTHENTICATOR_NAME2); + } + + private void dropAllTables() + { + for (String table : connector.getTableNamesForPrefix(BASIC_AUTHENTICATOR_NAME)) { + dropTable(table); + } + + for (String table : connector.getTableNamesForPrefix(BASIC_AUTHENTICATOR_NAME2)) { + dropTable(table); + } + } + + private void dropTable(final String tableName) + { + connector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } + + 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/db/BasicAuthorizerResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthorizerResourceTest.java new file mode 100644 index 000000000000..2f8e280a0d08 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthorizerResourceTest.java @@ -0,0 +1,617 @@ +/* + * 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.db; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.BasicAuthorizerResource; +import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; +import io.druid.server.security.Action; +import io.druid.server.security.AllowAllAuthorizer; +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 org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; + +public class BasicAuthorizerResourceTest +{ + private static final String BASIC_AUTHORIZER_NAME = "basic"; + private static final String BASIC_AUTHORIZER_NAME2 = "basic2"; + + private BasicAuthorizerResource resource; + private HttpServletRequest req; + private TestDerbyAuthorizerStorageConnector connector; + + @Rule + public final TestDerbyAuthorizerStorageConnector.DerbyConnectorRule derbyConnectorRule = + new TestDerbyAuthorizerStorageConnector.DerbyConnectorRule("test"); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + + @Before + public void setUp() throws Exception + { + req = EasyMock.createStrictMock(HttpServletRequest.class); + connector = derbyConnectorRule.getConnector(); + + AuthorizerMapper mapper = new AuthorizerMapper( + ImmutableMap.of( + BASIC_AUTHORIZER_NAME, new BasicRoleBasedAuthorizer(connector, BASIC_AUTHORIZER_NAME, 5000), + BASIC_AUTHORIZER_NAME2, new BasicRoleBasedAuthorizer(connector, BASIC_AUTHORIZER_NAME2, 5000), + "allowAll", new AllowAllAuthorizer() + ) + ); + + createAllTables(); + resource = new BasicAuthorizerResource(connector, mapper); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testSeparateDatabaseTables() + { + Response response = resource.getAllUsers(req, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid3"); + + resource.createUser(req, BASIC_AUTHORIZER_NAME2, "druid4"); + resource.createUser(req, BASIC_AUTHORIZER_NAME2, "druid5"); + resource.createUser(req, BASIC_AUTHORIZER_NAME2, "druid6"); + + List> expectedUsers = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + List> expectedUsers2 = ImmutableList.of( + ImmutableMap.of("name", "druid4"), + ImmutableMap.of("name", "druid5"), + ImmutableMap.of("name", "druid6") + ); + + response = resource.getAllUsers(req, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + + response = resource.getAllUsers(req, BASIC_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, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid3"); + + List> expectedUsers = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + + response = resource.getAllUsers(req, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + } + + + @Test + public void testGetAllRoles() + { + Response response = resource.getAllRoles(req, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + resource.createRole(req, BASIC_AUTHORIZER_NAME, "druid"); + resource.createRole(req, BASIC_AUTHORIZER_NAME, "druid2"); + resource.createRole(req, BASIC_AUTHORIZER_NAME, "druid3"); + + List> expectedRoles = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + + response = resource.getAllRoles(req, BASIC_AUTHORIZER_NAME); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoles, response.getEntity()); + } + + @Test + public void testCreateDeleteUser() + { + Response response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.deleteUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + @Test + public void testCreateDeleteRole() + { + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + Map expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + 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, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole")), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + Map expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(ImmutableMap.of("name", "druid")), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedRole, response.getEntity()); + } + + @Test + public void testDeleteAssignedRole() + { + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole")), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser2 = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid2"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole")), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser2, response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + Map expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(ImmutableMap.of("name", "druid"), ImmutableMap.of("name", "druid2")), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + expectedUser2 = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid2"), + "roles", ImmutableList.of(), + "permissions", ImmutableList.of() + ); + Assert.assertEquals(expectedUser2, response.getEntity()); + } + + @Test + public void testRolesAndPerms() + { + Response response = resource.createRole(req, BASIC_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.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "wrongRole", perms); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [wrongRole] does not exist."), response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + Map expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 1, "resourceAction", perms.get(0)), + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)) + ) + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 7); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Permission with id [7] does not exist."), response.getEntity()); + + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 2); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 1, "resourceAction", perms.get(0)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)) + ) + ); + Assert.assertEquals(expectedRole, response.getEntity()); + } + + @Test + public void testUsersRolesAndPerms() + { + Response response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.createRole(req, BASIC_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.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = resource.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole2", perms2); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole"), ImmutableMap.of("name", "druidRole2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 1, "resourceAction", perms.get(0)), + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)), + ImmutableMap.of("id", 4, "resourceAction", perms2.get(0)), + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid2"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole"), ImmutableMap.of("name", "druidRole2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 1, "resourceAction", perms.get(0)), + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)), + ImmutableMap.of("id", 4, "resourceAction", perms2.get(0)), + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + Map expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole"), + "users", ImmutableList.of(ImmutableMap.of("name", "druid"), ImmutableMap.of("name", "druid2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 1, "resourceAction", perms.get(0)), + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)) + ) + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + expectedRole = ImmutableMap.of( + "role", ImmutableMap.of("name", "druidRole2"), + "users", ImmutableList.of(ImmutableMap.of("name", "druid"), ImmutableMap.of("name", "druid2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 4, "resourceAction", perms2.get(0)), + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedRole, response.getEntity()); + + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 1); + Assert.assertEquals(200, response.getStatus()); + + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 4); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole"), ImmutableMap.of("name", "druidRole2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)), + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid2"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole"), ImmutableMap.of("name", "druidRole2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)), + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole2")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 5, "resourceAction", perms2.get(1)), + ImmutableMap.of("id", 6, "resourceAction", perms2.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + Assert.assertEquals(200, response.getStatus()); + expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid2"), + "roles", ImmutableList.of(ImmutableMap.of("name", "druidRole")), + "permissions", ImmutableList.of( + ImmutableMap.of("id", 2, "resourceAction", perms.get(1)), + ImmutableMap.of("id", 3, "resourceAction", perms.get(2)) + ) + ); + Assert.assertEquals(expectedUser, response.getEntity()); + } + + private void createAllTables() + { + connector.createUserTable(BASIC_AUTHORIZER_NAME); + connector.createRoleTable(BASIC_AUTHORIZER_NAME); + connector.createPermissionTable(BASIC_AUTHORIZER_NAME); + connector.createUserRoleTable(BASIC_AUTHORIZER_NAME); + + connector.createUserTable(BASIC_AUTHORIZER_NAME2); + connector.createRoleTable(BASIC_AUTHORIZER_NAME2); + connector.createPermissionTable(BASIC_AUTHORIZER_NAME2); + connector.createUserRoleTable(BASIC_AUTHORIZER_NAME2); + } + + private void dropAllTables() + { + for (String table : connector.getTableNames(BASIC_AUTHORIZER_NAME)) { + dropTable(table); + } + + for (String table : connector.getTableNames(BASIC_AUTHORIZER_NAME2)) { + dropTable(table); + } + } + + private void dropTable(final String tableName) + { + connector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } + + 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/db/SQLBasicAuthenticatorStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthenticatorStorageConnectorTest.java new file mode 100644 index 000000000000..2ebc89cf648c --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthenticatorStorageConnectorTest.java @@ -0,0 +1,191 @@ +/* + * 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.db; + +import com.google.common.collect.ImmutableMap; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.BasicAuthUtils; +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 org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.exceptions.CallbackFailedException; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import java.util.Map; + +public class SQLBasicAuthenticatorStorageConnectorTest +{ + private final String TEST_DB_PREFIX = "test"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyAuthenticatorStorageConnector.DerbyConnectorRule authenticatorRule = + new TestDerbyAuthenticatorStorageConnector.DerbyConnectorRule(TEST_DB_PREFIX); + + private TestDerbyAuthenticatorStorageConnector authenticatorConnector; + + @Before + public void setUp() throws Exception + { + authenticatorConnector = authenticatorRule.getConnector(); + createAllTables(); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testCreateTables() throws Exception + { + authenticatorConnector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + for (String table : authenticatorConnector.getTableNamesForPrefix(TEST_DB_PREFIX)) { + Assert.assertTrue( + StringUtils.format("authentication table %s was not created!", table), + authenticatorConnector.tableExists(handle, table) + ); + } + + return null; + } + } + ); + } + + // user tests + @Test + public void testCreateDeleteUser() throws Exception + { + authenticatorConnector.createUser(TEST_DB_PREFIX, "druid"); + Map expectedUser = ImmutableMap.of( + "name", "druid" + ); + Map dbUser = authenticatorConnector.getUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(expectedUser, dbUser); + + authenticatorConnector.deleteUser(TEST_DB_PREFIX, "druid"); + dbUser = authenticatorConnector.getUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(null, dbUser); + } + + @Test + public void testDeleteNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + authenticatorConnector.deleteUser(TEST_DB_PREFIX, "druid"); + } + + @Test + public void testCreateDuplicateUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] already exists."); + authenticatorConnector.createUser(TEST_DB_PREFIX, "druid"); + authenticatorConnector.createUser(TEST_DB_PREFIX, "druid"); + } + + // user credentials + @Test + public void testAddUserCredentials() throws Exception + { + char[] pass = "blah".toCharArray(); + authenticatorConnector.createUser(TEST_DB_PREFIX, "druid"); + authenticatorConnector.setUserCredentials(TEST_DB_PREFIX, "druid", pass); + Assert.assertTrue(authenticatorConnector.checkCredentials(TEST_DB_PREFIX, "druid", pass)); + Assert.assertFalse(authenticatorConnector.checkCredentials(TEST_DB_PREFIX, "druid", "wrongPass".toCharArray())); + + Map creds = authenticatorConnector.getUserCredentials(TEST_DB_PREFIX, "druid"); + Assert.assertEquals("druid", creds.get("user_name")); + byte[] salt = (byte[]) creds.get("salt"); + byte[] hash = (byte[]) creds.get("hash"); + int iterations = (Integer) creds.get("iterations"); + Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length); + Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length); + Assert.assertEquals(BasicAuthUtils.KEY_ITERATIONS, iterations); + + byte[] recalculatedHash = BasicAuthUtils.hashPassword( + pass, + salt, + iterations + ); + Assert.assertArrayEquals(recalculatedHash, hash); + } + + @Test + public void testAddCredentialsToNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + char[] pass = "blah".toCharArray(); + authenticatorConnector.setUserCredentials(TEST_DB_PREFIX, "druid", pass); + } + + @Test + public void testGetCredentialsForNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + authenticatorConnector.getUserCredentials(TEST_DB_PREFIX, "druid"); + } + + + private void createAllTables() + { + authenticatorConnector.createUserTable(TEST_DB_PREFIX); + authenticatorConnector.createUserCredentialsTable(TEST_DB_PREFIX); + } + + private void dropAllTables() + { + for (String table : authenticatorConnector.getTableNamesForPrefix(TEST_DB_PREFIX)) { + dropAuthenticatorTable(table); + } + } + + private void dropAuthenticatorTable(final String tableName) + { + authenticatorConnector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java new file mode 100644 index 000000000000..d775736ddd33 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java @@ -0,0 +1,333 @@ +/* + * 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.db; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.druid.java.util.common.StringUtils; +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 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 org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.exceptions.CallbackFailedException; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import java.util.List; +import java.util.Map; + +public class SQLBasicAuthorizerStorageConnectorTest +{ + private final String TEST_DB_PREFIX = "test"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbyAuthorizerStorageConnector.DerbyConnectorRule authorizerRule = + new TestDerbyAuthorizerStorageConnector.DerbyConnectorRule(TEST_DB_PREFIX); + + private TestDerbyAuthorizerStorageConnector authorizerConnector; + + @Before + public void setUp() throws Exception + { + authorizerConnector = authorizerRule.getConnector(); + createAllTables(); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testCreateTables() throws Exception + { + authorizerConnector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + for (String table : authorizerConnector.getTableNames(TEST_DB_PREFIX)) { + Assert.assertTrue( + StringUtils.format("authorization table %s was not created!", table), + authorizerConnector.tableExists(handle, table) + ); + } + + return null; + } + } + ); + } + + // user tests + @Test + public void testCreateDeleteUser() throws Exception + { + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + Map expectedUser = ImmutableMap.of( + "name", "druid" + ); + Map dbUser = authorizerConnector.getUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(expectedUser, dbUser); + + authorizerConnector.deleteUser(TEST_DB_PREFIX, "druid"); + dbUser = authorizerConnector.getUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(null, dbUser); + } + + @Test + public void testDeleteNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + authorizerConnector.deleteUser(TEST_DB_PREFIX, "druid"); + } + + @Test + public void testCreateDuplicateUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] already exists."); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + } + + // role tests + @Test + public void testCreateRole() throws Exception + { + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + Map expectedRole = ImmutableMap.of( + "name", "druid" + ); + Map dbRole = authorizerConnector.getRole(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(expectedRole, dbRole); + } + + @Test + public void testDeleteNonExistentRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druid] does not exist."); + authorizerConnector.deleteRole(TEST_DB_PREFIX, "druid"); + } + + @Test + public void testCreateDuplicateRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druid] already exists."); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + } + + // role and user tests + @Test + public void testAddAndRemoveRole() throws Exception + { + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + + List> expectedUsersWithRole = ImmutableList.of( + ImmutableMap.of("name", "druid") + ); + + List> expectedRolesForUser = ImmutableList.of( + ImmutableMap.of("name", "druidRole") + ); + + List> usersWithRole = authorizerConnector.getUsersWithRole(TEST_DB_PREFIX, "druidRole"); + List> rolesForUser = authorizerConnector.getRolesForUser(TEST_DB_PREFIX, "druid"); + + Assert.assertEquals(expectedUsersWithRole, usersWithRole); + Assert.assertEquals(expectedRolesForUser, rolesForUser); + + authorizerConnector.unassignRole(TEST_DB_PREFIX, "druid", "druidRole"); + usersWithRole = authorizerConnector.getUsersWithRole(TEST_DB_PREFIX, "druidRole"); + rolesForUser = authorizerConnector.getRolesForUser(TEST_DB_PREFIX, "druid"); + + Assert.assertEquals(ImmutableList.of(), usersWithRole); + Assert.assertEquals(ImmutableList.of(), rolesForUser); + } + + @Test + public void testAddRoleToNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [nonUser] does not exist."); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "nonUser", "druid"); + } + + @Test + public void testAddNonexistentRoleToUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [nonRole] does not exist."); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "nonRole"); + } + + @Test + public void testAddExistingRoleToUserFails() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] already has role [druidRole]."); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + } + + @Test + public void testUnassignInvalidRoleAssignmentFails() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not have role [druidRole]."); + + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); + + List> usersWithRole = authorizerConnector.getUsersWithRole(TEST_DB_PREFIX, "druidRole"); + List> rolesForUser = authorizerConnector.getRolesForUser(TEST_DB_PREFIX, "druid"); + + Assert.assertEquals(ImmutableList.of(), usersWithRole); + Assert.assertEquals(ImmutableList.of(), rolesForUser); + + authorizerConnector.unassignRole(TEST_DB_PREFIX, "druid", "druidRole"); + } + + // role and permission tests + @Test + public void testAddPermissionToRole() throws Exception + { + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); + + ResourceAction permission = new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); + + List> expectedPerms = ImmutableList.of( + ImmutableMap.of( + "id", 1, + "resourceAction", permission + ) + ); + List> dbPermsRole = authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); + Assert.assertEquals(expectedPerms, dbPermsRole); + List> dbPermsUser = authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(expectedPerms, dbPermsUser); + + authorizerConnector.deletePermission(TEST_DB_PREFIX, 1); + dbPermsRole = authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); + Assert.assertEquals(ImmutableList.of(), dbPermsRole); + dbPermsUser = authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); + Assert.assertEquals(ImmutableList.of(), dbPermsUser); + } + + @Test + public void testAddPermissionToNonExistentRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druidRole] does not exist."); + + ResourceAction permission = new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); + } + + @Test + public void testAddBadPermission() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Invalid permission, resource name regex[??????????] does not compile."); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); + ResourceAction permission = new ResourceAction( + new Resource("??????????", ResourceType.DATASOURCE), + Action.WRITE + ); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); + } + + @Test + public void testGetPermissionForNonExistentRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druidRole] does not exist."); + authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); + } + + @Test + public void testGetPermissionForNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); + } + + private void createAllTables() + { + authorizerConnector.createUserTable(TEST_DB_PREFIX); + authorizerConnector.createRoleTable(TEST_DB_PREFIX); + authorizerConnector.createPermissionTable(TEST_DB_PREFIX); + authorizerConnector.createUserRoleTable(TEST_DB_PREFIX); + } + + private void dropAllTables() + { + for (String table : authorizerConnector.getTableNames(TEST_DB_PREFIX)) { + dropAuthorizerTable(table); + } + } + + private void dropAuthorizerTable(final String tableName) + { + authorizerConnector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthenticatorStorageConnector.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthenticatorStorageConnector.java new file mode 100644 index 000000000000..70f4bfd8034a --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthenticatorStorageConnector.java @@ -0,0 +1,130 @@ +/* + * 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.db; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import io.druid.java.util.common.StringUtils; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.metadata.NoopMetadataStorageProvider; +import io.druid.security.basic.db.BasicAuthDBConfig; +import io.druid.security.basic.db.derby.DerbySQLBasicAuthenticatorStorageConnector; +import org.junit.Assert; +import org.junit.rules.ExternalResource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; + +import java.sql.SQLException; +import java.util.UUID; + +public class TestDerbyAuthenticatorStorageConnector extends DerbySQLBasicAuthenticatorStorageConnector +{ + private final String jdbcUri; + + public TestDerbyAuthenticatorStorageConnector( + Supplier config, + Supplier dbConfigSupplier + ) + { + this(config, dbConfigSupplier, "jdbc:derby:memory:druidTest" + dbSafeUUID()); + } + + protected TestDerbyAuthenticatorStorageConnector( + Supplier config, + Supplier dbConfigSupplier, + String jdbcUri + ) + { + super( + new NoopMetadataStorageProvider().get(), + config, + null, + new DBI(jdbcUri + ";create=true") + ); + this.jdbcUri = jdbcUri; + } + + public void tearDown() + { + try { + new DBI(jdbcUri + ";drop=true").open().close(); + } + catch (UnableToObtainConnectionException e) { + SQLException cause = (SQLException) e.getCause(); + // error code "08006" indicates proper shutdown + Assert.assertEquals(StringUtils.format("Derby not shutdown: [%s]", cause.toString()), "08006", cause.getSQLState()); + } + } + + public static String dbSafeUUID() + { + return UUID.randomUUID().toString().replace("-", ""); + } + + public String getJdbcUri() + { + return jdbcUri; + } + + public static class DerbyConnectorRule extends ExternalResource + { + private TestDerbyAuthenticatorStorageConnector connector; + private final Supplier dbConfigSupplier; + private final MetadataStorageConnectorConfig connectorConfig; + + public DerbyConnectorRule(String dbPrefix) + { + this(Suppliers.ofInstance(new BasicAuthDBConfig(dbPrefix, "druid", "druid"))); + } + + public DerbyConnectorRule( + Supplier dbConfigSupplier + ) + { + this.dbConfigSupplier = dbConfigSupplier; + this.connectorConfig = new MetadataStorageConnectorConfig() + { + @Override + public String getConnectURI() + { + return connector.getJdbcUri(); + } + }; + } + + @Override + protected void before() throws Throwable + { + connector = new TestDerbyAuthenticatorStorageConnector(Suppliers.ofInstance(connectorConfig), dbConfigSupplier); + connector.getDBI().open().close(); // create db + } + + @Override + protected void after() + { + connector.tearDown(); + } + + public TestDerbyAuthenticatorStorageConnector getConnector() + { + return connector; + } + } +} diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java new file mode 100644 index 000000000000..dc7dfa8156bb --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java @@ -0,0 +1,132 @@ +/* + * 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.db; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import io.druid.java.util.common.StringUtils; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.metadata.NoopMetadataStorageProvider; +import io.druid.security.basic.db.BasicAuthDBConfig; +import io.druid.security.basic.db.derby.DerbySQLBasicAuthorizerStorageConnector; +import org.junit.Assert; +import org.junit.rules.ExternalResource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; + +import java.sql.SQLException; +import java.util.UUID; + +public class TestDerbyAuthorizerStorageConnector extends DerbySQLBasicAuthorizerStorageConnector +{ + private final String jdbcUri; + + public TestDerbyAuthorizerStorageConnector( + Supplier config, + Supplier dbConfigSupplier + ) + { + this(config, dbConfigSupplier, "jdbc:derby:memory:druidTest" + dbSafeUUID()); + } + + protected TestDerbyAuthorizerStorageConnector( + Supplier config, + Supplier dbConfigSupplier, + String jdbcUri + ) + { + super( + new NoopMetadataStorageProvider().get(), + config, + null, + new ObjectMapper(), + new DBI(jdbcUri + ";create=true") + ); + this.jdbcUri = jdbcUri; + } + + public void tearDown() + { + try { + new DBI(jdbcUri + ";drop=true").open().close(); + } + catch (UnableToObtainConnectionException e) { + SQLException cause = (SQLException) e.getCause(); + // error code "08006" indicates proper shutdown + Assert.assertEquals(StringUtils.format("Derby not shutdown: [%s]", cause.toString()), "08006", cause.getSQLState()); + } + } + + public static String dbSafeUUID() + { + return UUID.randomUUID().toString().replace("-", ""); + } + + public String getJdbcUri() + { + return jdbcUri; + } + + public static class DerbyConnectorRule extends ExternalResource + { + private TestDerbyAuthorizerStorageConnector connector; + private final Supplier dbConfigSupplier; + private final MetadataStorageConnectorConfig connectorConfig; + + public DerbyConnectorRule(String dbPrefix) + { + this(Suppliers.ofInstance(new BasicAuthDBConfig("test", "druid", "druid"))); + } + + public DerbyConnectorRule( + Supplier dbConfigSupplier + ) + { + this.dbConfigSupplier = dbConfigSupplier; + this.connectorConfig = new MetadataStorageConnectorConfig() + { + @Override + public String getConnectURI() + { + return connector.getJdbcUri(); + } + }; + } + + @Override + protected void before() throws Throwable + { + connector = new TestDerbyAuthorizerStorageConnector(Suppliers.ofInstance(connectorConfig), dbConfigSupplier); + connector.getDBI().open().close(); // create db + } + + @Override + protected void after() + { + connector.tearDown(); + } + + public TestDerbyAuthorizerStorageConnector getConnector() + { + return connector; + } + } +} diff --git a/extensions-core/mysql-metadata-storage/pom.xml b/extensions-core/mysql-metadata-storage/pom.xml index 0269029c2f23..bca1a29e6442 100644 --- a/extensions-core/mysql-metadata-storage/pom.xml +++ b/extensions-core/mysql-metadata-storage/pom.xml @@ -54,7 +54,7 @@ mysql mysql-connector-java - 5.1.38 + ${mysql.version} org.jdbi diff --git a/extensions-core/postgresql-metadata-storage/pom.xml b/extensions-core/postgresql-metadata-storage/pom.xml index f06eb57b5a62..9cfcf7673dd3 100644 --- a/extensions-core/postgresql-metadata-storage/pom.xml +++ b/extensions-core/postgresql-metadata-storage/pom.xml @@ -54,7 +54,7 @@ org.postgresql postgresql - 9.4.1208.jre7 + ${postgresql.version} org.jdbi diff --git a/pom.xml b/pom.xml index 6029a1393305..830676617d16 100644 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,8 @@ Need to update Druid to use Jackson 2.6+ --> 1.10.77 2.5.5 + 5.1.38 + 9.4.1208.jre7 @@ -116,6 +118,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/metadata/BaseSQLMetadataConnector.java b/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java new file mode 100644 index 000000000000..17e753189fa9 --- /dev/null +++ b/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java @@ -0,0 +1,140 @@ +/* + * 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; + +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import io.druid.java.util.common.RetryUtils; +import io.druid.java.util.common.logger.Logger; +import org.skife.jdbi.v2.Batch; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.TransactionCallback; +import org.skife.jdbi.v2.exceptions.DBIException; +import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; +import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; +import org.skife.jdbi.v2.tweak.HandleCallback; + +import java.sql.SQLException; +import java.sql.SQLRecoverableException; +import java.sql.SQLTransientException; +import java.util.concurrent.Callable; + +public abstract class BaseSQLMetadataConnector +{ + private static final Logger log = new Logger(BaseSQLMetadataConnector.class); + + static final int DEFAULT_MAX_TRIES = 10; + protected Predicate shouldRetry; + + public abstract boolean tableExists(Handle handle, String tableName); + + public abstract DBI getDBI(); + + public T retryWithHandle( + final HandleCallback callback, + final Predicate myShouldRetry + ) + { + final Callable call = new Callable() + { + @Override + public T call() throws Exception + { + return getDBI().withHandle(callback); + } + }; + try { + return RetryUtils.retry(call, myShouldRetry, DEFAULT_MAX_TRIES); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + + public T retryWithHandle(final HandleCallback callback) + { + return retryWithHandle(callback, shouldRetry); + } + + public T retryTransaction(final TransactionCallback callback, final int quietTries, final int maxTries) + { + final Callable call = new Callable() + { + @Override + public T call() throws Exception + { + return getDBI().inTransaction(callback); + } + }; + try { + return RetryUtils.retry(call, shouldRetry, quietTries, maxTries); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + + public void createTable(final String tableName, final Iterable sql) + { + try { + retryWithHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + if (!tableExists(handle, tableName)) { + log.info("Creating table[%s]", tableName); + final Batch batch = handle.createBatch(); + for (String s : sql) { + batch.add(s); + } + batch.execute(); + } else { + log.info("Table[%s] already exists", tableName); + } + return null; + } + } + ); + } + catch (Exception e) { + log.warn(e, "Exception creating table"); + } + } + + public final boolean isTransientException(Throwable e) + { + return e != null && (e instanceof RetryTransactionException + || e instanceof SQLTransientException + || e instanceof SQLRecoverableException + || e instanceof UnableToObtainConnectionException + || e instanceof UnableToExecuteStatementException + || connectorIsTransientException(e) + || (e instanceof SQLException && isTransientException(e.getCause())) + || (e instanceof DBIException && isTransientException(e.getCause()))); + } + + protected boolean connectorIsTransientException(Throwable e) + { + return false; + } +} diff --git a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java index d30231de3d5e..cf1b08f2c153 100644 --- a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java +++ b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java @@ -21,42 +21,30 @@ import com.google.common.base.Predicate; import com.google.common.base.Supplier; -import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; 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.logger.Logger; import org.apache.commons.dbcp2.BasicDataSource; import org.skife.jdbi.v2.Batch; -import org.skife.jdbi.v2.DBI; import org.skife.jdbi.v2.Handle; import org.skife.jdbi.v2.TransactionCallback; import org.skife.jdbi.v2.TransactionStatus; -import org.skife.jdbi.v2.exceptions.DBIException; -import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException; -import org.skife.jdbi.v2.exceptions.UnableToObtainConnectionException; import org.skife.jdbi.v2.tweak.HandleCallback; import org.skife.jdbi.v2.util.ByteArrayMapper; import org.skife.jdbi.v2.util.IntegerMapper; import java.sql.Connection; import java.sql.SQLException; -import java.sql.SQLRecoverableException; -import java.sql.SQLTransientException; import java.util.List; -import java.util.concurrent.Callable; -public abstract class SQLMetadataConnector implements MetadataStorageConnector +public abstract class SQLMetadataConnector extends BaseSQLMetadataConnector implements MetadataStorageConnector { private static final Logger log = new Logger(SQLMetadataConnector.class); private static final String PAYLOAD_TYPE = "BLOB"; - static final int DEFAULT_MAX_TRIES = 10; - private final Supplier config; private final Supplier tablesConfigSupplier; - private final Predicate shouldRetry; public SQLMetadataConnector( Supplier config, @@ -118,98 +106,6 @@ public String getValidationQuery() return "SELECT 1"; } - public abstract boolean tableExists(Handle handle, String tableName); - - public T retryWithHandle( - final HandleCallback callback, - final Predicate myShouldRetry - ) - { - final Callable call = new Callable() - { - @Override - public T call() throws Exception - { - return getDBI().withHandle(callback); - } - }; - try { - return RetryUtils.retry(call, myShouldRetry, DEFAULT_MAX_TRIES); - } - catch (Exception e) { - throw Throwables.propagate(e); - } - } - - public T retryWithHandle(final HandleCallback callback) - { - return retryWithHandle(callback, shouldRetry); - } - - public T retryTransaction(final TransactionCallback callback, final int quietTries, final int maxTries) - { - final Callable call = new Callable() - { - @Override - public T call() throws Exception - { - return getDBI().inTransaction(callback); - } - }; - try { - return RetryUtils.retry(call, shouldRetry, quietTries, maxTries); - } - catch (Exception e) { - throw Throwables.propagate(e); - } - } - - public final boolean isTransientException(Throwable e) - { - return e != null && (e instanceof RetryTransactionException - || e instanceof SQLTransientException - || e instanceof SQLRecoverableException - || e instanceof UnableToObtainConnectionException - || e instanceof UnableToExecuteStatementException - || connectorIsTransientException(e) - || (e instanceof SQLException && isTransientException(e.getCause())) - || (e instanceof DBIException && isTransientException(e.getCause()))); - } - - protected boolean connectorIsTransientException(Throwable e) - { - return false; - } - - public void createTable(final String tableName, final Iterable sql) - { - try { - retryWithHandle( - new HandleCallback() - { - @Override - public Void withHandle(Handle handle) throws Exception - { - if (!tableExists(handle, tableName)) { - log.info("Creating table[%s]", tableName); - final Batch batch = handle.createBatch(); - for (String s : sql) { - batch.add(s); - } - batch.execute(); - } else { - log.info("Table[%s] already exists", tableName); - } - return null; - } - } - ); - } - catch (Exception e) { - log.warn(e, "Exception creating table"); - } - } - public void createPendingSegmentsTable(final String tableName) { createTable( @@ -446,8 +342,6 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th ); } - public abstract DBI getDBI(); - @Override public void createDataSourceTable() { 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 952f607cbaf8..39b0a8b04322 100644 --- a/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java +++ b/server/src/main/java/io/druid/server/security/AuthenticatorMapper.java @@ -55,4 +55,9 @@ public List getAuthenticatorChain() { return Lists.newArrayList(authenticatorMap.values()); } + + public Map getAuthenticatorMap() + { + return authenticatorMap; + } } 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 2c029aafe038..9a2052ca6f0d 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 Authorizer getAuthorizer(String name) { 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 240f9280562f..3a7641b44156 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 @@ 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 int hashCode() result = 31 * result + getAction().hashCode(); return result; } + + @Override + public String toString() + { + return StringUtils.format("{%s,%s}", resource, action); + } }