From 5addb33ffcdd4a6ef4ab216797625e4b9b429246 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Mon, 9 Oct 2017 14:20:23 -0700 Subject: [PATCH 1/8] Basic security extension --- distribution/pom.xml | 2 + .../extensions-core/druid-basic-security.md | 157 +++ extensions-core/druid-basic-security/pom.xml | 78 ++ .../druid/security/basic/BasicAuthConfig.java | 41 + .../druid/security/basic/BasicAuthUtils.java | 103 ++ .../basic/BasicSecurityDruidModule.java | 101 ++ .../security/basic/BasicSecurityResource.java | 372 ++++++ .../basic/BasicSecurityResourceFilter.java | 84 ++ .../BasicHTTPAuthenticator.java | 236 ++++ .../BasicRoleBasedAuthorizer.java | 112 ++ .../cli/BasicSecurityCliCommandCreator.java | 32 + .../basic/cli/CreateAuthorizationTables.java | 286 +++++ .../db/BasicSecurityStorageConnector.java | 88 ++ .../db/SQLBasicSecurityStorageConnector.java | 1109 +++++++++++++++++ .../db/derby/DerbyAuthorizationStorage.java | 65 + ...DerbySQLBasicSecurityStorageConnector.java | 174 +++ .../MySQLBasicSecurityStorageConnector.java | 175 +++ ...PostgresBasicSecurityStorageConnector.java | 138 ++ .../services/io.druid.cli.CliCommandCreator | 1 + .../io.druid.initialization.DruidModule | 1 + .../security/basic/BasicAuthUtilsTest.java | 55 + pom.xml | 1 + 22 files changed, 3411 insertions(+) create mode 100644 docs/content/development/extensions-core/druid-basic-security.md create mode 100644 extensions-core/druid-basic-security/pom.xml create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthUtils.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator create mode 100644 extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.initialization.DruidModule create mode 100644 extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java 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..5f2cf2d06faf --- /dev/null +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -0,0 +1,157 @@ +--- +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| + +### 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" +} +] +``` + +`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. \ No newline at end of file diff --git a/extensions-core/druid-basic-security/pom.xml b/extensions-core/druid-basic-security/pom.xml new file mode 100644 index 000000000000..5250e70ff4d4 --- /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 + 5.1.38 + + + org.postgresql + postgresql + 9.4.1208.jre7 + + + 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/BasicAuthConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java new file mode 100644 index 000000000000..68977b389290 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java @@ -0,0 +1,41 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BasicAuthConfig +{ + @JsonProperty + private String initialAdminPassword = "druid"; + + @JsonProperty + private String initialInternalClientPassword = "druid"; + + 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/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/BasicSecurityDruidModule.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java new file mode 100644 index 000000000000..099762b93750 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java @@ -0,0 +1,101 @@ +/* + * 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.JsonConfigProvider; +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.BasicSecurityStorageConnector; +import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +import io.druid.security.basic.db.derby.DerbySQLBasicSecurityStorageConnector; +import io.druid.security.basic.db.mysql.MySQLBasicSecurityStorageConnector; +import io.druid.security.basic.db.postgres.PostgresBasicSecurityStorageConnector; + +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) + { + JsonConfigProvider.bind(binder, "druid.auth.basic", BasicAuthConfig.class); + + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(BasicSecurityStorageConnector.class), null, "derby" + ); + PolyBind.createChoiceWithDefault( + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(SQLBasicSecurityStorageConnector.class), null, "derby" + ); + + PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + .addBinding("derby") + .to(DerbySQLBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + .addBinding("mysql") + .to(MySQLBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + .addBinding("postgresql") + .to(PostgresBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + .addBinding("postgresql") + .to(PostgresBasicSecurityStorageConnector.class) + .in(ManageLifecycle.class); + + Jerseys.addResource(binder, BasicSecurityResource.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/BasicSecurityResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java new file mode 100644 index 000000000000..fdc17c3362f1 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java @@ -0,0 +1,372 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ResourceFilters; +import io.druid.guice.annotations.Json; +import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.db.BasicSecurityStorageConnector; +import io.druid.server.security.ResourceAction; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Map; + +@Path("/druid/coordinator/v1/security") +public class BasicSecurityResource +{ + private final BasicSecurityStorageConnector dbConnector; + private final ObjectMapper jsonMapper; + + @Inject + public BasicSecurityResource( + @Json ObjectMapper jsonMapper, + BasicSecurityStorageConnector dbConnector + ) + { + this.jsonMapper = jsonMapper; + this.dbConnector = dbConnector; + } + + @GET + @Path("/users") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllUsers( + @Context HttpServletRequest req + ) + { + List> users = dbConnector.getAllUsers(); + return Response.ok(users).build(); + } + + @GET + @Path("/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getUser( + @Context HttpServletRequest req, + @PathParam("userName") final String userName + ) + { + Map user = dbConnector.getUser(userName); + List> roles = dbConnector.getRolesForUser(userName); + List> permissions = dbConnector.getPermissionsForUser(userName); + + Map userInfo = ImmutableMap.of( + "user", user, + "roles", roles, + "permissions", permissions + ); + + return Response.ok(userInfo).build(); + } + + @POST + @Path("/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response createUser( + @Context HttpServletRequest req, + @PathParam("userName") String userName + ) + { + dbConnector.createUser(userName); + return Response.ok().build(); + } + + @DELETE + @Path("/users/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteUser( + @Context HttpServletRequest req, + @PathParam("userName") String userName + ) + { + Map dbUser = dbConnector.getUser(userName); + if (dbUser == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of("error", StringUtils.format("user [%s] not found", userName))) + .build(); + } + + dbConnector.deleteUser(userName); + + return Response.ok().build(); + } + + @GET + @Path("/credentials/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getUserCredentials( + @Context HttpServletRequest req, + @PathParam("userName") final String userName + ) + { + Map credentials = dbConnector.getUserCredentials(userName); + return Response.ok(credentials).build(); + } + + @POST + @Path("/credentials/{userName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response updateUserCredentials( + @Context HttpServletRequest req, + @PathParam("userName") String userName, + String password + ) + { + Map dbUser = dbConnector.getUser(userName); + if (dbUser == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of("error", StringUtils.format("user [%s] not found", userName))) + .build(); + } + + dbConnector.setUserCredentials(userName, password.toCharArray()); + return Response.ok().build(); + } + + @GET + @Path("/roles") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAllRoles( + @Context HttpServletRequest req + ) + { + List> roles = dbConnector.getAllRoles(); + return Response.ok(roles).build(); + } + + @GET + @Path("/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getRole( + @Context HttpServletRequest req, + @PathParam("roleName") final String roleName + ) + { + Map role = dbConnector.getRole(roleName); + List> users = dbConnector.getUsersWithRole(roleName); + List> permissions = dbConnector.getPermissionsForRole(roleName); + + Map roleInfo = ImmutableMap.of( + "role", role, + "users", users, + "permissions", permissions + ); + + return Response.ok(roleInfo).build(); + } + + @POST + @Path("/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response createRole( + @Context HttpServletRequest req, + @PathParam("roleName") final String roleName + ) + { + dbConnector.createRole(roleName); + return Response.ok().build(); + } + + @DELETE + @Path("/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteRole( + @Context HttpServletRequest req, + @PathParam("roleName") String roleName + ) + { + Map dbRole = dbConnector.getRole(roleName); + if (dbRole == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of("error", StringUtils.format("role [%s] not found", roleName))) + .build(); + } + + dbConnector.deleteRole(roleName); + + return Response.ok().build(); + } + + + @POST + @Path("/users/{userName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response assignRoleToUser( + @Context HttpServletRequest req, + @PathParam("userName") String userName, + @PathParam("roleName") String roleName + ) + { + dbConnector.assignRole(userName, roleName); + return Response.ok().build(); + } + + @DELETE + @Path("/users/{userName}/roles/{roleName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response unassignRoleFromUser( + @Context HttpServletRequest req, + @PathParam("userName") String userName, + @PathParam("roleName") String roleName + ) + { + dbConnector.unassignRole(userName, roleName); + return Response.ok().build(); + } + + @POST + @Path("/roles/{roleName}/permissions") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response addPermissionsToRole( + @Context HttpServletRequest req, + @PathParam("roleName") String roleName, + List resourceActions + ) + { + Map dbRole = dbConnector.getRole(roleName); + if (dbRole == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format("role does not exist: %s", roleName) + )) + .build(); + } + + for (ResourceAction resourceAction : resourceActions) { + try { + final byte[] serializedPermission = jsonMapper.writeValueAsBytes(resourceAction); + dbConnector.addPermission(roleName, serializedPermission, "a"); + } + catch (JsonProcessingException jpe) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format( + "cannot serialize permission: %s", + resourceAction + ) + )) + .build(); + } + } + + return Response.ok().build(); + } + + @DELETE + @Path("/permissions/{permId}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response removePermissionFromRole( + @Context HttpServletRequest req, + @PathParam("roleName") String roleName, + @PathParam("permId") Integer permId + ) + { + dbConnector.deletePermission(permId); + return Response.ok().build(); + } + + @GET + @Path("/authenticationMappings/{authenticationName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response getAuthenticationMapping( + @Context HttpServletRequest req, + @PathParam("authenticationName") String authenticationName + ) + { + String authorizationName = dbConnector.getAuthorizationNameFromAuthenticationName(authenticationName); + return Response.ok(authorizationName).build(); + } + + @POST + @Path("/authenticationMappings/{authenticationName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response setAuthenticationMapping( + @Context HttpServletRequest req, + @PathParam("authenticationName") String authenticationName, + String authorizationName + ) + { + dbConnector.createAuthenticationToAuthorizationNameMapping(authenticationName, authorizationName); + return Response.ok().build(); + } + + @DELETE + @Path("/authenticationMappings/{authenticationName}") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(BasicSecurityResourceFilter.class) + public Response deleteAuthenticationMapping( + @Context HttpServletRequest req, + @PathParam("authenticationName") String authenticationName + ) + { + dbConnector.deleteAuthenticationToAuthorizationNameMapping(authenticationName); + return Response.ok().build(); + } +} 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..d530f6e71119 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -0,0 +1,84 @@ +/* + * 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.AuthorizerMapper; +import io.druid.server.security.AuthorizationUtils; +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 +{ + @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) + { + List applicablePaths = ImmutableList.of("druid/coordinator/v1/security/"); + 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..8faf6ac88b2a --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java @@ -0,0 +1,236 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.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.BasicSecurityStorageConnector; +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 BasicSecurityStorageConnector dbConnector; + private final String internalClientUsername; + private final String internalClientPassword; + private final String authorizerName; + + @JsonCreator + public BasicHTTPAuthenticator( + @JacksonInject BasicSecurityStorageConnector dbConnector, + @JsonProperty("internalClientUsername") String internalClientUsername, + @JsonProperty("internalClientPassword") String internalClientPassword, + @JsonProperty("authorizerName") String authorizerName + ) + { + this.dbConnector = dbConnector; + this.internalClientUsername = internalClientUsername; + this.internalClientPassword = internalClientPassword; + this.authorizerName = authorizerName; + } + + @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(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 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(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..25b3d67b6ab3 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -0,0 +1,112 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic.authorization; + +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.druid.java.util.common.logger.Logger; +import io.druid.security.basic.db.BasicSecurityStorageConnector; +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 Logger log = new Logger(BasicRoleBasedAuthorizer.class); + + private final BasicSecurityStorageConnector dbConnector; + + @JsonProperty + private final boolean remapAuthNames; + + @JsonCreator + public BasicRoleBasedAuthorizer( + @JacksonInject BasicSecurityStorageConnector dbConnector, + @JsonProperty("remapAuthNames") Boolean remapAuthNames + ) + { + this.dbConnector = dbConnector; + this.remapAuthNames = remapAuthNames == null ? false : remapAuthNames; + } + + @Override + public Access authorize( + AuthenticationResult authenticationResult, Resource resource, Action action + ) + { + if (authenticationResult == null) { + return new Access(false); + } + + String identity = null; + if (remapAuthNames) { + String authorizationName = dbConnector.getAuthorizationNameFromAuthenticationName( + authenticationResult.getIdentity() + ); + if (authorizationName == null) { + return new Access(false); + } else { + identity = authorizationName; + } + } else { + identity = authenticationResult.getIdentity(); + } + + List> permissions = dbConnector.getPermissionsForUser(identity); + + // maybe optimize this later + for (Map permission : permissions) { + if (permissionCheck(resource, action, permission)) { + return new Access(true); + } + } + + return new Access(false); + } + + 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 = Pattern.compile(permissionResourceName); + Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName()); + return resourceNameMatcher.matches(); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java new file mode 100644 index 000000000000..505b09acf021 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java @@ -0,0 +1,32 @@ +/* + * 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.cli; + +import io.airlift.airline.Cli; +import io.druid.cli.CliCommandCreator; + +public class BasicSecurityCliCommandCreator implements CliCommandCreator +{ + @Override + public void addCommands(Cli.CliBuilder builder) + { + builder.withGroup("tools").withCommands(CreateAuthorizationTables.class); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java new file mode 100644 index 000000000000..433200b92796 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java @@ -0,0 +1,286 @@ +/* + * 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.cli; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.inject.Binder; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Module; + +import io.airlift.airline.Command; +import io.airlift.airline.Option; +import io.druid.cli.GuiceRunnable; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.annotations.Self; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.db.BasicSecurityStorageConnector; +import io.druid.server.DruidNode; +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 java.util.List; + +@Command( + name = "authorization-init", + description = "Initialize Authorization Storage" +) +public class CreateAuthorizationTables extends GuiceRunnable +{ + private static final String DEFAULT_ADMIN_NAME = "admin"; + private static final String DEFAULT_ADMIN_ROLE = "admin"; + private static final String DEFAULT_ADMIN_PASS = "druid"; + + private static final String DEFAULT_SYSTEM_USER_NAME = "druid_system"; + private static final String DEFAULT_SYSTEM_USER_ROLE = "druid_system"; + private static final String DEFAULT_SYSTEM_USER_PASS = "druid"; + + @Option(name = "--connectURI", description = "Database JDBC connection string", required = true) + private String connectURI; + + @Option(name = "--user", description = "Database username", required = true) + private String user; + + @Option(name = "--password", description = "Database password", required = true) + private String password; + + @Option(name = "--admin-user", description = "Name of default admin to be created.", required = false) + private String adminUser; + + @Option(name = "--admin-password", description = "Password of default admin to be created.", required = false) + private String adminPassword; + + @Option(name = "--admin-role", description = "Role of default admin to be created.", required = false) + private String adminRole; + + @Option(name = "--system-user", description = "Name of internal system user to be created.", required = false) + private String systemUser; + + @Option(name = "--system-password", description = "Password of internal system user to be created.", required = false) + private String systemPassword; + + @Option(name = "--system-role", description = "Role of internal system user to be created.", required = false) + private String systemRole; + + @Option(name = "--base", description = "Base table name") + private String base; + + private static final Logger log = new Logger(CreateAuthorizationTables.class); + + public CreateAuthorizationTables() + { + super(log); + } + + @Override + protected List getModules() + { + return ImmutableList.of( + new Module() + { + @Override + public void configure(Binder binder) + { + JsonConfigProvider.bindInstance( + binder, Key.get(MetadataStorageConnectorConfig.class), new MetadataStorageConnectorConfig() + { + @Override + public String getConnectURI() + { + return connectURI; + } + + @Override + public String getUser() + { + return user; + } + + @Override + public String getPassword() + { + return password; + } + } + ); + JsonConfigProvider.bindInstance( + binder, + Key.get(DruidNode.class, Self.class), + new DruidNode("tools", "localhost", -1, null, true, false) + ); + } + } + ); + } + + @Override + public void run() + { + final Injector injector = makeInjector(); + BasicSecurityStorageConnector dbConnector = injector.getInstance(BasicSecurityStorageConnector.class); + ObjectMapper jsonMapper = injector.getInstance(ObjectMapper.class); + + dbConnector.createUserTable(); + dbConnector.createAuthenticationToAuthorizationNameMappingTable(); + dbConnector.createRoleTable(); + dbConnector.createPermissionTable(); + dbConnector.createUserRoleTable(); + dbConnector.createUserCredentialsTable(); + + setupDefaultAdmin(dbConnector, jsonMapper); + setupInternalDruidSystemUser(dbConnector, jsonMapper); + } + + private void setupInternalDruidSystemUser(BasicSecurityStorageConnector dbConnector, ObjectMapper jsonMapper) + { + if (systemUser == null) { + systemUser = DEFAULT_SYSTEM_USER_NAME; + } + + if (systemPassword == null) { + systemPassword = DEFAULT_SYSTEM_USER_PASS; + } + + if (systemRole == null) { + systemRole = DEFAULT_SYSTEM_USER_ROLE; + } + + dbConnector.createUser(systemUser); + dbConnector.createRole(systemRole); + dbConnector.assignRole(systemUser, systemRole); + + 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) { + try { + byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); + dbConnector.addPermission(systemRole, serializedPermission, null); + } + catch (JsonProcessingException jpe) { + log.error("WTF? Couldn't serialize internal druid system permission."); + } + } + + dbConnector.setUserCredentials(systemUser, systemPassword.toCharArray()); + + dbConnector.createAuthenticationToAuthorizationNameMapping(systemUser, systemUser); + } + + private void setupDefaultAdmin(BasicSecurityStorageConnector dbConnector, ObjectMapper jsonMapper) + { + if (adminUser == null) { + adminUser = DEFAULT_ADMIN_NAME; + } + + if (adminPassword == null) { + adminPassword = DEFAULT_ADMIN_PASS; + } + + if (adminRole == null) { + adminRole = DEFAULT_ADMIN_ROLE; + } + + dbConnector.createUser(adminUser); + dbConnector.createRole(adminRole); + dbConnector.assignRole(adminUser, adminRole); + + 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) { + try { + byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); + dbConnector.addPermission(adminRole, serializedPermission, null); + } + catch (JsonProcessingException jpe) { + log.error("WTF? Couldn't serialize default admin permission."); + } + } + + dbConnector.setUserCredentials(adminUser, adminPassword.toCharArray()); + + dbConnector.createAuthenticationToAuthorizationNameMapping(adminUser, adminUser); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java new file mode 100644 index 000000000000..ce6c635f896e --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java @@ -0,0 +1,88 @@ +/* + * 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 BasicSecurityStorageConnector +{ + void createUser(String userName); + + void deleteUser(String userName); + + void createRole(String roleName); + + void deleteRole(String roleName); + + void addPermission(String roleName, byte[] serializedResourceIdentifier, String action); + + void deleteAllPermissionsFromRole(String roleName); + + void deletePermission(int permissionId); + + void assignRole(String userName, String roleName); + + void unassignRole(String userName, String roleName); + + List> getAllUsers(); + + List> getAllRoles(); + + Map getUser(String userName); + + Map getRole(String roleName); + + List> getRolesForUser(String userName); + + List> getUsersWithRole(String roleName); + + List> getPermissionsForRole(String roleName); + + List> getPermissionsForUser(String userName); + + void createRoleTable(); + + void createUserTable(); + + void createPermissionTable(); + + void createUserRoleTable(); + + void createUserCredentialsTable(); + + void createAuthenticationToAuthorizationNameMappingTable(); + + void deleteAllRecords(String tableName); + + + void createAuthenticationToAuthorizationNameMapping(String authenticationName, String authorizationName); + + String getAuthorizationNameFromAuthenticationName(String authenticationName); + + void deleteAuthenticationToAuthorizationNameMapping(String authenticationName); + + + void setUserCredentials(String userName, char[] password); + + boolean checkCredentials(String userName, char[] password); + + Map getUserCredentials(String userName); +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java new file mode 100644 index 000000000000..759ede30ab79 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java @@ -0,0 +1,1109 @@ +/* + * 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.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.inject.Inject; +import io.druid.java.util.common.RetryUtils; +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.MetadataStorageConnectorConfig; +import io.druid.metadata.RetryTransactionException; +import io.druid.security.basic.BasicAuthConfig; +import io.druid.security.basic.BasicAuthUtils; +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.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.StatementContext; +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.tweak.ResultSetMapper; +import org.skife.jdbi.v2.util.StringMapper; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLRecoverableException; +import java.sql.SQLTransientException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +public abstract class SQLBasicSecurityStorageConnector implements BasicSecurityStorageConnector +{ + private static final Logger log = new Logger(SQLBasicSecurityStorageConnector.class); + + private static final String PAYLOAD_TYPE = "BLOB"; + + public static final String AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS = "authentication_authorization_name_mappings"; + public static final String USERS = "users"; + public static final String USER_CREDENTIALS = "user_credentials"; + 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"; + + public static final int DEFAULT_MAX_TRIES = 10; + + private final Supplier config; + private final BasicAuthConfig basicAuthConfig; + private final Predicate shouldRetry; + private final ObjectMapper jsonMapper; + private final PermissionsMapper permMapper; + private final UserCredentialsMapper credsMapper; + + @Inject + public SQLBasicSecurityStorageConnector( + Supplier config, + Supplier basicAuthConfigSupplier, + ObjectMapper jsonMapper + ) + { + this.config = config; + this.basicAuthConfig = basicAuthConfigSupplier.get(); + this.jsonMapper = jsonMapper; + this.permMapper = new PermissionsMapper(); + this.credsMapper = new UserCredentialsMapper(); + this.shouldRetry = new Predicate() + { + @Override + public boolean apply(Throwable e) + { + return isTransientException(e); + } + }; + } + + @LifecycleStart + public void start() + { + createUserTable(); + createAuthenticationToAuthorizationNameMappingTable(); + createRoleTable(); + createPermissionTable(); + createUserRoleTable(); + createUserCredentialsTable(); + + makeDefaultSuperuser(DEFAULT_ADMIN_NAME, basicAuthConfig.getInitialAdminPassword(), DEFAULT_ADMIN_ROLE); + makeDefaultSuperuser(DEFAULT_SYSTEM_USER_NAME, basicAuthConfig.getInitialInternalClientPassword(), DEFAULT_SYSTEM_USER_ROLE); + } + + @Override + public void createRoleTable() + { + createTable( + ROLES, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + ROLES + ) + ) + ); + } + + @Override + public void createUserTable() + { + createTable( + USERS, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + USERS + ) + ) + ); + } + + @Override + public void createUserCredentialsTable() + { + createTable( + USER_CREDENTIALS, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " user_name INTEGER 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 users(name) ON DELETE CASCADE\n" + + ")", + USER_CREDENTIALS + ) + ) + ); + } + + @Override + public void createPermissionTable() + { + createTable( + PERMISSIONS, + 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 roles(name) ON DELETE CASCADE\n" + + ")", + PERMISSIONS + ) + ) + ); + } + + @Override + public void createUserRoleTable() + { + createTable( + USER_ROLES, + 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 users(name) ON DELETE CASCADE,\n" + + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\n" + + ")", + USER_ROLES + ) + ) + ); + } + + @Override + public void createAuthenticationToAuthorizationNameMappingTable() + { + createTable( + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " authentication_name VARCHAR(255) NOT NULL, \n" + + " authorization_name VARCHAR(255) NOT NULL, \n" + + " PRIMARY KEY (authentication_name),\n" + + " FOREIGN KEY (authorization_name) REFERENCES users(name) ON DELETE CASCADE\n" + + ")", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + ); + } + + @Override + public void deleteAllRecords(String tableName) + { + throw new UnsupportedOperationException("delete all not supported yet for authorization storage"); + } + + public MetadataStorageConnectorConfig getConfig() + { + return config.get(); + } + + 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 boolean connectorIsTransientException(Throwable e) + { + return false; + } + + /** + * SQL type to use for payload data (e.g. JSON blobs). + * Must be a binary type, which values can be accessed using ResultSet.getBytes() + *

+ * The resulting string will be interpolated into the table creation statement, e.g. + * CREATE TABLE druid_table ( payload NOT NULL, ... ) + * + * @return String representing the SQL type + */ + protected String getPayloadType() + { + return PAYLOAD_TYPE; + } + + /** + * @return the string that should be used to quote string fields + */ + public abstract String getQuoteString(); + + public abstract boolean tableExists(Handle handle, String tableName); + + public abstract DBI getDBI(); + + public String getValidationQuery() + { + return "SELECT 1"; + } + + @Override + public void createUser(String userName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (name) VALUES (:user_name)", USERS + ) + ) + .bind("user_name", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public void deleteUser(String userName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE name = :userName", USERS + ) + ) + .bind("userName", userName) + .execute(); + return null; + } + } + ); + } + + @Override + public void createRole(String roleName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (name) VALUES (:roleName)", ROLES + ) + ) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void deleteRole(String roleName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE name = :roleName", ROLES + ) + ) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void addPermission(String roleName, byte[] serializedResourceIdentifier, String action) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (resource_json, role_name) VALUES (:resourceJson, :roleName)", + PERMISSIONS + ) + ) + .bind("resourceJson", serializedResourceIdentifier) + .bind("roleName", roleName) + .execute(); + + return null; + } + } + ); + } + + @Override + public void deleteAllPermissionsFromRole(String roleName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE role_name = :roleName", + PERMISSIONS + ) + ) + .bind("roleName", roleName) + .execute(); + + return null; + } + } + ); + } + + @Override + public void deletePermission(int permissionId) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE id = :permissionId", PERMISSIONS + ) + ) + .bind("permissionId", permissionId) + .execute(); + return null; + } + } + ); + } + + @Override + public void assignRole(String userName, String roleName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (user_name, role_name) VALUES (:userName, :roleName)", USER_ROLES + ) + ) + .bind("userName", userName) + .bind("roleName", roleName) + .execute(); + return null; + } + } + ); + } + + @Override + public void unassignRole(String userName, String roleName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE user_name = :userName AND role_name = :roleName", USER_ROLES + ) + ) + .bind("userName", userName) + .bind("roleName", roleName) + .execute(); + + return null; + } + } + ); + } + + @Override + public List> getAllUsers() + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM users") + ) + .list(); + } + } + ); + } + + @Override + public List> getAllRoles() + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM roles") + ) + .list(); + } + } + ); + } + + @Override + public Map getUser(String userName) + { + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM users where name = :userName") + ) + .bind("userName", userName) + .first(); + } + } + ); + } + + @Override + public Map getRole(String roleName) + { + return getDBI().inTransaction( + new TransactionCallback>() + { + @Override + public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format("SELECT * FROM roles where name = :roleName") + ) + .bind("roleName", roleName) + .first(); + } + } + ); + } + + @Override + public List> getRolesForUser(String userName) + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + List> user_roles = handle + .createQuery( + StringUtils.format( + "SELECT roles.name\n" + + "FROM roles\n" + + "JOIN user_roles\n" + + " ON user_roles.role_name = roles.name\n" + + "WHERE user_roles.user_name = :userName" + ) + ) + .bind("userName", userName) + .list(); + return user_roles; + } + } + ); + } + + @Override + public List> getUsersWithRole(String roleName) + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + List> user_roles = handle + .createQuery( + StringUtils.format( + "SELECT users.name\n" + + "FROM users\n" + + "JOIN user_roles\n" + + " ON user_roles.user_name = users.name\n" + + "WHERE user_roles.role_name = :roleName" + ) + ) + .bind("roleName", roleName) + .list(); + return user_roles; + } + } + ); + } + + 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) { + return null; + } + } + } + + @Override + public List> getPermissionsForRole(String roleName) + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + List> role_permissions = handle + .createQuery( + StringUtils.format( + "SELECT permissions.id, permissions.resource_json\n" + + "FROM permissions\n" + + "WHERE permissions.role_name = :roleName" + ) + ) + .map(permMapper) + .bind("roleName", roleName) + .list(); + return role_permissions; + } + } + ); + } + + @Override + public List> getPermissionsForUser(String userName) + { + return getDBI().inTransaction( + new TransactionCallback>>() + { + @Override + public List> inTransaction(Handle handle, TransactionStatus transactionStatus) + throws Exception + { + List> user_permissions = handle + .createQuery( + StringUtils.format( + "SELECT permissions.id, permissions.resource_json, roles.name\n" + + "FROM permissions\n" + + "JOIN roles\n" + + " ON permissions.role_name = roles.name\n" + + "JOIN user_roles\n" + + " ON user_roles.role_name = roles.name\n" + + "WHERE user_roles.user_name = :userName" + ) + ) + .map(permMapper) + .bind("userName", userName) + .list(); + return user_permissions; + } + } + ); + } + + @Override + public void createAuthenticationToAuthorizationNameMapping( + String authenticationName, String authorizationName + ) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + String existingMapping = handle + .createQuery( + StringUtils.format( + "SELECT authorization_name FROM %1$s WHERE authentication_name = :authenticationName", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + .bind("authenticationName", authenticationName) + .map(StringMapper.FIRST) + .first(); + + if (existingMapping == null) { + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (authentication_name, authorization_name) VALUES (:authenticationName, :authorizationName)", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + .bind("authenticationName", authenticationName) + .bind("authorizationName", authorizationName) + .execute(); + } else { + handle.createStatement( + StringUtils.format( + "UPDATE %1$s SET authorization_name = :authorizationName " + + "WHERE authentication_name = :authenticationName", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + .bind("authenticationName", authenticationName) + .bind("authorizationName", authorizationName) + .execute(); + } + + return null; + } + } + ); + } + + @Override + public String getAuthorizationNameFromAuthenticationName(String authenticationName) + { + return getDBI().inTransaction( + new TransactionCallback() + { + @Override + public String inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + return handle + .createQuery( + StringUtils.format( + "SELECT authorization_name FROM %1$s WHERE authentication_name = :authenticationName", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + .bind("authenticationName", authenticationName) + .map(StringMapper.FIRST) + .first(); + } + } + ); + } + + @Override + public void deleteAuthenticationToAuthorizationNameMapping(String authenticationName) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + handle.createStatement( + StringUtils.format( + "DELETE FROM %1$s WHERE authentication_name = :authenticationName", + AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS + ) + ) + .bind("authenticationName", authenticationName) + .execute(); + return null; + } + } + ); + } + + 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 + ); + } + } + + + @Override + public Map getUserCredentials(String userName) + { + 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 user_name = :userName", USER_CREDENTIALS) + ) + .map(credsMapper) + .bind("userName", userName) + .first(); + } + } + ); + } + + @Override + public void setUserCredentials(String userName, char[] password) + { + getDBI().inTransaction( + new TransactionCallback() + { + @Override + public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception + { + Map existingMapping = handle + .createQuery( + StringUtils.format( + "SELECT user_name FROM %1$s WHERE user_name = :userName", + USER_CREDENTIALS + ) + ) + .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)", + USER_CREDENTIALS + ) + ) + .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", + USER_CREDENTIALS + ) + ) + .bind("userName", userName) + .bind("salt", salt) + .bind("hash", hash) + .bind("iterations", iterations) + .execute(); + } + + return null; + } + } + ); + } + + @Override + public boolean checkCredentials(String userName, char[] password) + { + 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", + USER_CREDENTIALS + ) + ) + .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 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()))); + } + + public void createTable(final String tableName, final Iterable sql) + { + if (!config.get().isCreateTables()) { + return; + } + + 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 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); + } + } + + private void makeDefaultSuperuser(String username, String password, String role) + { + if (getUser(username) != null) { + return; + } + + createUser(username); + createRole(role); + assignRole(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) { + try { + byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); + addPermission(role, serializedPermission, null); + } + catch (JsonProcessingException jpe) { + log.error("WTF? Couldn't serialize default superuser permission."); + } + } + + setUserCredentials(username, password.toCharArray()); + + createAuthenticationToAuthorizationNameMapping(username, username); + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java new file mode 100644 index 000000000000..19466a46aef0 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java @@ -0,0 +1,65 @@ +/* + * 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.Throwables; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorageConnectorConfig; +import org.apache.derby.drda.NetworkServerControl; + +import java.net.InetAddress; + +public class DerbyAuthorizationStorage +{ + private static final Logger log = new Logger(DerbyAuthorizationStorage.class); + + private final NetworkServerControl server; + + public DerbyAuthorizationStorage(MetadataStorageConnectorConfig config) + { + try { + this.server = new NetworkServerControl(InetAddress.getByName(config.getHost()), config.getPort()); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + + public void start() + { + try { + log.info("Starting Derby Authorization Storage"); + server.start(null); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } + + public void stop() + { + try { + log.info("Stopping Derby Authorization Storage"); + server.shutdown(); + } + catch (Exception e) { + throw Throwables.propagate(e); + } + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java new file mode 100644 index 000000000000..8f45b6a1edc6 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java @@ -0,0 +1,174 @@ +/* + * 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 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.MetadataStorageConnectorConfig; +import io.druid.security.basic.BasicAuthConfig; +import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +import org.apache.commons.dbcp2.BasicDataSource; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +@ManageLifecycle +public class DerbySQLBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +{ + private static final Logger log = new Logger(DerbySQLBasicSecurityStorageConnector.class); + + private final DBI dbi; + private final DerbyAuthorizationStorage storage; + private static final String QUOTE_STRING = "\\\""; + + @Inject + public DerbySQLBasicSecurityStorageConnector( + Supplier config, + Supplier basicAuthConfigSupplier, + ObjectMapper jsonMapper + ) + { + super(config, basicAuthConfigSupplier, jsonMapper); + + final BasicDataSource datasource = getDatasource(); + datasource.setDriverClassLoader(getClass().getClassLoader()); + datasource.setDriverClassName("org.apache.derby.jdbc.ClientDriver"); + + this.dbi = new DBI(datasource); + this.storage = new DerbyAuthorizationStorage(config.get()); + log.info("Derby connector instantiated with auth storage [%s].", this.storage.getClass().getName()); + } + + @Override + @LifecycleStart + public void start() + { + storage.start(); + super.start(); + } + + @Override + public void createRoleTable() + { + createTable( + ROLES, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name)\n" + + ")", + ROLES + ) + ) + ); + } + + @Override + public void createUserTable() + { + createTable( + USERS, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name)\n" + + ")", + USERS + ) + ) + ); + } + + @Override + public void createPermissionTable() + { + createTable( + PERMISSIONS, + 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 roles(name) ON DELETE CASCADE\n" + + ")", + PERMISSIONS + ) + ) + ); + } + + @Override + public void createUserCredentialsTable() + { + createTable( + USER_CREDENTIALS, + 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 users(name) ON DELETE CASCADE\n" + + ")", + USER_CREDENTIALS + ) + ) + ); + } + + + @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 String getQuoteString() + { + return QUOTE_STRING; + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java new file mode 100644 index 000000000000..69d49e4a0077 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java @@ -0,0 +1,175 @@ +/* + * 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 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.BasicAuthConfig; +import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +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 MySQLBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +{ + private static final Logger log = new Logger(MySQLBasicSecurityStorageConnector.class); + + private final DBI dbi; + private static final String QUOTE_STRING = "`"; + + @Inject + public MySQLBasicSecurityStorageConnector( + Supplier config, + Supplier basicAuthConfigSupplier, + ObjectMapper jsonMapper + ) + { + super(config, basicAuthConfigSupplier, 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() + { + createTable( + ROLES, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + ROLES + ) + ) + ); + } + + @Override + public void createUserTable() + { + createTable( + USERS, + ImmutableList.of( + StringUtils.format( + "CREATE TABLE %1$s (\n" + + " name VARCHAR(255) NOT NULL,\n" + + " PRIMARY KEY (name),\n" + + " UNIQUE (name)\n" + + ")", + USERS + ) + ) + ); + } + + @Override + public void createPermissionTable() + { + createTable( + PERMISSIONS, + 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 roles(name) ON DELETE CASCADE\n" + + ")", + PERMISSIONS + ) + ) + ); + } + + @Override + public void createUserCredentialsTable() + { + createTable( + USER_CREDENTIALS, + 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 users(name) ON DELETE CASCADE\n" + + ")", + USER_CREDENTIALS + ) + ) + ); + } + + + @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 String getQuoteString() + { + return QUOTE_STRING; + } + + @Override + public DBI getDBI() + { + return dbi; + } +} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java new file mode 100644 index 000000000000..a9341551303e --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java @@ -0,0 +1,138 @@ +/* + * 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 io.druid.java.util.common.StringUtils; +import io.druid.java.util.common.logger.Logger; +import io.druid.metadata.MetadataStorageConnectorConfig; +import io.druid.security.basic.BasicAuthConfig; +import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +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 PostgresBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +{ + private static final Logger log = new Logger(PostgresBasicSecurityStorageConnector.class); + + private static final String PAYLOAD_TYPE = "BYTEA"; + private static final String SERIAL_TYPE = "BIGSERIAL"; + private static final String QUOTE_STRING = "\\\""; + public static final int DEFAULT_STREAMING_RESULT_SIZE = 100; + + private final DBI dbi; + + @Inject + public PostgresBasicSecurityStorageConnector( + Supplier config, + Supplier basicAuthConfigSupplier, + ObjectMapper jsonMapper + ) + { + super(config, basicAuthConfigSupplier, 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 String getQuoteString() + { + return QUOTE_STRING; + } + + @Override + public DBI getDBI() + { + return dbi; + } + + @Override + protected String getPayloadType() + { + return PAYLOAD_TYPE; + } + + @Override + public void createPermissionTable() + { + createTable( + PERMISSIONS, + 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 roles(name) ON DELETE CASCADE\n" + + ")", + PERMISSIONS + ) + ) + ); + } + + @Override + public void createUserCredentialsTable() + { + createTable( + USER_CREDENTIALS, + 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 users(name) ON DELETE CASCADE\n" + + ")", + USER_CREDENTIALS + ) + ) + ); + } +} diff --git a/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator b/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator new file mode 100644 index 000000000000..3ff5ce132e60 --- /dev/null +++ b/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator @@ -0,0 +1 @@ +io.druid.security.basic.cli.BasicSecurityCliCommandCreator \ No newline at end of file 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/test/java/io/druid/security/basic/BasicAuthUtilsTest.java b/extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java new file mode 100644 index 000000000000..cd924b902c92 --- /dev/null +++ b/extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.druid.security.basic; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.inject.Binder; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provides; +import io.druid.guice.ConfigModule; +import io.druid.guice.DruidGuiceExtensions; +import io.druid.guice.JsonConfigProvider; +import io.druid.guice.LazySingleton; +import io.druid.guice.PropertiesModule; +import io.druid.jackson.DefaultObjectMapper; +import io.druid.java.util.common.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Base64; +import java.util.Properties; + +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/pom.xml b/pom.xml index 6029a1393305..48d34b8d3d11 100644 --- a/pom.xml +++ b/pom.xml @@ -116,6 +116,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 From aac0f92940f1c836004b9116edd7917edd21fa99 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Fri, 20 Oct 2017 11:22:07 -0700 Subject: [PATCH 2/8] PR comments --- .../extensions-core/druid-basic-security.md | 2 +- extensions-core/druid-basic-security/pom.xml | 4 +- .../BasicSecurityDBResourceException.java | 34 + .../security/basic/BasicSecurityResource.java | 362 +++++++---- .../basic/BasicSecurityResourceFilter.java | 5 +- .../BasicRoleBasedAuthorizer.java | 30 +- .../basic/cli/CreateAuthorizationTables.java | 22 +- .../db/BasicSecurityStorageConnector.java | 17 +- .../db/SQLBasicSecurityStorageConnector.java | 449 +++++-------- .../db/derby/DerbyAuthorizationStorage.java | 65 -- ...DerbySQLBasicSecurityStorageConnector.java | 29 +- .../MySQLBasicSecurityStorageConnector.java | 7 - ...PostgresBasicSecurityStorageConnector.java | 17 - .../druid/security}/BasicAuthUtilsTest.java | 20 +- .../security/BasicSecurityResourceTest.java | 589 ++++++++++++++++++ .../SQLBasicSecurityStorageConnectorTest.java | 363 +++++++++++ .../db/TestDerbySecurityConnector.java | 142 +++++ .../mysql-metadata-storage/pom.xml | 2 +- .../postgresql-metadata-storage/pom.xml | 2 +- pom.xml | 2 + .../metadata/BaseSQLMetadataConnector.java | 121 ++++ .../druid/metadata/SQLMetadataConnector.java | 87 +-- .../druid/server/security/ResourceAction.java | 7 + 23 files changed, 1697 insertions(+), 681 deletions(-) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDBResourceException.java delete mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java rename extensions-core/druid-basic-security/{test/java/io/druid/security/basic => src/test/java/io/druid/security}/BasicAuthUtilsTest.java (69%) create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbySecurityConnector.java create mode 100644 server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java diff --git a/docs/content/development/extensions-core/druid-basic-security.md b/docs/content/development/extensions-core/druid-basic-security.md index 5f2cf2d06faf..d736b1364d23 100644 --- a/docs/content/development/extensions-core/druid-basic-security.md +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -2,7 +2,7 @@ layout: doc_page --- -# Druid-Basic-Security +# Druid Basic Security This extension adds: - an Authenticator which supports [HTTP Basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) diff --git a/extensions-core/druid-basic-security/pom.xml b/extensions-core/druid-basic-security/pom.xml index 5250e70ff4d4..75f8c4de1201 100644 --- a/extensions-core/druid-basic-security/pom.xml +++ b/extensions-core/druid-basic-security/pom.xml @@ -50,12 +50,12 @@ mysql mysql-connector-java - 5.1.38 + ${mysql.version} org.postgresql postgresql - 9.4.1208.jre7 + ${postgresql.version} org.jdbi 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/BasicSecurityResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java index fdc17c3362f1..51c2be314d76 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java @@ -19,15 +19,12 @@ package io.druid.security.basic; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; -import io.druid.guice.annotations.Json; -import io.druid.java.util.common.StringUtils; import io.druid.security.basic.db.BasicSecurityStorageConnector; import io.druid.server.security.ResourceAction; +import org.skife.jdbi.v2.exceptions.CallbackFailedException; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -43,22 +40,27 @@ import java.util.List; import java.util.Map; +/** + * Configuration resource for users, roles, and permissions. + */ @Path("/druid/coordinator/v1/security") public class BasicSecurityResource { private final BasicSecurityStorageConnector dbConnector; - private final ObjectMapper jsonMapper; @Inject public BasicSecurityResource( - @Json ObjectMapper jsonMapper, BasicSecurityStorageConnector dbConnector ) { - this.jsonMapper = jsonMapper; this.dbConnector = dbConnector; } + /** + * @param req HTTP request + * + * @return List of all users + */ @GET @Path("/users") @Produces(MediaType.APPLICATION_JSON) @@ -72,6 +74,12 @@ public Response getAllUsers( 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("/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @@ -82,19 +90,32 @@ public Response getUser( @PathParam("userName") final String userName ) { - Map user = dbConnector.getUser(userName); - List> roles = dbConnector.getRolesForUser(userName); - List> permissions = dbConnector.getPermissionsForUser(userName); - - Map userInfo = ImmutableMap.of( - "user", user, - "roles", roles, - "permissions", permissions - ); - - return Response.ok(userInfo).build(); + try { + Map user = dbConnector.getUser(userName); + List> roles = dbConnector.getRolesForUser(userName); + List> permissions = dbConnector.getPermissionsForUser(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("/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @@ -105,10 +126,24 @@ public Response createUser( @PathParam("userName") String userName ) { - dbConnector.createUser(userName); - return Response.ok().build(); + + try { + dbConnector.createUser(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("/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @@ -119,18 +154,23 @@ public Response deleteUser( @PathParam("userName") String userName ) { - Map dbUser = dbConnector.getUser(userName); - if (dbUser == null) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of("error", StringUtils.format("user [%s] not found", userName))) - .build(); + try { + dbConnector.deleteUser(userName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); } - - dbConnector.deleteUser(userName); - - return Response.ok().build(); } + /** + * Get credential information of user + * + * @param req HTTP request + * @param userName Name of user + * + * @return salt, hash, and number of iterations for this user's credentials, 400 error if user doesn't exist + */ @GET @Path("/credentials/{userName}") @Produces(MediaType.APPLICATION_JSON) @@ -141,10 +181,24 @@ public Response getUserCredentials( @PathParam("userName") final String userName ) { - Map credentials = dbConnector.getUserCredentials(userName); - return Response.ok(credentials).build(); + try { + Map credentials = dbConnector.getUserCredentials(userName); + return Response.ok(credentials).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("/credentials/{userName}") @Produces(MediaType.APPLICATION_JSON) @@ -156,17 +210,20 @@ public Response updateUserCredentials( String password ) { - Map dbUser = dbConnector.getUser(userName); - if (dbUser == null) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of("error", StringUtils.format("user [%s] not found", userName))) - .build(); + try { + dbConnector.setUserCredentials(userName, password.toCharArray()); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); } - - dbConnector.setUserCredentials(userName, password.toCharArray()); - return Response.ok().build(); } + /** + * @param req HTTP request + * + * @return List of all roles + */ @GET @Path("/roles") @Produces(MediaType.APPLICATION_JSON) @@ -180,6 +237,14 @@ public Response getAllRoles( 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("/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @@ -190,19 +255,32 @@ public Response getRole( @PathParam("roleName") final String roleName ) { - Map role = dbConnector.getRole(roleName); - List> users = dbConnector.getUsersWithRole(roleName); - List> permissions = dbConnector.getPermissionsForRole(roleName); - - Map roleInfo = ImmutableMap.of( - "role", role, - "users", users, - "permissions", permissions - ); - - return Response.ok(roleInfo).build(); + try { + Map role = dbConnector.getRole(roleName); + List> users = dbConnector.getUsersWithRole(roleName); + List> permissions = dbConnector.getPermissionsForRole(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("/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @@ -213,10 +291,23 @@ public Response createRole( @PathParam("roleName") final String roleName ) { - dbConnector.createRole(roleName); - return Response.ok().build(); + try { + dbConnector.createRole(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("/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @@ -227,19 +318,24 @@ public Response deleteRole( @PathParam("roleName") String roleName ) { - Map dbRole = dbConnector.getRole(roleName); - if (dbRole == null) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of("error", StringUtils.format("role [%s] not found", roleName))) - .build(); + try { + dbConnector.deleteRole(roleName); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); } - - dbConnector.deleteRole(roleName); - - return Response.ok().build(); } - + /** + * 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("/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @@ -251,10 +347,24 @@ public Response assignRoleToUser( @PathParam("roleName") String roleName ) { - dbConnector.assignRole(userName, roleName); - return Response.ok().build(); + try { + dbConnector.assignRole(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("/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @@ -266,10 +376,24 @@ public Response unassignRoleFromUser( @PathParam("roleName") String roleName ) { - dbConnector.unassignRole(userName, roleName); - return Response.ok().build(); + try { + dbConnector.unassignRole(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("/roles/{roleName}/permissions") @Produces(MediaType.APPLICATION_JSON) @@ -281,92 +405,56 @@ public Response addPermissionsToRole( List resourceActions ) { - Map dbRole = dbConnector.getRole(roleName); - if (dbRole == null) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of( - "error", - StringUtils.format("role does not exist: %s", roleName) - )) - .build(); - } - - for (ResourceAction resourceAction : resourceActions) { - try { - final byte[] serializedPermission = jsonMapper.writeValueAsBytes(resourceAction); - dbConnector.addPermission(roleName, serializedPermission, "a"); - } - catch (JsonProcessingException jpe) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of( - "error", - StringUtils.format( - "cannot serialize permission: %s", - resourceAction - ) - )) - .build(); + try { + for (ResourceAction resourceAction : resourceActions) { + dbConnector.addPermission(roleName, resourceAction); } - } - return Response.ok().build(); + 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("/permissions/{permId}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) - public Response removePermissionFromRole( + public Response deletePermission( @Context HttpServletRequest req, - @PathParam("roleName") String roleName, @PathParam("permId") Integer permId ) { - dbConnector.deletePermission(permId); - return Response.ok().build(); - } - - @GET - @Path("/authenticationMappings/{authenticationName}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) - public Response getAuthenticationMapping( - @Context HttpServletRequest req, - @PathParam("authenticationName") String authenticationName - ) - { - String authorizationName = dbConnector.getAuthorizationNameFromAuthenticationName(authenticationName); - return Response.ok(authorizationName).build(); - } - - @POST - @Path("/authenticationMappings/{authenticationName}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) - public Response setAuthenticationMapping( - @Context HttpServletRequest req, - @PathParam("authenticationName") String authenticationName, - String authorizationName - ) - { - dbConnector.createAuthenticationToAuthorizationNameMapping(authenticationName, authorizationName); - return Response.ok().build(); + try { + dbConnector.deletePermission(permId); + return Response.ok().build(); + } + catch (CallbackFailedException cfe) { + return makeResponseForCallbackFailedException(cfe); + } } - @DELETE - @Path("/authenticationMappings/{authenticationName}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) - public Response deleteAuthenticationMapping( - @Context HttpServletRequest req, - @PathParam("authenticationName") String authenticationName - ) + private static Response makeResponseForCallbackFailedException(CallbackFailedException cfe) { - dbConnector.deleteAuthenticationToAuthorizationNameMapping(authenticationName); - return Response.ok().build(); + 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/BasicSecurityResourceFilter.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java index d530f6e71119..c05d7fe13f1a 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -37,6 +37,10 @@ public class BasicSecurityResourceFilter extends AbstractResourceFilter { + private static final List applicablePaths = ImmutableList.of( + "druid/coordinator/v1/security/" + ); + @Inject public BasicSecurityResourceFilter( AuthorizerMapper authorizerMapper @@ -73,7 +77,6 @@ public ContainerRequest filter(ContainerRequest request) @Override public boolean isApplicable(String requestPath) { - List applicablePaths = ImmutableList.of("druid/coordinator/v1/security/"); for (String path : applicablePaths) { if (requestPath.startsWith(path) && !requestPath.equals(path)) { return true; diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java index 25b3d67b6ab3..23d15a3fb72a 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -21,9 +21,8 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; -import io.druid.java.util.common.logger.Logger; +import io.druid.java.util.common.IAE; import io.druid.security.basic.db.BasicSecurityStorageConnector; import io.druid.server.security.Access; import io.druid.server.security.Action; @@ -40,21 +39,14 @@ @JsonTypeName("basic") public class BasicRoleBasedAuthorizer implements Authorizer { - private static final Logger log = new Logger(BasicRoleBasedAuthorizer.class); - private final BasicSecurityStorageConnector dbConnector; - @JsonProperty - private final boolean remapAuthNames; - @JsonCreator public BasicRoleBasedAuthorizer( - @JacksonInject BasicSecurityStorageConnector dbConnector, - @JsonProperty("remapAuthNames") Boolean remapAuthNames + @JacksonInject BasicSecurityStorageConnector dbConnector ) { this.dbConnector = dbConnector; - this.remapAuthNames = remapAuthNames == null ? false : remapAuthNames; } @Override @@ -63,24 +55,10 @@ public Access authorize( ) { if (authenticationResult == null) { - return new Access(false); - } - - String identity = null; - if (remapAuthNames) { - String authorizationName = dbConnector.getAuthorizationNameFromAuthenticationName( - authenticationResult.getIdentity() - ); - if (authorizationName == null) { - return new Access(false); - } else { - identity = authorizationName; - } - } else { - identity = authenticationResult.getIdentity(); + throw new IAE("WTF? authenticationResult should never be null."); } - List> permissions = dbConnector.getPermissionsForUser(identity); + List> permissions = dbConnector.getPermissionsForUser(authenticationResult.getIdentity()); // maybe optimize this later for (Map permission : permissions) { diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java index 433200b92796..85dde6faf580 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java @@ -19,7 +19,6 @@ package io.druid.security.basic.cli; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -144,7 +143,6 @@ public void run() ObjectMapper jsonMapper = injector.getInstance(ObjectMapper.class); dbConnector.createUserTable(); - dbConnector.createAuthenticationToAuthorizationNameMappingTable(); dbConnector.createRoleTable(); dbConnector.createPermissionTable(); dbConnector.createUserRoleTable(); @@ -205,18 +203,10 @@ private void setupInternalDruidSystemUser(BasicSecurityStorageConnector dbConnec List resActs = Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); for (ResourceAction resAct : resActs) { - try { - byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); - dbConnector.addPermission(systemRole, serializedPermission, null); - } - catch (JsonProcessingException jpe) { - log.error("WTF? Couldn't serialize internal druid system permission."); - } + dbConnector.addPermission(systemRole, resAct); } dbConnector.setUserCredentials(systemUser, systemPassword.toCharArray()); - - dbConnector.createAuthenticationToAuthorizationNameMapping(systemUser, systemUser); } private void setupDefaultAdmin(BasicSecurityStorageConnector dbConnector, ObjectMapper jsonMapper) @@ -270,17 +260,9 @@ private void setupDefaultAdmin(BasicSecurityStorageConnector dbConnector, Object List resActs = Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); for (ResourceAction resAct : resActs) { - try { - byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); - dbConnector.addPermission(adminRole, serializedPermission, null); - } - catch (JsonProcessingException jpe) { - log.error("WTF? Couldn't serialize default admin permission."); - } + dbConnector.addPermission(adminRole, resAct); } dbConnector.setUserCredentials(adminUser, adminPassword.toCharArray()); - - dbConnector.createAuthenticationToAuthorizationNameMapping(adminUser, adminUser); } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java index ce6c635f896e..e10a1c5e08d8 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java @@ -19,9 +19,14 @@ package io.druid.security.basic.db; +import io.druid.server.security.ResourceAction; + import java.util.List; import java.util.Map; +/** + * Interface for classes that provide access to a database that contains user/role/permission/credential information. + */ public interface BasicSecurityStorageConnector { void createUser(String userName); @@ -32,7 +37,7 @@ public interface BasicSecurityStorageConnector void deleteRole(String roleName); - void addPermission(String roleName, byte[] serializedResourceIdentifier, String action); + void addPermission(String roleName, ResourceAction resourceAction); void deleteAllPermissionsFromRole(String roleName); @@ -68,18 +73,8 @@ public interface BasicSecurityStorageConnector void createUserCredentialsTable(); - void createAuthenticationToAuthorizationNameMappingTable(); - void deleteAllRecords(String tableName); - - void createAuthenticationToAuthorizationNameMapping(String authenticationName, String authorizationName); - - String getAuthorizationNameFromAuthenticationName(String authenticationName); - - void deleteAuthenticationToAuthorizationNameMapping(String authenticationName); - - void setUserCredentials(String userName, char[] password); boolean checkCredentials(String userName, char[] password); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java index 759ede30ab79..3764650d5723 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java @@ -23,60 +23,57 @@ import com.fasterxml.jackson.databind.ObjectMapper; 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 com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.inject.Inject; -import io.druid.java.util.common.RetryUtils; +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.java.util.common.logger.Logger; +import io.druid.metadata.BaseSQLMetadataConnector; import io.druid.metadata.MetadataStorageConnectorConfig; -import io.druid.metadata.RetryTransactionException; import io.druid.security.basic.BasicAuthConfig; import io.druid.security.basic.BasicAuthUtils; +import io.druid.security.basic.BasicSecurityDBResourceException; 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.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.StatementContext; 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.tweak.ResultSetMapper; -import org.skife.jdbi.v2.util.StringMapper; +import org.skife.jdbi.v2.util.IntegerMapper; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLRecoverableException; -import java.sql.SQLTransientException; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; -public abstract class SQLBasicSecurityStorageConnector implements BasicSecurityStorageConnector +/** + * Base class for BasicSecurityStorageConnector implementations that interface with a database using SQL. + */ +public abstract class SQLBasicSecurityStorageConnector + extends BaseSQLMetadataConnector + implements BasicSecurityStorageConnector { - private static final Logger log = new Logger(SQLBasicSecurityStorageConnector.class); - - private static final String PAYLOAD_TYPE = "BLOB"; - - public static final String AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS = "authentication_authorization_name_mappings"; public static final String USERS = "users"; public static final String USER_CREDENTIALS = "user_credentials"; public static final String PERMISSIONS = "permissions"; public static final String ROLES = "roles"; public static final String USER_ROLES = "user_roles"; + public static final List TABLE_NAMES = Lists.newArrayList( + USER_CREDENTIALS, + USER_ROLES, + PERMISSIONS, + ROLES, + USERS + ); private static final String DEFAULT_ADMIN_NAME = "admin"; private static final String DEFAULT_ADMIN_ROLE = "admin"; @@ -84,11 +81,8 @@ public abstract class SQLBasicSecurityStorageConnector implements BasicSecurityS private static final String DEFAULT_SYSTEM_USER_NAME = "druid_system"; private static final String DEFAULT_SYSTEM_USER_ROLE = "druid_system"; - public static final int DEFAULT_MAX_TRIES = 10; - private final Supplier config; private final BasicAuthConfig basicAuthConfig; - private final Predicate shouldRetry; private final ObjectMapper jsonMapper; private final PermissionsMapper permMapper; private final UserCredentialsMapper credsMapper; @@ -119,7 +113,6 @@ public boolean apply(Throwable e) public void start() { createUserTable(); - createAuthenticationToAuthorizationNameMappingTable(); createRoleTable(); createPermissionTable(); createUserRoleTable(); @@ -173,7 +166,7 @@ public void createUserCredentialsTable() ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" - + " user_name INTEGER NOT NULL, \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" @@ -224,25 +217,6 @@ public void createUserRoleTable() ); } - @Override - public void createAuthenticationToAuthorizationNameMappingTable() - { - createTable( - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS, - ImmutableList.of( - StringUtils.format( - "CREATE TABLE %1$s (\n" - + " authentication_name VARCHAR(255) NOT NULL, \n" - + " authorization_name VARCHAR(255) NOT NULL, \n" - + " PRIMARY KEY (authentication_name),\n" - + " FOREIGN KEY (authorization_name) REFERENCES users(name) ON DELETE CASCADE\n" - + ")", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - ); - } - @Override public void deleteAllRecords(String tableName) { @@ -270,34 +244,6 @@ protected BasicDataSource getDatasource() return dataSource; } - protected boolean connectorIsTransientException(Throwable e) - { - return false; - } - - /** - * SQL type to use for payload data (e.g. JSON blobs). - * Must be a binary type, which values can be accessed using ResultSet.getBytes() - *

- * The resulting string will be interpolated into the table creation statement, e.g. - * CREATE TABLE druid_table ( payload NOT NULL, ... ) - * - * @return String representing the SQL type - */ - protected String getPayloadType() - { - return PAYLOAD_TYPE; - } - - /** - * @return the string that should be used to quote string fields - */ - public abstract String getQuoteString(); - - public abstract boolean tableExists(Handle handle, String tableName); - - public abstract DBI getDBI(); - public String getValidationQuery() { return "SELECT 1"; @@ -312,6 +258,11 @@ public void createUser(String userName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int count = getUserCountInTransaction(handle, userName); + if (count != 0) { + throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); + } + handle.createStatement( StringUtils.format( "INSERT INTO %1$s (name) VALUES (:user_name)", USERS @@ -334,6 +285,10 @@ public void deleteUser(String userName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int count = getUserCountInTransaction(handle, userName); + if (count == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } handle.createStatement( StringUtils.format( "DELETE FROM %1$s WHERE name = :userName", USERS @@ -356,6 +311,10 @@ public void createRole(String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int count = getRoleCountInTransaction(handle, roleName); + if (count != 0) { + throw new BasicSecurityDBResourceException("Role [%s] already exists.", roleName); + } handle.createStatement( StringUtils.format( "INSERT INTO %1$s (name) VALUES (:roleName)", ROLES @@ -378,6 +337,10 @@ public void deleteRole(String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int count = getRoleCountInTransaction(handle, roleName); + if (count == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } handle.createStatement( StringUtils.format( "DELETE FROM %1$s WHERE name = :roleName", ROLES @@ -392,7 +355,7 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void addPermission(String roleName, byte[] serializedResourceIdentifier, String action) + public void addPermission(String roleName, ResourceAction resourceAction) { getDBI().inTransaction( new TransactionCallback() @@ -400,17 +363,28 @@ public void addPermission(String roleName, byte[] serializedResourceIdentifier, @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - handle.createStatement( - StringUtils.format( - "INSERT INTO %1$s (resource_json, role_name) VALUES (:resourceJson, :roleName)", - PERMISSIONS - ) - ) - .bind("resourceJson", serializedResourceIdentifier) - .bind("roleName", roleName) - .execute(); + int roleCount = getRoleCountInTransaction(handle, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } - return null; + try { + byte[] serializedResourceAction = jsonMapper.writeValueAsBytes(resourceAction); + handle.createStatement( + StringUtils.format( + "INSERT INTO %1$s (resource_json, role_name) VALUES (:resourceJson, :roleName)", + PERMISSIONS + ) + ) + .bind("resourceJson", serializedResourceAction) + .bind("roleName", roleName) + .execute(); + + return null; + } + catch (JsonProcessingException jpe) { + throw new IAE(jpe, "Could not serialize resourceAction [%s].", resourceAction); + } } } ); @@ -425,6 +399,11 @@ public void deleteAllPermissionsFromRole(String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int roleCount = getRoleCountInTransaction(handle, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + handle.createStatement( StringUtils.format( "DELETE FROM %1$s WHERE role_name = :roleName", @@ -449,6 +428,10 @@ public void deletePermission(int permissionId) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int permCount = getPermissionCountInTransaction(handle, 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", PERMISSIONS @@ -471,6 +454,22 @@ public void assignRole(String userName, String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int userCount = getUserCountInTransaction(handle, userName); + int roleCount = getRoleCountInTransaction(handle, 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 = getUserRoleMappingCountInTransaction(handle, 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)", USER_ROLES @@ -494,6 +493,22 @@ public void unassignRole(String userName, String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int userCount = getUserCountInTransaction(handle, userName); + int roleCount = getRoleCountInTransaction(handle, 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 = getUserRoleMappingCountInTransaction(handle, 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", USER_ROLES @@ -599,7 +614,12 @@ public List> getRolesForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - List> user_roles = handle + int userCount = getUserCountInTransaction(handle, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + return handle .createQuery( StringUtils.format( "SELECT roles.name\n" @@ -611,7 +631,6 @@ public List> inTransaction(Handle handle, TransactionStatus ) .bind("userName", userName) .list(); - return user_roles; } } ); @@ -627,7 +646,12 @@ public List> getUsersWithRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - List> user_roles = handle + int roleCount = getRoleCountInTransaction(handle, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + return handle .createQuery( StringUtils.format( "SELECT users.name\n" @@ -639,7 +663,6 @@ public List> inTransaction(Handle handle, TransactionStatus ) .bind("roleName", roleName) .list(); - return user_roles; } } ); @@ -651,7 +674,6 @@ private class PermissionsMapper implements ResultSetMapper> public Map map(int index, ResultSet resultSet, StatementContext context) throws SQLException { - int id = resultSet.getInt("id"); byte[] resourceJson = resultSet.getBytes("resource_json"); try { @@ -662,7 +684,7 @@ public Map map(int index, ResultSet resultSet, StatementContext ); } catch (IOException ioe) { - return null; + throw new RuntimeException(ioe); } } } @@ -677,7 +699,12 @@ public List> getPermissionsForRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - List> role_permissions = handle + int roleCount = getRoleCountInTransaction(handle, roleName); + if (roleCount == 0) { + throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); + } + + return handle .createQuery( StringUtils.format( "SELECT permissions.id, permissions.resource_json\n" @@ -688,7 +715,6 @@ public List> inTransaction(Handle handle, TransactionStatus .map(permMapper) .bind("roleName", roleName) .list(); - return role_permissions; } } ); @@ -704,7 +730,12 @@ public List> getPermissionsForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - List> user_permissions = handle + int userCount = getUserCountInTransaction(handle, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + + return handle .createQuery( StringUtils.format( "SELECT permissions.id, permissions.resource_json, roles.name\n" @@ -719,105 +750,6 @@ public List> inTransaction(Handle handle, TransactionStatus .map(permMapper) .bind("userName", userName) .list(); - return user_permissions; - } - } - ); - } - - @Override - public void createAuthenticationToAuthorizationNameMapping( - String authenticationName, String authorizationName - ) - { - getDBI().inTransaction( - new TransactionCallback() - { - @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - String existingMapping = handle - .createQuery( - StringUtils.format( - "SELECT authorization_name FROM %1$s WHERE authentication_name = :authenticationName", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - .bind("authenticationName", authenticationName) - .map(StringMapper.FIRST) - .first(); - - if (existingMapping == null) { - handle.createStatement( - StringUtils.format( - "INSERT INTO %1$s (authentication_name, authorization_name) VALUES (:authenticationName, :authorizationName)", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - .bind("authenticationName", authenticationName) - .bind("authorizationName", authorizationName) - .execute(); - } else { - handle.createStatement( - StringUtils.format( - "UPDATE %1$s SET authorization_name = :authorizationName " + - "WHERE authentication_name = :authenticationName", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - .bind("authenticationName", authenticationName) - .bind("authorizationName", authorizationName) - .execute(); - } - - return null; - } - } - ); - } - - @Override - public String getAuthorizationNameFromAuthenticationName(String authenticationName) - { - return getDBI().inTransaction( - new TransactionCallback() - { - @Override - public String inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - return handle - .createQuery( - StringUtils.format( - "SELECT authorization_name FROM %1$s WHERE authentication_name = :authenticationName", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - .bind("authenticationName", authenticationName) - .map(StringMapper.FIRST) - .first(); - } - } - ); - } - - @Override - public void deleteAuthenticationToAuthorizationNameMapping(String authenticationName) - { - getDBI().inTransaction( - new TransactionCallback() - { - @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - handle.createStatement( - StringUtils.format( - "DELETE FROM %1$s WHERE authentication_name = :authenticationName", - AUTHENTICATION_AUTHORIZATION_NAME_MAPPINGS - ) - ) - .bind("authenticationName", authenticationName) - .execute(); - return null; } } ); @@ -843,7 +775,6 @@ public Map map(int index, ResultSet resultSet, StatementContext } } - @Override public Map getUserCredentials(String userName) { @@ -853,6 +784,11 @@ public Map getUserCredentials(String userName) @Override public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int userCount = getUserCountInTransaction(handle, 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", USER_CREDENTIALS) @@ -874,6 +810,11 @@ public void setUserCredentials(String userName, char[] password) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { + int userCount = getUserCountInTransaction(handle, userName); + if (userCount == 0) { + throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); + } + Map existingMapping = handle .createQuery( StringUtils.format( @@ -961,93 +902,53 @@ public Boolean inTransaction(Handle handle, TransactionStatus transactionStatus) ); } - public final boolean isTransientException(Throwable e) + private int getUserCountInTransaction(Handle handle, String userName) { - 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()))); + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", USERS, "name") + ) + .bind("key", userName) + .map(IntegerMapper.FIRST) + .first(); } - public void createTable(final String tableName, final Iterable sql) + private int getRoleCountInTransaction(Handle handle, String roleName) { - if (!config.get().isCreateTables()) { - return; - } - - 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 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); - } + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", ROLES, "name") + ) + .bind("key", roleName) + .map(IntegerMapper.FIRST) + .first(); } - public T retryWithHandle(final HandleCallback callback) + private int getPermissionCountInTransaction(Handle handle, int permissionId) { - return retryWithHandle(callback, shouldRetry); + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :key", PERMISSIONS, "id") + ) + .bind("key", permissionId) + .map(IntegerMapper.FIRST) + .first(); } - public T retryTransaction(final TransactionCallback callback, final int quietTries, final int maxTries) + private int getUserRoleMappingCountInTransaction(Handle handle, String userName, String roleName) { - 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); - } + return handle + .createQuery( + StringUtils.format("SELECT COUNT(*) FROM %1$s WHERE %2$s = :userkey AND %3$s = :rolekey", + USER_ROLES, + "user_name", + "role_name" + ) + ) + .bind("userkey", userName) + .bind("rolekey", roleName) + .map(IntegerMapper.FIRST) + .first(); } private void makeDefaultSuperuser(String username, String password, String role) @@ -1093,17 +994,9 @@ private void makeDefaultSuperuser(String username, String password, String role) List resActs = Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); for (ResourceAction resAct : resActs) { - try { - byte[] serializedPermission = jsonMapper.writeValueAsBytes(resAct); - addPermission(role, serializedPermission, null); - } - catch (JsonProcessingException jpe) { - log.error("WTF? Couldn't serialize default superuser permission."); - } + addPermission(role, resAct); } setUserCredentials(username, password.toCharArray()); - - createAuthenticationToAuthorizationNameMapping(username, username); } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java deleted file mode 100644 index 19466a46aef0..000000000000 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbyAuthorizationStorage.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package io.druid.security.basic.db.derby; - -import com.google.common.base.Throwables; -import io.druid.java.util.common.logger.Logger; -import io.druid.metadata.MetadataStorageConnectorConfig; -import org.apache.derby.drda.NetworkServerControl; - -import java.net.InetAddress; - -public class DerbyAuthorizationStorage -{ - private static final Logger log = new Logger(DerbyAuthorizationStorage.class); - - private final NetworkServerControl server; - - public DerbyAuthorizationStorage(MetadataStorageConnectorConfig config) - { - try { - this.server = new NetworkServerControl(InetAddress.getByName(config.getHost()), config.getPort()); - } - catch (Exception e) { - throw Throwables.propagate(e); - } - } - - public void start() - { - try { - log.info("Starting Derby Authorization Storage"); - server.start(null); - } - catch (Exception e) { - throw Throwables.propagate(e); - } - } - - public void stop() - { - try { - log.info("Stopping Derby Authorization Storage"); - server.shutdown(); - } - catch (Exception e) { - throw Throwables.propagate(e); - } - } -} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java index 8f45b6a1edc6..13662c4e68c6 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java @@ -27,6 +27,7 @@ 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.BasicAuthConfig; import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; @@ -40,11 +41,11 @@ public class DerbySQLBasicSecurityStorageConnector extends SQLBasicSecurityStora private static final Logger log = new Logger(DerbySQLBasicSecurityStorageConnector.class); private final DBI dbi; - private final DerbyAuthorizationStorage storage; - private static final String QUOTE_STRING = "\\\""; + private final MetadataStorage storage; @Inject public DerbySQLBasicSecurityStorageConnector( + MetadataStorage storage, Supplier config, Supplier basicAuthConfigSupplier, ObjectMapper jsonMapper @@ -57,10 +58,24 @@ public DerbySQLBasicSecurityStorageConnector( datasource.setDriverClassName("org.apache.derby.jdbc.ClientDriver"); this.dbi = new DBI(datasource); - this.storage = new DerbyAuthorizationStorage(config.get()); - log.info("Derby connector instantiated with auth storage [%s].", this.storage.getClass().getName()); + this.storage = storage; + log.info("Derby connector instantiated with metadata storage [%s].", this.storage.getClass().getName()); } + public DerbySQLBasicSecurityStorageConnector( + MetadataStorage storage, + Supplier config, + Supplier basicAuthConfigSupplier, + ObjectMapper jsonMapper, + DBI dbi + ) + { + super(config, basicAuthConfigSupplier, jsonMapper); + this.dbi = dbi; + this.storage = storage; + } + + @Override @LifecycleStart public void start() @@ -160,12 +175,6 @@ public String getValidationQuery() return "VALUES 1"; } - @Override - public String getQuoteString() - { - return QUOTE_STRING; - } - @Override public DBI getDBI() { diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java index 69d49e4a0077..6d7c7bea0095 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java @@ -39,7 +39,6 @@ public class MySQLBasicSecurityStorageConnector extends SQLBasicSecurityStorageC private static final Logger log = new Logger(MySQLBasicSecurityStorageConnector.class); private final DBI dbi; - private static final String QUOTE_STRING = "`"; @Inject public MySQLBasicSecurityStorageConnector( @@ -161,12 +160,6 @@ public boolean tableExists(Handle handle, String tableName) .isEmpty(); } - @Override - public String getQuoteString() - { - return QUOTE_STRING; - } - @Override public DBI getDBI() { diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java index a9341551303e..9a45116ce030 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java @@ -37,11 +37,6 @@ public class PostgresBasicSecurityStorageConnector extends SQLBasicSecurityStora { private static final Logger log = new Logger(PostgresBasicSecurityStorageConnector.class); - private static final String PAYLOAD_TYPE = "BYTEA"; - private static final String SERIAL_TYPE = "BIGSERIAL"; - private static final String QUOTE_STRING = "\\\""; - public static final int DEFAULT_STREAMING_RESULT_SIZE = 100; - private final DBI dbi; @Inject @@ -77,24 +72,12 @@ public boolean tableExists(final Handle handle, final String tableName) .isEmpty(); } - @Override - public String getQuoteString() - { - return QUOTE_STRING; - } - @Override public DBI getDBI() { return dbi; } - @Override - protected String getPayloadType() - { - return PAYLOAD_TYPE; - } - @Override public void createPermissionTable() { diff --git a/extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java similarity index 69% rename from extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java index cd924b902c92..5c0afedf7395 100644 --- a/extensions-core/druid-basic-security/test/java/io/druid/security/basic/BasicAuthUtilsTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicAuthUtilsTest.java @@ -17,28 +17,12 @@ * under the License. */ -package io.druid.security.basic; +package io.druid.security; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.inject.Binder; -import com.google.inject.Guice; -import com.google.inject.Injector; -import com.google.inject.Module; -import com.google.inject.Provides; -import io.druid.guice.ConfigModule; -import io.druid.guice.DruidGuiceExtensions; -import io.druid.guice.JsonConfigProvider; -import io.druid.guice.LazySingleton; -import io.druid.guice.PropertiesModule; -import io.druid.jackson.DefaultObjectMapper; -import io.druid.java.util.common.StringUtils; +import io.druid.security.basic.BasicAuthUtils; import org.junit.Assert; import org.junit.Test; -import java.util.Arrays; -import java.util.Base64; -import java.util.Properties; - public class BasicAuthUtilsTest { @Test diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java new file mode 100644 index 000000000000..346b982fe1a6 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java @@ -0,0 +1,589 @@ +/* + * 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 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.BasicSecurityResource; +import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +import io.druid.security.db.TestDerbySecurityConnector; +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.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 BasicSecurityResourceTest +{ + private BasicSecurityResource basicSecurityResource; + private HttpServletRequest req; + private TestDerbySecurityConnector connector; + + @Rule + public final TestDerbySecurityConnector.DerbyConnectorRule derbyConnectorRule = + new TestDerbySecurityConnector.DerbyConnectorRule(); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() throws Exception + { + req = EasyMock.createStrictMock(HttpServletRequest.class); + connector = derbyConnectorRule.getConnector(); + createAllTables(); + basicSecurityResource = new BasicSecurityResource(connector); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testGetAllUsers() + { + Response response = basicSecurityResource.getAllUsers(req); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + basicSecurityResource.createUser(req, "druid"); + basicSecurityResource.createUser(req, "druid2"); + basicSecurityResource.createUser(req, "druid3"); + + + List> expectedUsers = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + + response = basicSecurityResource.getAllUsers(req); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedUsers, response.getEntity()); + } + + @Test + public void testGetAllRoles() + { + Response response = basicSecurityResource.getAllRoles(req); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); + + basicSecurityResource.createRole(req, "druid"); + basicSecurityResource.createRole(req, "druid2"); + basicSecurityResource.createRole(req, "druid3"); + + List> expectedRoles = ImmutableList.of( + ImmutableMap.of("name", "druid"), + ImmutableMap.of("name", "druid2"), + ImmutableMap.of("name", "druid3") + ); + + response = basicSecurityResource.getAllRoles(req); + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(expectedRoles, response.getEntity()); + } + + @Test + public void testCreateDeleteUser() + { + Response response = basicSecurityResource.createUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.deleteUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.deleteUser(req, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = basicSecurityResource.getUser(req, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + @Test + public void testUserCredentials() + { + Response response = basicSecurityResource.createUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.updateUserCredentials(req, "druid", "helloworld"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUserCredentials(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + Map ent = (Map ) response.getEntity(); + Assert.assertEquals("druid", ent.get("user_name")); + byte[] salt = (byte[]) ent.get("salt"); + byte[] hash = (byte[]) ent.get("hash"); + int iterations = (Integer) ent.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 = basicSecurityResource.deleteUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUserCredentials(req, "druid"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + + response = basicSecurityResource.updateUserCredentials(req, "druid", "helloworld"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + } + + @Test + public void testCreateDeleteRole() + { + Response response = basicSecurityResource.createRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getRole(req, "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 = basicSecurityResource.deleteRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.deleteRole(req, "druidRole"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); + + response = basicSecurityResource.getRole(req, "druidRole"); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); + } + + @Test + public void testRoleAssignment() throws Exception + { + Response response = basicSecurityResource.createRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getRole(req, "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 = basicSecurityResource.unassignRoleFromUser(req, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getRole(req, "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 = basicSecurityResource.createRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createUser(req, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getRole(req, "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 = basicSecurityResource.deleteRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getUser(req, "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 = basicSecurityResource.createRole(req, "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 = basicSecurityResource.addPermissionsToRole(req, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.addPermissionsToRole(req, "wrongRole", perms); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Role [wrongRole] does not exist."), response.getEntity()); + + response = basicSecurityResource.getRole(req, "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 = basicSecurityResource.deletePermission(req, 7); + Assert.assertEquals(400, response.getStatus()); + Assert.assertEquals(errorMapWithMsg("Permission with id [7] does not exist."), response.getEntity()); + + response = basicSecurityResource.deletePermission(req, 2); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getRole(req, "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 = basicSecurityResource.createUser(req, "druid"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createUser(req, "druid2"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createRole(req, "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.createRole(req, "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 = basicSecurityResource.addPermissionsToRole(req, "druidRole", perms); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.addPermissionsToRole(req, "druidRole2", perms2); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getRole(req, "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 = basicSecurityResource.getRole(req, "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 = basicSecurityResource.deletePermission(req, 1); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.deletePermission(req, 4); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getUser(req, "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 = basicSecurityResource.unassignRoleFromUser(req, "druid", "druidRole"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.unassignRoleFromUser(req, "druid2", "druidRole2"); + Assert.assertEquals(200, response.getStatus()); + + response = basicSecurityResource.getUser(req, "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 = basicSecurityResource.getUser(req, "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(); + connector.createRoleTable(); + connector.createPermissionTable(); + connector.createUserRoleTable(); + connector.createUserCredentialsTable(); + } + + private void dropAllTables() + { + for (String table : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + 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/SQLBasicSecurityStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java new file mode 100644 index 000000000000..fd4ca85c79d0 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java @@ -0,0 +1,363 @@ +/* + * 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.db.SQLBasicSecurityStorageConnector; +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 SQLBasicSecurityStorageConnectorTest +{ + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Rule + public final TestDerbySecurityConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbySecurityConnector.DerbyConnectorRule(); + + private TestDerbySecurityConnector connector; + + @Before + public void setUp() throws Exception + { + connector = derbyConnectorRule.getConnector(); + createAllTables(); + } + + @After + public void tearDown() throws Exception + { + dropAllTables(); + } + + @Test + public void testCreateTables() throws Exception + { + connector.getDBI().withHandle( + new HandleCallback() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + for (String table : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + Assert.assertTrue( + StringUtils.format("table %s was not created!", table), + connector.tableExists(handle, table) + ); + } + + return null; + } + } + ); + } + + // user tests + @Test + public void testCreateDeleteUser() throws Exception + { + connector.createUser("druid"); + Map expectedUser = ImmutableMap.of( + "name", "druid" + ); + Map dbUser = connector.getUser("druid"); + Assert.assertEquals(expectedUser, dbUser); + + connector.deleteUser("druid"); + dbUser = connector.getUser("druid"); + Assert.assertEquals(null, dbUser); + } + + @Test + public void testDeleteNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + connector.deleteUser("druid"); + } + + @Test + public void testCreateDuplicateUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] already exists."); + connector.createUser("druid"); + connector.createUser("druid"); + } + + // role tests + @Test + public void testCreateRole() throws Exception + { + connector.createRole("druid"); + Map expectedRole = ImmutableMap.of( + "name", "druid" + ); + Map dbRole = connector.getRole("druid"); + Assert.assertEquals(expectedRole, dbRole); + } + + @Test + public void testDeleteNonExistentRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druid] does not exist."); + connector.deleteRole("druid"); + } + + @Test + public void testCreateDuplicateRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druid] already exists."); + connector.createRole("druid"); + connector.createRole("druid"); + } + + // role and user tests + @Test + public void testAddAndRemoveRole() throws Exception + { + connector.createUser("druid"); + connector.createRole("druidRole"); + connector.assignRole("druid", "druidRole"); + + List> expectedUsersWithRole = ImmutableList.of( + ImmutableMap.of("name", "druid") + ); + + List> expectedRolesForUser = ImmutableList.of( + ImmutableMap.of("name", "druidRole") + ); + + List> usersWithRole = connector.getUsersWithRole("druidRole"); + List> rolesForUser = connector.getRolesForUser("druid"); + + Assert.assertEquals(expectedUsersWithRole, usersWithRole); + Assert.assertEquals(expectedRolesForUser, rolesForUser); + + connector.unassignRole("druid", "druidRole"); + usersWithRole = connector.getUsersWithRole("druidRole"); + rolesForUser = connector.getRolesForUser("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."); + connector.createRole("druid"); + connector.assignRole("nonUser", "druid"); + } + + @Test + public void testAddNonexistentRoleToUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [nonRole] does not exist."); + connector.createUser("druid"); + connector.assignRole("druid", "nonRole"); + } + + @Test + public void testAddExistingRoleToUserFails() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] already has role [druidRole]."); + connector.createUser("druid"); + connector.createRole("druidRole"); + connector.assignRole("druid", "druidRole"); + connector.assignRole("druid", "druidRole"); + } + + @Test + public void testUnassignInvalidRoleAssignmentFails() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not have role [druidRole]."); + + connector.createUser("druid"); + connector.createRole("druidRole"); + + List> usersWithRole = connector.getUsersWithRole("druidRole"); + List> rolesForUser = connector.getRolesForUser("druid"); + + Assert.assertEquals(ImmutableList.of(), usersWithRole); + Assert.assertEquals(ImmutableList.of(), rolesForUser); + + connector.unassignRole("druid", "druidRole"); + } + + // role and permission tests + @Test + public void testAddPermissionToRole() throws Exception + { + connector.createUser("druid"); + connector.createRole("druidRole"); + connector.assignRole("druid", "druidRole"); + + ResourceAction permission = new ResourceAction( + new Resource("testResource", ResourceType.DATASOURCE), + Action.WRITE + ); + connector.addPermission("druidRole", permission); + + List> expectedPerms = ImmutableList.of( + ImmutableMap.of( + "id", 1, + "resourceAction", permission + ) + ); + List> dbPermsRole = connector.getPermissionsForRole("druidRole"); + Assert.assertEquals(expectedPerms, dbPermsRole); + List> dbPermsUser = connector.getPermissionsForUser("druid"); + Assert.assertEquals(expectedPerms, dbPermsUser); + + connector.deletePermission(1); + dbPermsRole = connector.getPermissionsForRole("druidRole"); + Assert.assertEquals(ImmutableList.of(), dbPermsRole); + dbPermsUser = connector.getPermissionsForUser("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 + ); + connector.addPermission("druidRole", permission); + } + + @Test + public void testGetPermissionForNonExistentRole() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Role [druidRole] does not exist."); + connector.getPermissionsForRole("druidRole"); + } + + @Test + public void testGetPermissionForNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + connector.getPermissionsForUser("druid"); + } + + // user credentials + @Test + public void testAddUserCredentials() throws Exception + { + char[] pass = "blah".toCharArray(); + connector.createUser("druid"); + connector.setUserCredentials("druid", pass); + Assert.assertTrue(connector.checkCredentials("druid", pass)); + Assert.assertFalse(connector.checkCredentials("druid", "wrongPass".toCharArray())); + + Map creds = connector.getUserCredentials("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(); + connector.setUserCredentials("druid", pass); + } + + @Test + public void testGetCredentialsForNonExistentUser() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("User [druid] does not exist."); + connector.getUserCredentials("druid"); + } + + private void createAllTables() + { + connector.createUserTable(); + connector.createRoleTable(); + connector.createPermissionTable(); + connector.createUserRoleTable(); + connector.createUserCredentialsTable(); + } + + private void dropAllTables() + { + for (String table : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + 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/TestDerbySecurityConnector.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbySecurityConnector.java new file mode 100644 index 000000000000..6f377d89c4f9 --- /dev/null +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbySecurityConnector.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.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.BasicAuthConfig; +import io.druid.security.basic.db.derby.DerbySQLBasicSecurityStorageConnector; +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 TestDerbySecurityConnector extends DerbySQLBasicSecurityStorageConnector +{ + private final String jdbcUri; + + public TestDerbySecurityConnector( + Supplier config, + Supplier basicAuthConfig + ) + { + this(config, basicAuthConfig, "jdbc:derby:memory:druidTest" + dbSafeUUID()); + } + + protected TestDerbySecurityConnector( + Supplier config, + Supplier basicAuthConfig, + String jdbcUri + ) + { + super( + new NoopMetadataStorageProvider().get(), + config, + basicAuthConfig, + 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 TestDerbySecurityConnector connector; + private final Supplier basicAuthConfigSupplier; + private final MetadataStorageConnectorConfig connectorConfig; + + public DerbyConnectorRule() + { + this(Suppliers.ofInstance(new BasicAuthConfig())); + } + + public DerbyConnectorRule( + Supplier basicAuthConfigSupplier + ) + { + this.basicAuthConfigSupplier = basicAuthConfigSupplier; + this.connectorConfig = new MetadataStorageConnectorConfig() + { + @Override + public String getConnectURI() + { + return connector.getJdbcUri(); + } + }; + } + + @Override + protected void before() throws Throwable + { + connector = new TestDerbySecurityConnector(Suppliers.ofInstance(connectorConfig), basicAuthConfigSupplier); + connector.getDBI().open().close(); // create db + } + + @Override + protected void after() + { + connector.tearDown(); + } + + public TestDerbySecurityConnector getConnector() + { + return connector; + } + + public MetadataStorageConnectorConfig getMetadataConnectorConfig() + { + return connectorConfig; + } + + public Supplier basicAuthConfigSupplier() + { + return basicAuthConfigSupplier; + } + } +} 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 48d34b8d3d11..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 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..6a0a6cfd8f76 --- /dev/null +++ b/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java @@ -0,0 +1,121 @@ +/* + * 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.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 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..396834fa0411 100644 --- a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java +++ b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java @@ -29,34 +29,25 @@ 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,34 +109,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() @@ -164,52 +127,6 @@ public T call() throws Exception } } - 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 +363,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/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); + } } From 1792477062d63db31d3929a93c01712277a6d7e7 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 25 Oct 2017 19:17:27 -0700 Subject: [PATCH 3/8] Add regex matching benchmark --- .../druid/benchmark/RegexMatchBenchmark.java | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java 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..0c6387e239b6 --- /dev/null +++ b/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java @@ -0,0 +1,176 @@ +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()); + } + } +} + From 6f98b308af01a7ba4b6cd38b03c2e1f8d5dc7b13 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 25 Oct 2017 20:48:39 -0700 Subject: [PATCH 4/8] More PR comments --- .../db/BasicSecurityStorageConnector.java | 4 - .../db/SQLBasicSecurityStorageConnector.java | 79 ++++++------------- 2 files changed, 22 insertions(+), 61 deletions(-) diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java index e10a1c5e08d8..e4642228f2d4 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java @@ -39,8 +39,6 @@ public interface BasicSecurityStorageConnector void addPermission(String roleName, ResourceAction resourceAction); - void deleteAllPermissionsFromRole(String roleName); - void deletePermission(int permissionId); void assignRole(String userName, String roleName); @@ -73,8 +71,6 @@ public interface BasicSecurityStorageConnector void createUserCredentialsTable(); - void deleteAllRecords(String tableName); - void setUserCredentials(String userName, char[] password); boolean checkCredentials(String userName, char[] password); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java index 3764650d5723..f7d3b8c60ef5 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java @@ -217,12 +217,6 @@ public void createUserRoleTable() ); } - @Override - public void deleteAllRecords(String tableName) - { - throw new UnsupportedOperationException("delete all not supported yet for authorization storage"); - } - public MetadataStorageConnectorConfig getConfig() { return config.get(); @@ -258,7 +252,7 @@ public void createUser(String userName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int count = getUserCountInTransaction(handle, userName); + int count = getUserCount(handle, userName); if (count != 0) { throw new BasicSecurityDBResourceException("User [%s] already exists.", userName); } @@ -285,7 +279,7 @@ public void deleteUser(String userName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int count = getUserCountInTransaction(handle, userName); + int count = getUserCount(handle, userName); if (count == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -311,7 +305,7 @@ public void createRole(String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int count = getRoleCountInTransaction(handle, roleName); + int count = getRoleCount(handle, roleName); if (count != 0) { throw new BasicSecurityDBResourceException("Role [%s] already exists.", roleName); } @@ -337,7 +331,7 @@ public void deleteRole(String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int count = getRoleCountInTransaction(handle, roleName); + int count = getRoleCount(handle, roleName); if (count == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -363,7 +357,7 @@ public void addPermission(String roleName, ResourceAction resourceAction) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int roleCount = getRoleCountInTransaction(handle, roleName); + int roleCount = getRoleCount(handle, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -390,35 +384,6 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th ); } - @Override - public void deleteAllPermissionsFromRole(String roleName) - { - getDBI().inTransaction( - new TransactionCallback() - { - @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - int roleCount = getRoleCountInTransaction(handle, roleName); - if (roleCount == 0) { - throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); - } - - handle.createStatement( - StringUtils.format( - "DELETE FROM %1$s WHERE role_name = :roleName", - PERMISSIONS - ) - ) - .bind("roleName", roleName) - .execute(); - - return null; - } - } - ); - } - @Override public void deletePermission(int permissionId) { @@ -428,7 +393,7 @@ public void deletePermission(int permissionId) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int permCount = getPermissionCountInTransaction(handle, permissionId); + int permCount = getPermissionCount(handle, permissionId); if (permCount == 0) { throw new BasicSecurityDBResourceException("Permission with id [%s] does not exist.", permissionId); } @@ -454,8 +419,8 @@ public void assignRole(String userName, String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); - int roleCount = getRoleCountInTransaction(handle, roleName); + int userCount = getUserCount(handle, userName); + int roleCount = getRoleCount(handle, roleName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -465,7 +430,7 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } - int userRoleMappingCount = getUserRoleMappingCountInTransaction(handle, userName, roleName); + int userRoleMappingCount = getUserRoleMappingCount(handle, userName, roleName); if (userRoleMappingCount != 0) { throw new BasicSecurityDBResourceException("User [%s] already has role [%s].", userName, roleName); } @@ -493,8 +458,8 @@ public void unassignRole(String userName, String roleName) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); - int roleCount = getRoleCountInTransaction(handle, roleName); + int userCount = getUserCount(handle, userName); + int roleCount = getRoleCount(handle, roleName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -504,7 +469,7 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } - int userRoleMappingCount = getUserRoleMappingCountInTransaction(handle, userName, roleName); + int userRoleMappingCount = getUserRoleMappingCount(handle, userName, roleName); if (userRoleMappingCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not have role [%s].", userName, roleName); } @@ -614,7 +579,7 @@ public List> getRolesForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); + int userCount = getUserCount(handle, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -646,7 +611,7 @@ public List> getUsersWithRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int roleCount = getRoleCountInTransaction(handle, roleName); + int roleCount = getRoleCount(handle, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -699,7 +664,7 @@ public List> getPermissionsForRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int roleCount = getRoleCountInTransaction(handle, roleName); + int roleCount = getRoleCount(handle, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -730,7 +695,7 @@ public List> getPermissionsForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); + int userCount = getUserCount(handle, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -784,7 +749,7 @@ public Map getUserCredentials(String userName) @Override public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); + int userCount = getUserCount(handle, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -810,7 +775,7 @@ public void setUserCredentials(String userName, char[] password) @Override public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCountInTransaction(handle, userName); + int userCount = getUserCount(handle, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -902,7 +867,7 @@ public Boolean inTransaction(Handle handle, TransactionStatus transactionStatus) ); } - private int getUserCountInTransaction(Handle handle, String userName) + private int getUserCount(Handle handle, String userName) { return handle .createQuery( @@ -913,7 +878,7 @@ private int getUserCountInTransaction(Handle handle, String userName) .first(); } - private int getRoleCountInTransaction(Handle handle, String roleName) + private int getRoleCount(Handle handle, String roleName) { return handle .createQuery( @@ -924,7 +889,7 @@ private int getRoleCountInTransaction(Handle handle, String roleName) .first(); } - private int getPermissionCountInTransaction(Handle handle, int permissionId) + private int getPermissionCount(Handle handle, int permissionId) { return handle .createQuery( @@ -935,7 +900,7 @@ private int getPermissionCountInTransaction(Handle handle, int permissionId) .first(); } - private int getUserRoleMappingCountInTransaction(Handle handle, String userName, String roleName) + private int getUserRoleMappingCount(Handle handle, String userName, String roleName) { return handle .createQuery( From dd89a13b0049d0a319f8e9f0fd798111805d63e7 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 25 Oct 2017 21:09:26 -0700 Subject: [PATCH 5/8] Checkstyle --- .../druid/benchmark/RegexMatchBenchmark.java | 21 ++++++++++++++++++- .../SQLBasicSecurityStorageConnectorTest.java | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java b/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java index 0c6387e239b6..7f081d754875 100644 --- a/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java +++ b/benchmarks/src/main/java/io/druid/benchmark/RegexMatchBenchmark.java @@ -1,3 +1,22 @@ +/* + * 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; @@ -126,7 +145,7 @@ public void deserializeUUIDRegex(final Blackhole blackhole) throws IOException blackhole.consume(pattern); } } - + @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java index fd4ca85c79d0..22c6435fd61f 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package io.druid.security.db; import com.google.common.collect.ImmutableList; From bbbd43a30ac958f11c1da081f1b4f57b1ad487e1 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 25 Oct 2017 23:34:45 -0700 Subject: [PATCH 6/8] Cache permission Patterns, update docs --- .../extensions-core/druid-basic-security.md | 64 ++++++- .../druid/security/basic/BasicAuthConfig.java | 8 + .../BasicRoleBasedAuthorizer.java | 14 +- .../BasicRoleBasedAuthorizerTest.java | 167 ++++++++++++++++++ 4 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicRoleBasedAuthorizerTest.java diff --git a/docs/content/development/extensions-core/druid-basic-security.md b/docs/content/development/extensions-core/druid-basic-security.md index d736b1364d23..17a15c819fc0 100644 --- a/docs/content/development/extensions-core/druid-basic-security.md +++ b/docs/content/development/extensions-core/druid-basic-security.md @@ -19,6 +19,7 @@ Make sure to [include](../../operations/including-extensions.html) `druid-basic- |--------|-----------|-------|--------| |`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 ``` @@ -143,6 +144,10 @@ Content: List of JSON Resource-Action objects, e.g.: ] ``` +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. @@ -154,4 +159,61 @@ A default internal system user account with full privileges, meant for internal 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. \ No newline at end of file +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/src/main/java/io/druid/security/basic/BasicAuthConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java index 68977b389290..cca977d7bbf8 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java @@ -29,6 +29,9 @@ public class BasicAuthConfig @JsonProperty private String initialInternalClientPassword = "druid"; + @JsonProperty + private int permissionCacheSize = 5000; + public String getInitialAdminPassword() { return initialAdminPassword; @@ -38,4 +41,9 @@ public String getInitialInternalClientPassword() { return initialInternalClientPassword; } + + public int getPermissionCacheSize() + { + return permissionCacheSize; + } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java index 23d15a3fb72a..8abd8da8d76e 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -22,7 +22,10 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; 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.BasicAuthConfig; import io.druid.security.basic.db.BasicSecurityStorageConnector; import io.druid.server.security.Access; import io.druid.server.security.Action; @@ -40,13 +43,18 @@ public class BasicRoleBasedAuthorizer implements Authorizer { private final BasicSecurityStorageConnector dbConnector; + private final LoadingCache permissionPatternCache; @JsonCreator public BasicRoleBasedAuthorizer( - @JacksonInject BasicSecurityStorageConnector dbConnector + @JacksonInject BasicSecurityStorageConnector dbConnector, + @JacksonInject BasicAuthConfig authConfig ) { this.dbConnector = dbConnector; + this.permissionPatternCache = Caffeine.newBuilder() + .maximumSize(authConfig.getPermissionCacheSize()) + .build(regexStr -> Pattern.compile(regexStr)); } @Override @@ -60,7 +68,6 @@ public Access authorize( List> permissions = dbConnector.getPermissionsForUser(authenticationResult.getIdentity()); - // maybe optimize this later for (Map permission : permissions) { if (permissionCheck(resource, action, permission)) { return new Access(true); @@ -83,7 +90,8 @@ private boolean permissionCheck(Resource resource, Action action, Map() + { + @Override + public Void withHandle(Handle handle) throws Exception + { + handle.createStatement(StringUtils.format("DROP TABLE %s", tableName)) + .execute(); + return null; + } + } + ); + } +} From f1cd250c9176a351240237bdada5659c5518b198 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Wed, 25 Oct 2017 23:52:14 -0700 Subject: [PATCH 7/8] Add regex compile failure check --- .../basic/db/SQLBasicSecurityStorageConnector.java | 13 +++++++++++++ .../db/SQLBasicSecurityStorageConnectorTest.java | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java index f7d3b8c60ef5..a41af4e7bb21 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java @@ -53,6 +53,8 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; /** * Base class for BasicSecurityStorageConnector implementations that interface with a database using SQL. @@ -362,6 +364,17 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th 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( diff --git a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java index 22c6435fd61f..4df73ab75143 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java @@ -270,6 +270,19 @@ public void testAddPermissionToNonExistentRole() throws Exception connector.addPermission("druidRole", permission); } + @Test + public void testAddBadPermission() throws Exception + { + expectedException.expect(CallbackFailedException.class); + expectedException.expectMessage("Invalid permission, resource name regex[??????????] does not compile."); + connector.createRole("druidRole"); + ResourceAction permission = new ResourceAction( + new Resource("??????????", ResourceType.DATASOURCE), + Action.WRITE + ); + connector.addPermission("druidRole", permission); + } + @Test public void testGetPermissionForNonExistentRole() throws Exception { From 07074493ef0a52192b7e58a8c608a588b642c4c8 Mon Sep 17 00:00:00 2001 From: jon-wei Date: Tue, 31 Oct 2017 15:11:35 -0700 Subject: [PATCH 8/8] Allow multiple authenticator/authorizer instances --- .../basic/BasicAuthenticatorResource.java | 260 +++++++++ ...urce.java => BasicAuthorizerResource.java} | 223 +++++--- .../basic/BasicSecurityDruidModule.java | 88 ++- .../basic/BasicSecurityResourceFilter.java | 4 +- .../BasicHTTPAuthenticator.java | 50 +- .../BasicRoleBasedAuthorizer.java | 58 +- .../basic/cli/CreateAuthorizationTables.java | 268 --------- .../BasicAuthDBConfig.java} | 36 +- .../BasicAuthenticatorStorageConnector.java} | 30 +- .../db/BasicAuthorizerStorageConnector.java | 68 +++ .../db/BasicSecurityStorageConnector.java | 79 --- ...SQLBasicAuthenticatorStorageConnector.java | 493 ++++++++++++++++ ...> SQLBasicAuthorizerStorageConnector.java} | 541 ++++++++---------- ...SQLBasicAuthenticatorStorageConnector.java | 147 +++++ ...bySQLBasicAuthorizerStorageConnector.java} | 71 +-- ...SQLBasicAuthenticatorStorageConnector.java | 134 +++++ ...MySQLBasicAuthorizerStorageConnector.java} | 64 +-- ...SQLBasicAuthenticatorStorageConnector.java | 102 ++++ ...reSQLBasicAuthorizerStorageConnector.java} | 48 +- .../services/io.druid.cli.CliCommandCreator | 1 - .../BasicRoleBasedAuthorizerTest.java | 53 +- .../db/BasicAuthenticatorResourceTest.java | 264 +++++++++ .../BasicAuthorizerResourceTest.java} | 280 +++++---- ...asicAuthenticatorStorageConnectorTest.java | 191 +++++++ ...LBasicAuthorizerStorageConnectorTest.java} | 176 +++--- ...estDerbyAuthenticatorStorageConnector.java | 130 +++++ ... TestDerbyAuthorizerStorageConnector.java} | 44 +- .../metadata/BaseSQLMetadataConnector.java | 19 + .../druid/metadata/SQLMetadataConnector.java | 21 - .../server/security/AuthenticatorMapper.java | 5 + .../server/security/AuthorizerMapper.java | 5 + 31 files changed, 2718 insertions(+), 1235 deletions(-) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthenticatorResource.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/{BasicSecurityResource.java => BasicAuthorizerResource.java} (61%) delete mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/CreateAuthorizationTables.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/{BasicAuthConfig.java => db/BasicAuthDBConfig.java} (64%) rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/{cli/BasicSecurityCliCommandCreator.java => db/BasicAuthenticatorStorageConnector.java} (53%) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthorizerStorageConnector.java delete mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthenticatorStorageConnector.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/{SQLBasicSecurityStorageConnector.java => SQLBasicAuthorizerStorageConnector.java} (63%) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthenticatorStorageConnector.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/{DerbySQLBasicSecurityStorageConnector.java => DerbySQLBasicAuthorizerStorageConnector.java} (70%) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthenticatorStorageConnector.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/{MySQLBasicSecurityStorageConnector.java => MySQLBasicAuthorizerStorageConnector.java} (72%) create mode 100644 extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthenticatorStorageConnector.java rename extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/{PostgresBasicSecurityStorageConnector.java => PostgreSQLBasicAuthorizerStorageConnector.java} (68%) delete mode 100644 extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthenticatorResourceTest.java rename extensions-core/druid-basic-security/src/test/java/io/druid/security/{BasicSecurityResourceTest.java => db/BasicAuthorizerResourceTest.java} (66%) create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthenticatorStorageConnectorTest.java rename extensions-core/druid-basic-security/src/test/java/io/druid/security/db/{SQLBasicSecurityStorageConnectorTest.java => SQLBasicAuthorizerStorageConnectorTest.java} (59%) create mode 100644 extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthenticatorStorageConnector.java rename extensions-core/druid-basic-security/src/test/java/io/druid/security/db/{TestDerbySecurityConnector.java => TestDerbyAuthorizerStorageConnector.java} (70%) 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/BasicSecurityResource.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java similarity index 61% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java index 51c2be314d76..0342c5419b0e 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResource.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthorizerResource.java @@ -20,9 +20,14 @@ 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.security.basic.db.BasicSecurityStorageConnector; +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; @@ -41,19 +46,30 @@ import java.util.Map; /** - * Configuration resource for users, roles, and permissions. + * Configuration resource for authorizer users/roles/permissions */ -@Path("/druid/coordinator/v1/security") -public class BasicSecurityResource +@Path("/druid/coordinator/v1/security/authorization") +public class BasicAuthorizerResource { - private final BasicSecurityStorageConnector dbConnector; + private final BasicAuthorizerStorageConnector dbConnector; + private final Map authorizerMap; @Inject - public BasicSecurityResource( - BasicSecurityStorageConnector dbConnector + 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)); + } + } } /** @@ -62,15 +78,21 @@ public BasicSecurityResource( * @return List of all users */ @GET - @Path("/users") + @Path("/{authorizerName}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) public Response getAllUsers( - @Context HttpServletRequest req + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName ) { - List> users = dbConnector.getAllUsers(); + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + List> users = dbConnector.getAllUsers(authorizer.getDBPrefix()); return Response.ok(users).build(); } @@ -81,19 +103,25 @@ public Response getAllUsers( * @return Name, roles, and permissions of the user with userName, 400 error response if user doesn't exist */ @GET - @Path("/users/{userName}") + @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(userName); - List> roles = dbConnector.getRolesForUser(userName); - List> permissions = dbConnector.getPermissionsForUser(userName); + 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, @@ -117,18 +145,23 @@ public Response getUser( * @return OK response, or 400 error response if user already exists */ @POST - @Path("/users/{userName}") + @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(userName); + dbConnector.createUser(authorizer.getDBPrefix(), userName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -145,73 +178,23 @@ public Response createUser( * @return OK response, or 400 error response if user doesn't exist */ @DELETE - @Path("/users/{userName}") + @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 ) { - try { - dbConnector.deleteUser(userName); - return Response.ok().build(); - } - catch (CallbackFailedException cfe) { - return makeResponseForCallbackFailedException(cfe); - } - } - - /** - * Get credential information of user - * - * @param req HTTP request - * @param userName Name of user - * - * @return salt, hash, and number of iterations for this user's credentials, 400 error if user doesn't exist - */ - @GET - @Path("/credentials/{userName}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) - public Response getUserCredentials( - @Context HttpServletRequest req, - @PathParam("userName") final String userName - ) - { - try { - Map credentials = dbConnector.getUserCredentials(userName); - return Response.ok(credentials).build(); - } - catch (CallbackFailedException cfe) { - return makeResponseForCallbackFailedException(cfe); + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); } - } - /** - * 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("/credentials/{userName}") - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) - public Response updateUserCredentials( - @Context HttpServletRequest req, - @PathParam("userName") String userName, - String password - ) - { try { - dbConnector.setUserCredentials(userName, password.toCharArray()); + dbConnector.deleteUser(authorizer.getDBPrefix(), userName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -225,15 +208,21 @@ public Response updateUserCredentials( * @return List of all roles */ @GET - @Path("/roles") + @Path("/{authorizerName}/roles") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @ResourceFilters(BasicSecurityResourceFilter.class) public Response getAllRoles( - @Context HttpServletRequest req + @Context HttpServletRequest req, + @PathParam("authorizerName") final String authorizerName ) { - List> roles = dbConnector.getAllRoles(); + final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName); + if (authorizer == null) { + return makeResponseForAuthorizerNotFound(authorizerName); + } + + List> roles = dbConnector.getAllRoles(authorizer.getDBPrefix()); return Response.ok(roles).build(); } @@ -246,19 +235,25 @@ public Response getAllRoles( * @return Role name, users with role, and permissions of role. 400 error if role doesn't exist. */ @GET - @Path("/roles/{roleName}") + @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(roleName); - List> users = dbConnector.getUsersWithRole(roleName); - List> permissions = dbConnector.getPermissionsForRole(roleName); + 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, @@ -282,17 +277,23 @@ public Response getRole( * @return OK response, 400 error if role already exists */ @POST - @Path("/roles/{roleName}") + @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(roleName); + dbConnector.createRole(authorizer.getDBPrefix(), roleName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -309,17 +310,23 @@ public Response createRole( * @return OK response, 400 error if role doesn't exist. */ @DELETE - @Path("/roles/{roleName}") + @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(roleName); + dbConnector.deleteRole(authorizer.getDBPrefix(), roleName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -337,18 +344,24 @@ public Response deleteRole( * @return OK response. 400 error if user/role don't exist, or if user already has the role */ @POST - @Path("/users/{userName}/roles/{roleName}") + @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(userName, roleName); + dbConnector.assignRole(authorizer.getDBPrefix(), userName, roleName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -366,18 +379,24 @@ public Response assignRoleToUser( * @return OK response. 400 error if user/role don't exist, or if user does not have the role. */ @DELETE - @Path("/users/{userName}/roles/{roleName}") + @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(userName, roleName); + dbConnector.unassignRole(authorizer.getDBPrefix(), userName, roleName); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -395,19 +414,25 @@ public Response unassignRoleFromUser( * @return OK response. 400 error if role doesn't exist. */ @POST - @Path("/roles/{roleName}/permissions") + @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(roleName, resourceAction); + dbConnector.addPermission(authorizer.getDBPrefix(), roleName, resourceAction); } return Response.ok().build(); @@ -426,17 +451,23 @@ public Response addPermissionsToRole( * @return OK response. 400 error if permission doesn't exist. */ @DELETE - @Path("/permissions/{permId}") + @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(permId); + dbConnector.deletePermission(authorizer.getDBPrefix(), permId); return Response.ok().build(); } catch (CallbackFailedException cfe) { @@ -444,6 +475,16 @@ public Response deletePermission( } } + 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(); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java index 099762b93750..484d56e5b5a7 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityDruidModule.java @@ -25,17 +25,21 @@ import com.google.inject.Binder; import com.google.inject.Key; import io.druid.guice.Jerseys; -import io.druid.guice.JsonConfigProvider; 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.BasicSecurityStorageConnector; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; -import io.druid.security.basic.db.derby.DerbySQLBasicSecurityStorageConnector; -import io.druid.security.basic.db.mysql.MySQLBasicSecurityStorageConnector; -import io.druid.security.basic.db.postgres.PostgresBasicSecurityStorageConnector; +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; @@ -46,46 +50,86 @@ public class BasicSecurityDruidModule implements DruidModule @Override public void configure(Binder binder) { - JsonConfigProvider.bind(binder, "druid.auth.basic", BasicAuthConfig.class); + // 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(BasicSecurityStorageConnector.class), null, "derby" + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(BasicAuthorizerStorageConnector.class), null, "derby" ); PolyBind.createChoiceWithDefault( - binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(SQLBasicSecurityStorageConnector.class), null, "derby" + binder, STORAGE_CONNECTOR_TYPE_PROPERTY, Key.get(SQLBasicAuthorizerStorageConnector.class), null, "derby" ); - PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) .addBinding("derby") - .to(DerbySQLBasicSecurityStorageConnector.class) + .to(DerbySQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) .addBinding("derby") - .to(DerbySQLBasicSecurityStorageConnector.class) + .to(DerbySQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) .addBinding("mysql") - .to(MySQLBasicSecurityStorageConnector.class) + .to(MySQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) .addBinding("mysql") - .to(MySQLBasicSecurityStorageConnector.class) + .to(MySQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(BasicAuthorizerStorageConnector.class)) .addBinding("postgresql") - .to(PostgresBasicSecurityStorageConnector.class) + .to(PostgreSQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class)) + PolyBind.optionBinder(binder, Key.get(SQLBasicAuthorizerStorageConnector.class)) .addBinding("postgresql") - .to(PostgresBasicSecurityStorageConnector.class) + .to(PostgreSQLBasicAuthorizerStorageConnector.class) .in(ManageLifecycle.class); - Jerseys.addResource(binder, BasicSecurityResource.class); + Jerseys.addResource(binder, BasicAuthorizerResource.class); } @Override diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java index c05d7fe13f1a..adb3b0cf8f4e 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicSecurityResourceFilter.java @@ -25,8 +25,8 @@ 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.AuthorizerMapper; 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; @@ -38,7 +38,7 @@ public class BasicSecurityResourceFilter extends AbstractResourceFilter { private static final List applicablePaths = ImmutableList.of( - "druid/coordinator/v1/security/" + "druid/coordinator/v1/security/*" ); @Inject diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java index 8faf6ac88b2a..6cab70cf60b8 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authentication/BasicHTTPAuthenticator.java @@ -29,7 +29,8 @@ 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.BasicSecurityStorageConnector; +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; @@ -37,7 +38,6 @@ 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; @@ -58,23 +58,46 @@ @JsonTypeName("basic") public class BasicHTTPAuthenticator implements Authenticator { - private final BasicSecurityStorageConnector dbConnector; + private final BasicAuthenticatorStorageConnector dbConnector; private final String internalClientUsername; private final String internalClientPassword; private final String authorizerName; + private final BasicAuthDBConfig dbConfig; @JsonCreator public BasicHTTPAuthenticator( - @JacksonInject BasicSecurityStorageConnector dbConnector, + @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 @@ -100,7 +123,7 @@ public AuthenticationResult authenticateJDBCContext(Map context) return null; } - if (dbConnector.checkCredentials(user, password.toCharArray())) { + if (dbConnector.checkCredentials(dbConfig.getDbPrefix(), user, password.toCharArray())) { return new AuthenticationResult(user, authorizerName, null); } else { return null; @@ -188,6 +211,21 @@ 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 @@ -218,7 +256,7 @@ public void doFilter( String user = splits[0]; char[] password = splits[1].toCharArray(); - if (dbConnector.checkCredentials(user, password)) { + 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); diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java index 8abd8da8d76e..7845555eedb3 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -21,12 +21,13 @@ 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.BasicAuthConfig; -import io.druid.security.basic.db.BasicSecurityStorageConnector; +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; @@ -42,21 +43,44 @@ @JsonTypeName("basic") public class BasicRoleBasedAuthorizer implements Authorizer { - private final BasicSecurityStorageConnector dbConnector; + 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 BasicSecurityStorageConnector dbConnector, - @JacksonInject BasicAuthConfig authConfig + @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(authConfig.getPermissionCacheSize()) + .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 @@ -66,7 +90,7 @@ public Access authorize( throw new IAE("WTF? authenticationResult should never be null."); } - List> permissions = dbConnector.getPermissionsForUser(authenticationResult.getIdentity()); + List> permissions = dbConnector.getPermissionsForUser(dbConfig.getDbPrefix(), authenticationResult.getIdentity()); for (Map permission : permissions) { if (permissionCheck(resource, action, permission)) { @@ -77,6 +101,16 @@ public Access authorize( 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"); @@ -95,4 +129,14 @@ private boolean permissionCheck(Resource resource, Action action, Map getModules() - { - return ImmutableList.of( - new Module() - { - @Override - public void configure(Binder binder) - { - JsonConfigProvider.bindInstance( - binder, Key.get(MetadataStorageConnectorConfig.class), new MetadataStorageConnectorConfig() - { - @Override - public String getConnectURI() - { - return connectURI; - } - - @Override - public String getUser() - { - return user; - } - - @Override - public String getPassword() - { - return password; - } - } - ); - JsonConfigProvider.bindInstance( - binder, - Key.get(DruidNode.class, Self.class), - new DruidNode("tools", "localhost", -1, null, true, false) - ); - } - } - ); - } - - @Override - public void run() - { - final Injector injector = makeInjector(); - BasicSecurityStorageConnector dbConnector = injector.getInstance(BasicSecurityStorageConnector.class); - ObjectMapper jsonMapper = injector.getInstance(ObjectMapper.class); - - dbConnector.createUserTable(); - dbConnector.createRoleTable(); - dbConnector.createPermissionTable(); - dbConnector.createUserRoleTable(); - dbConnector.createUserCredentialsTable(); - - setupDefaultAdmin(dbConnector, jsonMapper); - setupInternalDruidSystemUser(dbConnector, jsonMapper); - } - - private void setupInternalDruidSystemUser(BasicSecurityStorageConnector dbConnector, ObjectMapper jsonMapper) - { - if (systemUser == null) { - systemUser = DEFAULT_SYSTEM_USER_NAME; - } - - if (systemPassword == null) { - systemPassword = DEFAULT_SYSTEM_USER_PASS; - } - - if (systemRole == null) { - systemRole = DEFAULT_SYSTEM_USER_ROLE; - } - - dbConnector.createUser(systemUser); - dbConnector.createRole(systemRole); - dbConnector.assignRole(systemUser, systemRole); - - 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) { - dbConnector.addPermission(systemRole, resAct); - } - - dbConnector.setUserCredentials(systemUser, systemPassword.toCharArray()); - } - - private void setupDefaultAdmin(BasicSecurityStorageConnector dbConnector, ObjectMapper jsonMapper) - { - if (adminUser == null) { - adminUser = DEFAULT_ADMIN_NAME; - } - - if (adminPassword == null) { - adminPassword = DEFAULT_ADMIN_PASS; - } - - if (adminRole == null) { - adminRole = DEFAULT_ADMIN_ROLE; - } - - dbConnector.createUser(adminUser); - dbConnector.createRole(adminRole); - dbConnector.assignRole(adminUser, adminRole); - - 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) { - dbConnector.addPermission(adminRole, resAct); - } - - dbConnector.setUserCredentials(adminUser, adminPassword.toCharArray()); - } -} diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.java similarity index 64% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.java index cca977d7bbf8..81a96e814c3b 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/BasicAuthConfig.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthDBConfig.java @@ -17,20 +17,29 @@ * under the License. */ -package io.druid.security.basic; +package io.druid.security.basic.db; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class BasicAuthConfig +public class BasicAuthDBConfig { - @JsonProperty - private String initialAdminPassword = "druid"; - - @JsonProperty - private String initialInternalClientPassword = "druid"; + 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; + } - @JsonProperty - private int permissionCacheSize = 5000; + public String getDbPrefix() + { + return dbPrefix; + } public String getInitialAdminPassword() { @@ -41,9 +50,4 @@ public String getInitialInternalClientPassword() { return initialInternalClientPassword; } - - public int getPermissionCacheSize() - { - return permissionCacheSize; - } } diff --git a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java similarity index 53% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java index 505b09acf021..bd685437edac 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/cli/BasicSecurityCliCommandCreator.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicAuthenticatorStorageConnector.java @@ -17,16 +17,28 @@ * under the License. */ -package io.druid.security.basic.cli; +package io.druid.security.basic.db; -import io.airlift.airline.Cli; -import io.druid.cli.CliCommandCreator; +import java.util.List; +import java.util.Map; -public class BasicSecurityCliCommandCreator implements CliCommandCreator +public interface BasicAuthenticatorStorageConnector { - @Override - public void addCommands(Cli.CliBuilder builder) - { - builder.withGroup("tools").withCommands(CreateAuthorizationTables.class); - } + 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/BasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java deleted file mode 100644 index e4642228f2d4..000000000000 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/BasicSecurityStorageConnector.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Metamarkets Group Inc. (Metamarkets) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. Metamarkets licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package io.druid.security.basic.db; - -import io.druid.server.security.ResourceAction; - -import java.util.List; -import java.util.Map; - -/** - * Interface for classes that provide access to a database that contains user/role/permission/credential information. - */ -public interface BasicSecurityStorageConnector -{ - void createUser(String userName); - - void deleteUser(String userName); - - void createRole(String roleName); - - void deleteRole(String roleName); - - void addPermission(String roleName, ResourceAction resourceAction); - - void deletePermission(int permissionId); - - void assignRole(String userName, String roleName); - - void unassignRole(String userName, String roleName); - - List> getAllUsers(); - - List> getAllRoles(); - - Map getUser(String userName); - - Map getRole(String roleName); - - List> getRolesForUser(String userName); - - List> getUsersWithRole(String roleName); - - List> getPermissionsForRole(String roleName); - - List> getPermissionsForUser(String userName); - - void createRoleTable(); - - void createUserTable(); - - void createPermissionTable(); - - void createUserRoleTable(); - - void createUserCredentialsTable(); - - void setUserCredentials(String userName, char[] password); - - boolean checkCredentials(String userName, char[] password); - - Map getUserCredentials(String userName); -} 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/SQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java similarity index 63% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java index a41af4e7bb21..3d13ef14d209 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/SQLBasicAuthorizerStorageConnector.java @@ -27,15 +27,17 @@ 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.BasicAuthConfig; -import io.druid.security.basic.BasicAuthUtils; 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; @@ -50,33 +52,20 @@ import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -/** - * Base class for BasicSecurityStorageConnector implementations that interface with a database using SQL. - */ -public abstract class SQLBasicSecurityStorageConnector +public abstract class SQLBasicAuthorizerStorageConnector extends BaseSQLMetadataConnector - implements BasicSecurityStorageConnector + implements BasicAuthorizerStorageConnector { public static final String USERS = "users"; - public static final String USER_CREDENTIALS = "user_credentials"; public static final String PERMISSIONS = "permissions"; public static final String ROLES = "roles"; public static final String USER_ROLES = "user_roles"; - public static final List TABLE_NAMES = Lists.newArrayList( - USER_CREDENTIALS, - USER_ROLES, - PERMISSIONS, - ROLES, - USERS - ); - private static final String DEFAULT_ADMIN_NAME = "admin"; private static final String DEFAULT_ADMIN_ROLE = "admin"; @@ -84,23 +73,21 @@ public abstract class SQLBasicSecurityStorageConnector private static final String DEFAULT_SYSTEM_USER_ROLE = "druid_system"; private final Supplier config; - private final BasicAuthConfig basicAuthConfig; private final ObjectMapper jsonMapper; private final PermissionsMapper permMapper; - private final UserCredentialsMapper credsMapper; + private final Injector injector; @Inject - public SQLBasicSecurityStorageConnector( + public SQLBasicAuthorizerStorageConnector( Supplier config, - Supplier basicAuthConfigSupplier, + Injector injector, ObjectMapper jsonMapper ) { this.config = config; - this.basicAuthConfig = basicAuthConfigSupplier.get(); + this.injector = injector; this.jsonMapper = jsonMapper; this.permMapper = new PermissionsMapper(); - this.credsMapper = new UserCredentialsMapper(); this.shouldRetry = new Predicate() { @Override @@ -114,21 +101,59 @@ public boolean apply(Throwable e) @LifecycleStart public void start() { - createUserTable(); - createRoleTable(); - createPermissionTable(); - createUserRoleTable(); - createUserCredentialsTable(); - - makeDefaultSuperuser(DEFAULT_ADMIN_NAME, basicAuthConfig.getInitialAdminPassword(), DEFAULT_ADMIN_ROLE); - makeDefaultSuperuser(DEFAULT_SYSTEM_USER_NAME, basicAuthConfig.getInitialInternalClientPassword(), DEFAULT_SYSTEM_USER_ROLE); + 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() + public void createRoleTable(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + createTable( - ROLES, + roleTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -136,17 +161,19 @@ public void createRoleTable() + " PRIMARY KEY (name),\n" + " UNIQUE (name)\n" + ")", - ROLES + roleTableName ) ) ); } @Override - public void createUserTable() + public void createUserTable(String dbPrefix) { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + createTable( - USERS, + userTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -154,37 +181,20 @@ public void createUserTable() + " PRIMARY KEY (name),\n" + " UNIQUE (name)\n" + ")", - USERS + userTableName ) ) ); } @Override - public void createUserCredentialsTable() + public void createPermissionTable(String dbPrefix) { - createTable( - USER_CREDENTIALS, - 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 users(name) ON DELETE CASCADE\n" - + ")", - USER_CREDENTIALS - ) - ) - ); - } + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); - @Override - public void createPermissionTable() - { createTable( - PERMISSIONS, + permissionTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -192,76 +202,59 @@ public void createPermissionTable() + " resource_json VARCHAR(255) NOT NULL,\n" + " role_name INTEGER NOT NULL, \n" + " PRIMARY KEY (id),\n" - + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + ")", - PERMISSIONS + permissionTableName, + roleTableName ) ) ); } @Override - public void createUserRoleTable() + 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( - USER_ROLES, + 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 users(name) ON DELETE CASCADE,\n" - + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\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" + ")", - USER_ROLES + userRoleTableName, + userTableName, + roleTableName ) ) ); } - public MetadataStorageConnectorConfig getConfig() - { - return config.get(); - } - - 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; - } - - public String getValidationQuery() - { - return "SELECT 1"; - } - @Override - public void createUser(String userName) + 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, userName); + 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)", USERS + "INSERT INTO %1$s (name) VALUES (:user_name)", userTableName ) ) .bind("user_name", userName) @@ -273,21 +266,23 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void deleteUser(String userName) + 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, userName); + 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", USERS + "DELETE FROM %1$s WHERE name = :userName", userTableName ) ) .bind("userName", userName) @@ -299,21 +294,23 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void createRole(String roleName) + 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, roleName); + 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)", ROLES + "INSERT INTO %1$s (name) VALUES (:roleName)", roleTableName ) ) .bind("roleName", roleName) @@ -325,21 +322,23 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void deleteRole(String roleName) + 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, roleName); + 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", ROLES + "DELETE FROM %1$s WHERE name = :roleName", roleTableName ) ) .bind("roleName", roleName) @@ -351,15 +350,17 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void addPermission(String roleName, ResourceAction resourceAction) + 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, roleName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -380,7 +381,7 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th handle.createStatement( StringUtils.format( "INSERT INTO %1$s (resource_json, role_name) VALUES (:resourceJson, :roleName)", - PERMISSIONS + permissionTableName ) ) .bind("resourceJson", serializedResourceAction) @@ -398,21 +399,23 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void deletePermission(int permissionId) + 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, permissionId); + 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", PERMISSIONS + "DELETE FROM %1$s WHERE id = :permissionId", permissionTableName ) ) .bind("permissionId", permissionId) @@ -424,16 +427,18 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void assignRole(String userName, String roleName) + 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, userName); - int roleCount = getRoleCount(handle, roleName); + int userCount = getUserCount(handle, dbPrefix, userName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -443,14 +448,14 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } - int userRoleMappingCount = getUserRoleMappingCount(handle, userName, 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)", USER_ROLES + "INSERT INTO %1$s (user_name, role_name) VALUES (:userName, :roleName)", userRoleTableName ) ) .bind("userName", userName) @@ -463,16 +468,18 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public void unassignRole(String userName, String roleName) + 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, userName); - int roleCount = getRoleCount(handle, roleName); + int userCount = getUserCount(handle, dbPrefix, userName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); @@ -482,14 +489,14 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } - int userRoleMappingCount = getUserRoleMappingCount(handle, userName, 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", USER_ROLES + "DELETE FROM %1$s WHERE user_name = :userName AND role_name = :roleName", userRoleTableName ) ) .bind("userName", userName) @@ -503,8 +510,10 @@ public Void inTransaction(Handle handle, TransactionStatus transactionStatus) th } @Override - public List> getAllUsers() + public List> getAllUsers(String dbPrefix) { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + return getDBI().inTransaction( new TransactionCallback>>() { @@ -514,7 +523,7 @@ public List> inTransaction(Handle handle, TransactionStatus { return handle .createQuery( - StringUtils.format("SELECT * FROM users") + StringUtils.format("SELECT * FROM %1$s", userTableName) ) .list(); } @@ -523,8 +532,10 @@ public List> inTransaction(Handle handle, TransactionStatus } @Override - public List> getAllRoles() + public List> getAllRoles(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + return getDBI().inTransaction( new TransactionCallback>>() { @@ -534,7 +545,7 @@ public List> inTransaction(Handle handle, TransactionStatus { return handle .createQuery( - StringUtils.format("SELECT * FROM roles") + StringUtils.format("SELECT * FROM %1$s", roleTableName) ) .list(); } @@ -543,8 +554,10 @@ public List> inTransaction(Handle handle, TransactionStatus } @Override - public Map getUser(String userName) + public Map getUser(String dbPrefix, String userName) { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + return getDBI().inTransaction( new TransactionCallback>() { @@ -553,7 +566,7 @@ public Map inTransaction(Handle handle, TransactionStatus transa { return handle .createQuery( - StringUtils.format("SELECT * FROM users where name = :userName") + StringUtils.format("SELECT * FROM %1$s where name = :userName", userTableName) ) .bind("userName", userName) .first(); @@ -563,8 +576,10 @@ public Map inTransaction(Handle handle, TransactionStatus transa } @Override - public Map getRole(String roleName) + public Map getRole(String dbPrefix, String roleName) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + return getDBI().inTransaction( new TransactionCallback>() { @@ -573,7 +588,7 @@ public Map inTransaction(Handle handle, TransactionStatus transa { return handle .createQuery( - StringUtils.format("SELECT * FROM roles where name = :roleName") + StringUtils.format("SELECT * FROM %1$s where name = :roleName", roleTableName) ) .bind("roleName", roleName) .first(); @@ -583,8 +598,11 @@ public Map inTransaction(Handle handle, TransactionStatus transa } @Override - public List> getRolesForUser(String userName) + 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>>() { @@ -592,7 +610,7 @@ public List> getRolesForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCount(handle, userName); + int userCount = getUserCount(handle, dbPrefix, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -600,11 +618,13 @@ public List> inTransaction(Handle handle, TransactionStatus return handle .createQuery( StringUtils.format( - "SELECT roles.name\n" - + "FROM roles\n" - + "JOIN user_roles\n" - + " ON user_roles.role_name = roles.name\n" - + "WHERE user_roles.user_name = :userName" + "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) @@ -615,8 +635,11 @@ public List> inTransaction(Handle handle, TransactionStatus } @Override - public List> getUsersWithRole(String roleName) + 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>>() { @@ -624,7 +647,7 @@ public List> getUsersWithRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int roleCount = getRoleCount(handle, roleName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -632,11 +655,13 @@ public List> inTransaction(Handle handle, TransactionStatus return handle .createQuery( StringUtils.format( - "SELECT users.name\n" - + "FROM users\n" - + "JOIN user_roles\n" - + " ON user_roles.user_name = users.name\n" - + "WHERE user_roles.role_name = :roleName" + "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) @@ -646,6 +671,16 @@ public List> inTransaction(Handle handle, TransactionStatus ); } + 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 @@ -668,8 +703,10 @@ public Map map(int index, ResultSet resultSet, StatementContext } @Override - public List> getPermissionsForRole(String roleName) + public List> getPermissionsForRole(String dbPrefix, String roleName) { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + return getDBI().inTransaction( new TransactionCallback>>() { @@ -677,7 +714,7 @@ public List> getPermissionsForRole(String roleName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int roleCount = getRoleCount(handle, roleName); + int roleCount = getRoleCount(handle, dbPrefix, roleName); if (roleCount == 0) { throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName); } @@ -685,9 +722,10 @@ public List> inTransaction(Handle handle, TransactionStatus return handle .createQuery( StringUtils.format( - "SELECT permissions.id, permissions.resource_json\n" - + "FROM permissions\n" - + "WHERE permissions.role_name = :roleName" + "SELECT %1$s.id, %1$s.resource_json\n" + + "FROM %1$s\n" + + "WHERE %1$s.role_name = :roleName", + permissionTableName ) ) .map(permMapper) @@ -699,8 +737,12 @@ public List> inTransaction(Handle handle, TransactionStatus } @Override - public List> getPermissionsForUser(String userName) + 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>>() { @@ -708,7 +750,7 @@ public List> getPermissionsForUser(String userName) public List> inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception { - int userCount = getUserCount(handle, userName); + int userCount = getUserCount(handle, dbPrefix, userName); if (userCount == 0) { throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName); } @@ -716,13 +758,16 @@ public List> inTransaction(Handle handle, TransactionStatus return handle .createQuery( StringUtils.format( - "SELECT permissions.id, permissions.resource_json, roles.name\n" - + "FROM permissions\n" - + "JOIN roles\n" - + " ON permissions.role_name = roles.name\n" - + "JOIN user_roles\n" - + " ON user_roles.role_name = roles.name\n" - + "WHERE user_roles.user_name = :userName" + "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) @@ -733,192 +778,84 @@ public List> inTransaction(Handle handle, TransactionStatus ); } - private static class UserCredentialsMapper implements ResultSetMapper> + public MetadataStorageConnectorConfig getConfig() { - @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 - ); - } + return config.get(); } - @Override - public Map getUserCredentials(String userName) + public String getValidationQuery() { - return getDBI().inTransaction( - new TransactionCallback>() - { - @Override - public Map inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - int userCount = getUserCount(handle, 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", USER_CREDENTIALS) - ) - .map(credsMapper) - .bind("userName", userName) - .first(); - } - } - ); + return "SELECT 1"; } - @Override - public void setUserCredentials(String userName, char[] password) + protected BasicDataSource getDatasource() { - getDBI().inTransaction( - new TransactionCallback() - { - @Override - public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception - { - int userCount = getUserCount(handle, 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", - USER_CREDENTIALS - ) - ) - .bind("userName", userName) - .first(); + MetadataStorageConnectorConfig connectorConfig = getConfig(); - int iterations = BasicAuthUtils.KEY_ITERATIONS; - byte[] salt = BasicAuthUtils.generateSalt(); - byte[] hash = BasicAuthUtils.hashPassword(password, salt, iterations); + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setUsername(connectorConfig.getUser()); + dataSource.setPassword(connectorConfig.getPassword()); + String uri = connectorConfig.getConnectURI(); + dataSource.setUrl(uri); - if (existingMapping == null) { - handle.createStatement( - StringUtils.format( - "INSERT INTO %1$s (user_name, salt, hash, iterations) " + - "VALUES (:userName, :salt, :hash, :iterations)", - USER_CREDENTIALS - ) - ) - .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", - USER_CREDENTIALS - ) - ) - .bind("userName", userName) - .bind("salt", salt) - .bind("hash", hash) - .bind("iterations", iterations) - .execute(); - } + dataSource.setValidationQuery(getValidationQuery()); + dataSource.setTestOnBorrow(true); - return null; - } - } - ); + return dataSource; } - @Override - public boolean checkCredentials(String userName, char[] password) + protected static String getPrefixedTableName(String dbPrefix, String baseTableName) { - 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", - USER_CREDENTIALS - ) - ) - .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); - } - } - ); + return StringUtils.format("basic_authorization_%s_%s", dbPrefix, baseTableName); } - private int getUserCount(Handle handle, String userName) + 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", USERS, "name") + 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 roleName) + 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", ROLES, "name") + 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, int permissionId) + 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", PERMISSIONS, "id") + 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 userName, String roleName) + 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", - USER_ROLES, + userRoleTableName, "user_name", "role_name" ) @@ -929,15 +866,15 @@ private int getUserRoleMappingCount(Handle handle, String userName, String roleN .first(); } - private void makeDefaultSuperuser(String username, String password, String role) + private void makeDefaultSuperuser(String dbPrefix, String username, String role) { - if (getUser(username) != null) { + if (getUser(dbPrefix, username) != null) { return; } - createUser(username); - createRole(role); - assignRole(username, role); + createUser(dbPrefix, username); + createRole(dbPrefix, role); + assignRole(dbPrefix, username, role); ResourceAction datasourceR = new ResourceAction( new Resource(".*", ResourceType.DATASOURCE), @@ -972,9 +909,7 @@ private void makeDefaultSuperuser(String username, String password, String role) List resActs = Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); for (ResourceAction resAct : resActs) { - addPermission(role, resAct); + addPermission(dbPrefix, role, resAct); } - - setUserCredentials(username, password.toCharArray()); } } 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/DerbySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java similarity index 70% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java index 13662c4e68c6..aba4c0eaef65 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/derby/DerbySQLBasicAuthorizerStorageConnector.java @@ -23,35 +23,35 @@ 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.BasicAuthConfig; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +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 DerbySQLBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +public class DerbySQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector { - private static final Logger log = new Logger(DerbySQLBasicSecurityStorageConnector.class); + private static final Logger log = new Logger(DerbySQLBasicAuthorizerStorageConnector.class); private final DBI dbi; private final MetadataStorage storage; @Inject - public DerbySQLBasicSecurityStorageConnector( + public DerbySQLBasicAuthorizerStorageConnector( MetadataStorage storage, Supplier config, - Supplier basicAuthConfigSupplier, + Injector injector, ObjectMapper jsonMapper ) { - super(config, basicAuthConfigSupplier, jsonMapper); + super(config, injector, jsonMapper); final BasicDataSource datasource = getDatasource(); datasource.setDriverClassLoader(getClass().getClassLoader()); @@ -62,20 +62,19 @@ public DerbySQLBasicSecurityStorageConnector( log.info("Derby connector instantiated with metadata storage [%s].", this.storage.getClass().getName()); } - public DerbySQLBasicSecurityStorageConnector( + public DerbySQLBasicAuthorizerStorageConnector( MetadataStorage storage, Supplier config, - Supplier basicAuthConfigSupplier, + Injector injector, ObjectMapper jsonMapper, DBI dbi ) { - super(config, basicAuthConfigSupplier, jsonMapper); + super(config, injector, jsonMapper); this.dbi = dbi; this.storage = storage; } - @Override @LifecycleStart public void start() @@ -85,44 +84,51 @@ public void start() } @Override - public void createRoleTable() + public void createRoleTable(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + createTable( - ROLES, + roleTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" + " name VARCHAR(255) NOT NULL,\n" + " PRIMARY KEY (name)\n" + ")", - ROLES + roleTableName ) ) ); } @Override - public void createUserTable() + public void createUserTable(String dbPrefix) { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + createTable( - USERS, + userTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" + " name VARCHAR(255) NOT NULL,\n" + " PRIMARY KEY (name)\n" + ")", - USERS + userTableName ) ) ); } @Override - public void createPermissionTable() + public void createPermissionTable(String dbPrefix) { + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + createTable( - PERMISSIONS, + permissionTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -130,36 +136,15 @@ public void createPermissionTable() + " resource_json BLOB(1024) NOT NULL,\n" + " role_name VARCHAR(255) NOT NULL, \n" + " PRIMARY KEY (id),\n" - + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + ")", - PERMISSIONS + permissionTableName, + roleTableName ) ) ); } - @Override - public void createUserCredentialsTable() - { - createTable( - USER_CREDENTIALS, - 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 users(name) ON DELETE CASCADE\n" - + ")", - USER_CREDENTIALS - ) - ) - ); - } - - @Override public boolean tableExists(Handle handle, String tableName) { 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/MySQLBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java similarity index 72% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java index 6d7c7bea0095..31a33d672ce4 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/mysql/MySQLBasicAuthorizerStorageConnector.java @@ -23,31 +23,31 @@ 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.BasicAuthConfig; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +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 MySQLBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +public class MySQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector { - private static final Logger log = new Logger(MySQLBasicSecurityStorageConnector.class); + private static final Logger log = new Logger(MySQLBasicAuthorizerStorageConnector.class); private final DBI dbi; @Inject - public MySQLBasicSecurityStorageConnector( + public MySQLBasicAuthorizerStorageConnector( Supplier config, - Supplier basicAuthConfigSupplier, + Injector injector, ObjectMapper jsonMapper ) { - super(config, basicAuthConfigSupplier, jsonMapper); + super(config, injector, jsonMapper); final BasicDataSource datasource = getDatasource(); datasource.setDriverClassLoader(getClass().getClassLoader()); @@ -61,10 +61,12 @@ public MySQLBasicSecurityStorageConnector( } @Override - public void createRoleTable() + public void createRoleTable(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + createTable( - ROLES, + roleTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -72,17 +74,19 @@ public void createRoleTable() + " PRIMARY KEY (name),\n" + " UNIQUE (name)\n" + ")", - ROLES + roleTableName ) ) ); } @Override - public void createUserTable() + public void createUserTable(String dbPrefix) { + final String userTableName = getPrefixedTableName(dbPrefix, USERS); + createTable( - USERS, + userTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -90,17 +94,20 @@ public void createUserTable() + " PRIMARY KEY (name),\n" + " UNIQUE (name)\n" + ")", - USERS + userTableName ) ) ); } @Override - public void createPermissionTable() + public void createPermissionTable(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + createTable( - PERMISSIONS, + permissionTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -108,36 +115,15 @@ public void createPermissionTable() + " resource_json BLOB(1024) NOT NULL,\n" + " role_name VARCHAR(255) NOT NULL, \n" + " PRIMARY KEY (id),\n" - + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\n" - + ")", - PERMISSIONS - ) - ) - ); - } - - @Override - public void createUserCredentialsTable() - { - createTable( - USER_CREDENTIALS, - 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 users(name) ON DELETE CASCADE\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + ")", - USER_CREDENTIALS + permissionTableName, + roleTableName ) ) ); } - @Override public boolean tableExists(Handle handle, String tableName) { 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/PostgresBasicSecurityStorageConnector.java b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java similarity index 68% rename from extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java rename to extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java index 9a45116ce030..8e8a596dd712 100644 --- a/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgresBasicSecurityStorageConnector.java +++ b/extensions-core/druid-basic-security/src/main/java/io/druid/security/basic/db/postgres/PostgreSQLBasicAuthorizerStorageConnector.java @@ -23,30 +23,30 @@ 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.BasicAuthConfig; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; +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 PostgresBasicSecurityStorageConnector extends SQLBasicSecurityStorageConnector +public class PostgreSQLBasicAuthorizerStorageConnector extends SQLBasicAuthorizerStorageConnector { - private static final Logger log = new Logger(PostgresBasicSecurityStorageConnector.class); + private static final Logger log = new Logger(PostgreSQLBasicAuthorizerStorageConnector.class); private final DBI dbi; @Inject - public PostgresBasicSecurityStorageConnector( + public PostgreSQLBasicAuthorizerStorageConnector( Supplier config, - Supplier basicAuthConfigSupplier, + Injector injector, ObjectMapper jsonMapper ) { - super(config, basicAuthConfigSupplier, jsonMapper); + super(config, injector, jsonMapper); final BasicDataSource datasource = getDatasource(); // PostgreSQL driver is classloader isolated as part of the extension @@ -59,7 +59,6 @@ public PostgresBasicSecurityStorageConnector( log.info("Configured PostgreSQL as security storage"); } - @Override public boolean tableExists(final Handle handle, final String tableName) { @@ -79,10 +78,13 @@ public DBI getDBI() } @Override - public void createPermissionTable() + public void createPermissionTable(String dbPrefix) { + final String roleTableName = getPrefixedTableName(dbPrefix, ROLES); + final String permissionTableName = getPrefixedTableName(dbPrefix, PERMISSIONS); + createTable( - PERMISSIONS, + permissionTableName, ImmutableList.of( StringUtils.format( "CREATE TABLE %1$s (\n" @@ -90,30 +92,10 @@ public void createPermissionTable() + " resource_json BYTEA NOT NULL,\n" + " role_name VARCHAR(255) NOT NULL, \n" + " PRIMARY KEY (id),\n" - + " FOREIGN KEY (role_name) REFERENCES roles(name) ON DELETE CASCADE\n" - + ")", - PERMISSIONS - ) - ) - ); - } - - @Override - public void createUserCredentialsTable() - { - createTable( - USER_CREDENTIALS, - 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 users(name) ON DELETE CASCADE\n" + + " FOREIGN KEY (role_name) REFERENCES %2$s(name) ON DELETE CASCADE\n" + ")", - USER_CREDENTIALS + permissionTableName, + roleTableName ) ) ); diff --git a/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator b/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator deleted file mode 100644 index 3ff5ce132e60..000000000000 --- a/extensions-core/druid-basic-security/src/main/resources/META-INF/services/io.druid.cli.CliCommandCreator +++ /dev/null @@ -1 +0,0 @@ -io.druid.security.basic.cli.BasicSecurityCliCommandCreator \ No newline at end of file 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 index 3f966d8e6b17..456ca3d638c1 100644 --- 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 @@ -20,10 +20,8 @@ package io.druid.security; import io.druid.java.util.common.StringUtils; -import io.druid.security.basic.BasicAuthConfig; import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; -import io.druid.security.db.TestDerbySecurityConnector; +import io.druid.security.db.TestDerbyAuthorizerStorageConnector; import io.druid.server.security.Access; import io.druid.server.security.Action; import io.druid.server.security.AuthenticationResult; @@ -40,42 +38,35 @@ public class BasicRoleBasedAuthorizerTest { + private static final String TEST_DB_PREFIX = "test"; + @Rule - public final TestDerbySecurityConnector.DerbyConnectorRule derbyConnectorRule = - new TestDerbySecurityConnector.DerbyConnectorRule(); + public final TestDerbyAuthorizerStorageConnector.DerbyConnectorRule derbyConnectorRule = + new TestDerbyAuthorizerStorageConnector.DerbyConnectorRule(TEST_DB_PREFIX); private BasicRoleBasedAuthorizer authorizer; - private TestDerbySecurityConnector connector; - private BasicAuthConfig authConfig; + private TestDerbyAuthorizerStorageConnector connector; @Before public void setUp() throws Exception { connector = derbyConnectorRule.getConnector(); createAllTables(); - authConfig = new BasicAuthConfig() { - - @Override - public int getPermissionCacheSize() - { - return 500; - } - }; - authorizer = new BasicRoleBasedAuthorizer(connector, authConfig); + authorizer = new BasicRoleBasedAuthorizer(connector, TEST_DB_PREFIX, 5000); } @Test public void testAuth() { - connector.createUser("druid"); - connector.createRole("druidRole"); - connector.assignRole("druid", "druidRole"); + 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("druidRole", permission); + connector.addPermission(TEST_DB_PREFIX, "druidRole", permission); AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null); @@ -94,20 +85,19 @@ public void testAuth() Assert.assertFalse(access.isAllowed()); } - @Test public void testMorePermissionsThanCacheSize() { - connector.createUser("druid"); - connector.createRole("druidRole"); - connector.assignRole("druid", "druidRole"); + connector.createUser(TEST_DB_PREFIX, "druid"); + connector.createRole(TEST_DB_PREFIX, "druidRole"); + connector.assignRole(TEST_DB_PREFIX, "druid", "druidRole"); - for (int i = 0; i < authConfig.getPermissionCacheSize() + 50; i++) { + for (int i = 0; i < authorizer.getPermissionCacheSize() + 50; i++) { ResourceAction permission = new ResourceAction( new Resource("testResource-" + i, ResourceType.DATASOURCE), Action.WRITE ); - connector.addPermission("druidRole", permission); + connector.addPermission(TEST_DB_PREFIX, "druidRole", permission); } AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null); @@ -135,16 +125,15 @@ public void tearDown() throws Exception private void createAllTables() { - connector.createUserTable(); - connector.createRoleTable(); - connector.createPermissionTable(); - connector.createUserRoleTable(); - connector.createUserCredentialsTable(); + 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 : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + for (String table : connector.getTableNames(TEST_DB_PREFIX)) { dropTable(table); } } 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/BasicSecurityResourceTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthorizerResourceTest.java similarity index 66% rename from extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthorizerResourceTest.java index 346b982fe1a6..2f8e280a0d08 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/BasicSecurityResourceTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/BasicAuthorizerResourceTest.java @@ -17,16 +17,16 @@ * under the License. */ -package io.druid.security; +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.BasicSecurityResource; -import io.druid.security.basic.db.SQLBasicSecurityStorageConnector; -import io.druid.security.db.TestDerbySecurityConnector; +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; @@ -45,26 +45,39 @@ import java.util.List; import java.util.Map; -public class BasicSecurityResourceTest +public class BasicAuthorizerResourceTest { - private BasicSecurityResource basicSecurityResource; + private static final String BASIC_AUTHORIZER_NAME = "basic"; + private static final String BASIC_AUTHORIZER_NAME2 = "basic2"; + + private BasicAuthorizerResource resource; private HttpServletRequest req; - private TestDerbySecurityConnector connector; + private TestDerbyAuthorizerStorageConnector connector; @Rule - public final TestDerbySecurityConnector.DerbyConnectorRule derbyConnectorRule = - new TestDerbySecurityConnector.DerbyConnectorRule(); + 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(); - basicSecurityResource = new BasicSecurityResource(connector); + resource = new BasicAuthorizerResource(connector, mapper); } @After @@ -74,112 +87,119 @@ public void tearDown() throws Exception } @Test - public void testGetAllUsers() + public void testSeparateDatabaseTables() { - Response response = basicSecurityResource.getAllUsers(req); + Response response = resource.getAllUsers(req, BASIC_AUTHORIZER_NAME); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(ImmutableList.of(), response.getEntity()); - basicSecurityResource.createUser(req, "druid"); - basicSecurityResource.createUser(req, "druid2"); - basicSecurityResource.createUser(req, "druid3"); + 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 = basicSecurityResource.getAllUsers(req); + 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 testGetAllRoles() + public void testInvalidAuthorizer() { - Response response = basicSecurityResource.getAllRoles(req); + 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()); - basicSecurityResource.createRole(req, "druid"); - basicSecurityResource.createRole(req, "druid2"); - basicSecurityResource.createRole(req, "druid3"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); + resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid3"); - List> expectedRoles = ImmutableList.of( + List> expectedUsers = ImmutableList.of( ImmutableMap.of("name", "druid"), ImmutableMap.of("name", "druid2"), ImmutableMap.of("name", "druid3") ); - response = basicSecurityResource.getAllRoles(req); + response = resource.getAllUsers(req, BASIC_AUTHORIZER_NAME); Assert.assertEquals(200, response.getStatus()); - Assert.assertEquals(expectedRoles, response.getEntity()); + Assert.assertEquals(expectedUsers, response.getEntity()); } + @Test - public void testCreateDeleteUser() + public void testGetAllRoles() { - Response response = basicSecurityResource.createUser(req, "druid"); + Response response = resource.getAllRoles(req, BASIC_AUTHORIZER_NAME); Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(ImmutableList.of(), response.getEntity()); - response = basicSecurityResource.getUser(req, "druid"); - Assert.assertEquals(200, response.getStatus()); - Map expectedUser = ImmutableMap.of( - "user", ImmutableMap.of("name", "druid"), - "roles", ImmutableList.of(), - "permissions", ImmutableList.of() + 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") ); - Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.deleteUser(req, "druid"); + response = resource.getAllRoles(req, BASIC_AUTHORIZER_NAME); Assert.assertEquals(200, response.getStatus()); - - response = basicSecurityResource.deleteUser(req, "druid"); - Assert.assertEquals(400, response.getStatus()); - Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); - - response = basicSecurityResource.getUser(req, "druid"); - Assert.assertEquals(400, response.getStatus()); - Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); + Assert.assertEquals(expectedRoles, response.getEntity()); } @Test - public void testUserCredentials() + public void testCreateDeleteUser() { - Response response = basicSecurityResource.createUser(req, "druid"); - Assert.assertEquals(200, response.getStatus()); - - response = basicSecurityResource.updateUserCredentials(req, "druid", "helloworld"); + Response response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUserCredentials(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - Map ent = (Map ) response.getEntity(); - Assert.assertEquals("druid", ent.get("user_name")); - byte[] salt = (byte[]) ent.get("salt"); - byte[] hash = (byte[]) ent.get("hash"); - int iterations = (Integer) ent.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 + Map expectedUser = ImmutableMap.of( + "user", ImmutableMap.of("name", "druid"), + "roles", ImmutableList.of(), + "permissions", ImmutableList.of() ); - Assert.assertArrayEquals(recalculatedHash, hash); + Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.deleteUser(req, "druid"); + response = resource.deleteUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUserCredentials(req, "druid"); + response = resource.deleteUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); - response = basicSecurityResource.updateUserCredentials(req, "druid", "helloworld"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity()); } @@ -187,10 +207,10 @@ public void testUserCredentials() @Test public void testCreateDeleteRole() { - Response response = basicSecurityResource.createRole(req, "druidRole"); + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); Map expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -199,14 +219,14 @@ public void testCreateDeleteRole() ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.deleteRole(req, "druidRole"); + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.deleteRole(req, "druidRole"); + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity()); } @@ -214,16 +234,16 @@ public void testCreateDeleteRole() @Test public void testRoleAssignment() throws Exception { - Response response = basicSecurityResource.createRole(req, "druidRole"); + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createUser(req, "druid"); + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); Map expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -232,7 +252,7 @@ public void testRoleAssignment() throws Exception ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); Map expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -241,10 +261,10 @@ public void testRoleAssignment() throws Exception ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.unassignRoleFromUser(req, "druid", "druidRole"); + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -253,7 +273,7 @@ public void testRoleAssignment() throws Exception ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -266,22 +286,22 @@ public void testRoleAssignment() throws Exception @Test public void testDeleteAssignedRole() { - Response response = basicSecurityResource.createRole(req, "druidRole"); + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createUser(req, "druid"); + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createUser(req, "druid2"); + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); Map expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -290,7 +310,7 @@ public void testDeleteAssignedRole() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getUser(req, "druid2"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); Map expectedUser2 = ImmutableMap.of( "user", ImmutableMap.of("name", "druid2"), @@ -299,7 +319,7 @@ public void testDeleteAssignedRole() ); Assert.assertEquals(expectedUser2, response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); Map expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -308,10 +328,10 @@ public void testDeleteAssignedRole() ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.deleteRole(req, "druidRole"); + response = resource.deleteRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -320,7 +340,7 @@ public void testDeleteAssignedRole() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getUser(req, "druid2"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); expectedUser2 = ImmutableMap.of( "user", ImmutableMap.of("name", "druid2"), @@ -333,7 +353,7 @@ public void testDeleteAssignedRole() @Test public void testRolesAndPerms() { - Response response = basicSecurityResource.createRole(req, "druidRole"); + Response response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); List perms = ImmutableList.of( @@ -342,14 +362,14 @@ public void testRolesAndPerms() new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE) ); - response = basicSecurityResource.addPermissionsToRole(req, "druidRole", perms); + response = resource.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole", perms); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.addPermissionsToRole(req, "wrongRole", perms); + 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 = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); Map expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -362,14 +382,14 @@ public void testRolesAndPerms() ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.deletePermission(req, 7); + 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 = basicSecurityResource.deletePermission(req, 2); + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 2); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -385,16 +405,16 @@ public void testRolesAndPerms() @Test public void testUsersRolesAndPerms() { - Response response = basicSecurityResource.createUser(req, "druid"); + Response response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createUser(req, "druid2"); + response = resource.createUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createRole(req, "druidRole"); + response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.createRole(req, "druidRole2"); + response = resource.createRole(req, BASIC_AUTHORIZER_NAME, "druidRole2"); Assert.assertEquals(200, response.getStatus()); List perms = ImmutableList.of( @@ -409,25 +429,25 @@ public void testUsersRolesAndPerms() new ResourceAction(new Resource("F", ResourceType.CONFIG), Action.WRITE) ); - response = basicSecurityResource.addPermissionsToRole(req, "druidRole", perms); + response = resource.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole", perms); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.addPermissionsToRole(req, "druidRole2", perms2); + response = resource.addPermissionsToRole(req, BASIC_AUTHORIZER_NAME, "druidRole2", perms2); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid", "druidRole2"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole2"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.assignRoleToUser(req, "druid2", "druidRole2"); + response = resource.assignRoleToUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole2"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); Map expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -443,7 +463,7 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getUser(req, "druid2"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid2"), @@ -459,7 +479,7 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole"); Assert.assertEquals(200, response.getStatus()); Map expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole"), @@ -472,7 +492,7 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.getRole(req, "druidRole2"); + response = resource.getRole(req, BASIC_AUTHORIZER_NAME, "druidRole2"); Assert.assertEquals(200, response.getStatus()); expectedRole = ImmutableMap.of( "role", ImmutableMap.of("name", "druidRole2"), @@ -485,13 +505,13 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedRole, response.getEntity()); - response = basicSecurityResource.deletePermission(req, 1); + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 1); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.deletePermission(req, 4); + response = resource.deletePermission(req, BASIC_AUTHORIZER_NAME, 4); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -505,7 +525,7 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getUser(req, "druid2"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid2"), @@ -519,13 +539,13 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.unassignRoleFromUser(req, "druid", "druidRole"); + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid", "druidRole"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.unassignRoleFromUser(req, "druid2", "druidRole2"); + response = resource.unassignRoleFromUser(req, BASIC_AUTHORIZER_NAME, "druid2", "druidRole2"); Assert.assertEquals(200, response.getStatus()); - response = basicSecurityResource.getUser(req, "druid"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid"), @@ -537,7 +557,7 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); - response = basicSecurityResource.getUser(req, "druid2"); + response = resource.getUser(req, BASIC_AUTHORIZER_NAME, "druid2"); Assert.assertEquals(200, response.getStatus()); expectedUser = ImmutableMap.of( "user", ImmutableMap.of("name", "druid2"), @@ -549,19 +569,27 @@ public void testUsersRolesAndPerms() ); Assert.assertEquals(expectedUser, response.getEntity()); } - + private void createAllTables() { - connector.createUserTable(); - connector.createRoleTable(); - connector.createPermissionTable(); - connector.createUserRoleTable(); - connector.createUserCredentialsTable(); + 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 : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + for (String table : connector.getTableNames(BASIC_AUTHORIZER_NAME)) { + dropTable(table); + } + + for (String table : connector.getTableNames(BASIC_AUTHORIZER_NAME2)) { dropTable(table); } } 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/SQLBasicSecurityStorageConnectorTest.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java similarity index 59% rename from extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java index 4df73ab75143..d775736ddd33 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicSecurityStorageConnectorTest.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/SQLBasicAuthorizerStorageConnectorTest.java @@ -22,8 +22,6 @@ 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.db.SQLBasicSecurityStorageConnector; import io.druid.server.security.Action; import io.druid.server.security.Resource; import io.druid.server.security.ResourceAction; @@ -41,20 +39,23 @@ import java.util.List; import java.util.Map; -public class SQLBasicSecurityStorageConnectorTest +public class SQLBasicAuthorizerStorageConnectorTest { + private final String TEST_DB_PREFIX = "test"; + @Rule public ExpectedException expectedException = ExpectedException.none(); @Rule - public final TestDerbySecurityConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbySecurityConnector.DerbyConnectorRule(); + public final TestDerbyAuthorizerStorageConnector.DerbyConnectorRule authorizerRule = + new TestDerbyAuthorizerStorageConnector.DerbyConnectorRule(TEST_DB_PREFIX); - private TestDerbySecurityConnector connector; + private TestDerbyAuthorizerStorageConnector authorizerConnector; @Before public void setUp() throws Exception { - connector = derbyConnectorRule.getConnector(); + authorizerConnector = authorizerRule.getConnector(); createAllTables(); } @@ -67,16 +68,16 @@ public void tearDown() throws Exception @Test public void testCreateTables() throws Exception { - connector.getDBI().withHandle( + authorizerConnector.getDBI().withHandle( new HandleCallback() { @Override public Void withHandle(Handle handle) throws Exception { - for (String table : SQLBasicSecurityStorageConnector.TABLE_NAMES) { + for (String table : authorizerConnector.getTableNames(TEST_DB_PREFIX)) { Assert.assertTrue( - StringUtils.format("table %s was not created!", table), - connector.tableExists(handle, table) + StringUtils.format("authorization table %s was not created!", table), + authorizerConnector.tableExists(handle, table) ); } @@ -90,15 +91,15 @@ public Void withHandle(Handle handle) throws Exception @Test public void testCreateDeleteUser() throws Exception { - connector.createUser("druid"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); Map expectedUser = ImmutableMap.of( "name", "druid" ); - Map dbUser = connector.getUser("druid"); + Map dbUser = authorizerConnector.getUser(TEST_DB_PREFIX, "druid"); Assert.assertEquals(expectedUser, dbUser); - connector.deleteUser("druid"); - dbUser = connector.getUser("druid"); + authorizerConnector.deleteUser(TEST_DB_PREFIX, "druid"); + dbUser = authorizerConnector.getUser(TEST_DB_PREFIX, "druid"); Assert.assertEquals(null, dbUser); } @@ -107,7 +108,7 @@ public void testDeleteNonExistentUser() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [druid] does not exist."); - connector.deleteUser("druid"); + authorizerConnector.deleteUser(TEST_DB_PREFIX, "druid"); } @Test @@ -115,19 +116,19 @@ public void testCreateDuplicateUser() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [druid] already exists."); - connector.createUser("druid"); - connector.createUser("druid"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); } // role tests @Test public void testCreateRole() throws Exception { - connector.createRole("druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); Map expectedRole = ImmutableMap.of( "name", "druid" ); - Map dbRole = connector.getRole("druid"); + Map dbRole = authorizerConnector.getRole(TEST_DB_PREFIX, "druid"); Assert.assertEquals(expectedRole, dbRole); } @@ -136,7 +137,7 @@ public void testDeleteNonExistentRole() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("Role [druid] does not exist."); - connector.deleteRole("druid"); + authorizerConnector.deleteRole(TEST_DB_PREFIX, "druid"); } @Test @@ -144,17 +145,17 @@ public void testCreateDuplicateRole() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("Role [druid] already exists."); - connector.createRole("druid"); - connector.createRole("druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); } // role and user tests @Test public void testAddAndRemoveRole() throws Exception { - connector.createUser("druid"); - connector.createRole("druidRole"); - connector.assignRole("druid", "druidRole"); + 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") @@ -164,15 +165,15 @@ public void testAddAndRemoveRole() throws Exception ImmutableMap.of("name", "druidRole") ); - List> usersWithRole = connector.getUsersWithRole("druidRole"); - List> rolesForUser = connector.getRolesForUser("druid"); + List> usersWithRole = authorizerConnector.getUsersWithRole(TEST_DB_PREFIX, "druidRole"); + List> rolesForUser = authorizerConnector.getRolesForUser(TEST_DB_PREFIX, "druid"); Assert.assertEquals(expectedUsersWithRole, usersWithRole); Assert.assertEquals(expectedRolesForUser, rolesForUser); - connector.unassignRole("druid", "druidRole"); - usersWithRole = connector.getUsersWithRole("druidRole"); - rolesForUser = connector.getRolesForUser("druid"); + 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); @@ -183,8 +184,8 @@ public void testAddRoleToNonExistentUser() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [nonUser] does not exist."); - connector.createRole("druid"); - connector.assignRole("nonUser", "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druid"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "nonUser", "druid"); } @Test @@ -192,8 +193,8 @@ public void testAddNonexistentRoleToUser() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("Role [nonRole] does not exist."); - connector.createUser("druid"); - connector.assignRole("druid", "nonRole"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.assignRole(TEST_DB_PREFIX, "druid", "nonRole"); } @Test @@ -201,10 +202,10 @@ public void testAddExistingRoleToUserFails() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [druid] already has role [druidRole]."); - connector.createUser("druid"); - connector.createRole("druidRole"); - connector.assignRole("druid", "druidRole"); - connector.assignRole("druid", "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 @@ -213,31 +214,31 @@ public void testUnassignInvalidRoleAssignmentFails() throws Exception expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [druid] does not have role [druidRole]."); - connector.createUser("druid"); - connector.createRole("druidRole"); + authorizerConnector.createUser(TEST_DB_PREFIX, "druid"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); - List> usersWithRole = connector.getUsersWithRole("druidRole"); - List> rolesForUser = connector.getRolesForUser("druid"); + 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); - connector.unassignRole("druid", "druidRole"); + authorizerConnector.unassignRole(TEST_DB_PREFIX, "druid", "druidRole"); } // role and permission tests @Test public void testAddPermissionToRole() throws Exception { - connector.createUser("druid"); - connector.createRole("druidRole"); - connector.assignRole("druid", "druidRole"); + 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 ); - connector.addPermission("druidRole", permission); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); List> expectedPerms = ImmutableList.of( ImmutableMap.of( @@ -245,15 +246,15 @@ public void testAddPermissionToRole() throws Exception "resourceAction", permission ) ); - List> dbPermsRole = connector.getPermissionsForRole("druidRole"); + List> dbPermsRole = authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); Assert.assertEquals(expectedPerms, dbPermsRole); - List> dbPermsUser = connector.getPermissionsForUser("druid"); + List> dbPermsUser = authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); Assert.assertEquals(expectedPerms, dbPermsUser); - connector.deletePermission(1); - dbPermsRole = connector.getPermissionsForRole("druidRole"); + authorizerConnector.deletePermission(TEST_DB_PREFIX, 1); + dbPermsRole = authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); Assert.assertEquals(ImmutableList.of(), dbPermsRole); - dbPermsUser = connector.getPermissionsForUser("druid"); + dbPermsUser = authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); Assert.assertEquals(ImmutableList.of(), dbPermsUser); } @@ -267,7 +268,7 @@ public void testAddPermissionToNonExistentRole() throws Exception new Resource("testResource", ResourceType.DATASOURCE), Action.WRITE ); - connector.addPermission("druidRole", permission); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); } @Test @@ -275,12 +276,12 @@ public void testAddBadPermission() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("Invalid permission, resource name regex[??????????] does not compile."); - connector.createRole("druidRole"); + authorizerConnector.createRole(TEST_DB_PREFIX, "druidRole"); ResourceAction permission = new ResourceAction( new Resource("??????????", ResourceType.DATASOURCE), Action.WRITE ); - connector.addPermission("druidRole", permission); + authorizerConnector.addPermission(TEST_DB_PREFIX, "druidRole", permission); } @Test @@ -288,7 +289,7 @@ public void testGetPermissionForNonExistentRole() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("Role [druidRole] does not exist."); - connector.getPermissionsForRole("druidRole"); + authorizerConnector.getPermissionsForRole(TEST_DB_PREFIX, "druidRole"); } @Test @@ -296,72 +297,27 @@ public void testGetPermissionForNonExistentUser() throws Exception { expectedException.expect(CallbackFailedException.class); expectedException.expectMessage("User [druid] does not exist."); - connector.getPermissionsForUser("druid"); - } - - // user credentials - @Test - public void testAddUserCredentials() throws Exception - { - char[] pass = "blah".toCharArray(); - connector.createUser("druid"); - connector.setUserCredentials("druid", pass); - Assert.assertTrue(connector.checkCredentials("druid", pass)); - Assert.assertFalse(connector.checkCredentials("druid", "wrongPass".toCharArray())); - - Map creds = connector.getUserCredentials("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(); - connector.setUserCredentials("druid", pass); - } - - @Test - public void testGetCredentialsForNonExistentUser() throws Exception - { - expectedException.expect(CallbackFailedException.class); - expectedException.expectMessage("User [druid] does not exist."); - connector.getUserCredentials("druid"); + authorizerConnector.getPermissionsForUser(TEST_DB_PREFIX, "druid"); } private void createAllTables() { - connector.createUserTable(); - connector.createRoleTable(); - connector.createPermissionTable(); - connector.createUserRoleTable(); - connector.createUserCredentialsTable(); + 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 : SQLBasicSecurityStorageConnector.TABLE_NAMES) { - dropTable(table); + for (String table : authorizerConnector.getTableNames(TEST_DB_PREFIX)) { + dropAuthorizerTable(table); } } - private void dropTable(final String tableName) + private void dropAuthorizerTable(final String tableName) { - connector.getDBI().withHandle( + authorizerConnector.getDBI().withHandle( new HandleCallback() { @Override 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/TestDerbySecurityConnector.java b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java similarity index 70% rename from extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbySecurityConnector.java rename to extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java index 6f377d89c4f9..dc7dfa8156bb 100644 --- a/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbySecurityConnector.java +++ b/extensions-core/druid-basic-security/src/test/java/io/druid/security/db/TestDerbyAuthorizerStorageConnector.java @@ -25,8 +25,8 @@ import io.druid.java.util.common.StringUtils; import io.druid.metadata.MetadataStorageConnectorConfig; import io.druid.metadata.NoopMetadataStorageProvider; -import io.druid.security.basic.BasicAuthConfig; -import io.druid.security.basic.db.derby.DerbySQLBasicSecurityStorageConnector; +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; @@ -35,28 +35,28 @@ import java.sql.SQLException; import java.util.UUID; -public class TestDerbySecurityConnector extends DerbySQLBasicSecurityStorageConnector +public class TestDerbyAuthorizerStorageConnector extends DerbySQLBasicAuthorizerStorageConnector { private final String jdbcUri; - public TestDerbySecurityConnector( + public TestDerbyAuthorizerStorageConnector( Supplier config, - Supplier basicAuthConfig + Supplier dbConfigSupplier ) { - this(config, basicAuthConfig, "jdbc:derby:memory:druidTest" + dbSafeUUID()); + this(config, dbConfigSupplier, "jdbc:derby:memory:druidTest" + dbSafeUUID()); } - protected TestDerbySecurityConnector( + protected TestDerbyAuthorizerStorageConnector( Supplier config, - Supplier basicAuthConfig, + Supplier dbConfigSupplier, String jdbcUri ) { super( new NoopMetadataStorageProvider().get(), config, - basicAuthConfig, + null, new ObjectMapper(), new DBI(jdbcUri + ";create=true") ); @@ -87,20 +87,20 @@ public String getJdbcUri() public static class DerbyConnectorRule extends ExternalResource { - private TestDerbySecurityConnector connector; - private final Supplier basicAuthConfigSupplier; + private TestDerbyAuthorizerStorageConnector connector; + private final Supplier dbConfigSupplier; private final MetadataStorageConnectorConfig connectorConfig; - public DerbyConnectorRule() + public DerbyConnectorRule(String dbPrefix) { - this(Suppliers.ofInstance(new BasicAuthConfig())); + this(Suppliers.ofInstance(new BasicAuthDBConfig("test", "druid", "druid"))); } public DerbyConnectorRule( - Supplier basicAuthConfigSupplier + Supplier dbConfigSupplier ) { - this.basicAuthConfigSupplier = basicAuthConfigSupplier; + this.dbConfigSupplier = dbConfigSupplier; this.connectorConfig = new MetadataStorageConnectorConfig() { @Override @@ -114,7 +114,7 @@ public String getConnectURI() @Override protected void before() throws Throwable { - connector = new TestDerbySecurityConnector(Suppliers.ofInstance(connectorConfig), basicAuthConfigSupplier); + connector = new TestDerbyAuthorizerStorageConnector(Suppliers.ofInstance(connectorConfig), dbConfigSupplier); connector.getDBI().open().close(); // create db } @@ -124,19 +124,9 @@ protected void after() connector.tearDown(); } - public TestDerbySecurityConnector getConnector() + public TestDerbyAuthorizerStorageConnector getConnector() { return connector; } - - public MetadataStorageConnectorConfig getMetadataConnectorConfig() - { - return connectorConfig; - } - - public Supplier basicAuthConfigSupplier() - { - return basicAuthConfigSupplier; - } } } diff --git a/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java b/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java index 6a0a6cfd8f76..17e753189fa9 100644 --- a/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java +++ b/server/src/main/java/io/druid/metadata/BaseSQLMetadataConnector.java @@ -26,6 +26,7 @@ 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; @@ -73,6 +74,24 @@ 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 { diff --git a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java index 396834fa0411..cf1b08f2c153 100644 --- a/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java +++ b/server/src/main/java/io/druid/metadata/SQLMetadataConnector.java @@ -21,10 +21,8 @@ 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; @@ -39,7 +37,6 @@ import java.sql.Connection; import java.sql.SQLException; import java.util.List; -import java.util.concurrent.Callable; public abstract class SQLMetadataConnector extends BaseSQLMetadataConnector implements MetadataStorageConnector { @@ -109,24 +106,6 @@ public String getValidationQuery() return "SELECT 1"; } - 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 createPendingSegmentsTable(final String tableName) { createTable( 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; + } }