Basic auth extension (#5099)

* Basic auth extension

* Add auth configuration integration test

* Fix missing authorizerName property

* PR comments

* Fix missing @JsonProperty annotation

* PR comments

* more PR comments
This commit is contained in:
Jonathan Wei 2017-12-14 10:36:04 -08:00 committed by GitHub
parent 3b4395a16e
commit f48c9d7be1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 9668 additions and 8 deletions

View File

@ -0,0 +1,182 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.benchmark;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@Fork(value = 1)
@Warmup(iterations = 10)
@Measurement(iterations = 25)
public class BasicAuthUserMapSerdeBenchmark
{
@Param({"1000"})
private int numUsers;
private ObjectMapper smileMapper;
private Map<String, BenchmarkUser> userMap;
private List<byte[]> serializedUsers;
@Setup
public void setup() throws IOException
{
smileMapper = new ObjectMapper(new SmileFactory());
userMap = new HashMap<>();
for (int i = 0; i < numUsers; i++) {
BenchmarkUser user = makeUser();
userMap.put(user.getName(), user);
}
serializedUsers = new ArrayList<>();
for (BenchmarkUser user : userMap.values()) {
byte[] serializedUser = smileMapper.writeValueAsBytes(user);
serializedUsers.add(serializedUser);
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void serialize(Blackhole blackhole) throws Exception
{
for (BenchmarkUser user : userMap.values()) {
byte[] serializedUser = smileMapper.writeValueAsBytes(user);
blackhole.consume(serializedUser);
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void deserialize(Blackhole blackhole) throws Exception
{
for (byte[] serializedUser : serializedUsers) {
BenchmarkUser user = smileMapper.readValue(serializedUser, BenchmarkUser.class);
blackhole.consume(user);
}
}
private BenchmarkUser makeUser()
{
byte[] salt = new byte[32];
byte[] hash = new byte[64];
Random random = new Random();
random.nextBytes(salt);
random.nextBytes(hash);
return new BenchmarkUser(
UUID.randomUUID().toString(),
new BenchmarkCredentials(
salt,
hash,
10000
)
);
}
private static class BenchmarkUser
{
private final String name;
private final BenchmarkCredentials credentials;
@JsonCreator
public BenchmarkUser(
@JsonProperty("name") String name,
@JsonProperty("credentials") BenchmarkCredentials credentials
)
{
this.name = name;
this.credentials = credentials;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public BenchmarkCredentials getCredentials()
{
return credentials;
}
}
private static class BenchmarkCredentials
{
private final byte[] salt;
private final byte[] hash;
private final int iterations;
@JsonCreator
public BenchmarkCredentials(
@JsonProperty("salt") byte[] salt,
@JsonProperty("hash") byte[] hash,
@JsonProperty("iterations") int iterations
)
{
this.salt = salt;
this.hash = hash;
this.iterations = iterations;
}
@JsonProperty
public byte[] getSalt()
{
return salt;
}
@JsonProperty
public byte[] getHash()
{
return hash;
}
@JsonProperty
public int getIterations()
{
return iterations;
}
}
}

View File

@ -0,0 +1,195 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.benchmark;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.druid.jackson.DefaultObjectMapper;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@State(Scope.Benchmark)
@Fork(value = 1)
@Warmup(iterations = 10)
@Measurement(iterations = 25)
public class RegexMatchBenchmark
{
@Param({"100000"})
private int numPatterns;
private ObjectMapper jsonMapper;
private List<String> 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());
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.metadata;
/**
* Expresses a single compare-and-swap update for MetadataStorageConnector's compareAndSwap method
*/
public class MetadataCASUpdate
{
private final String tableName;
private final String keyColumn;
private final String valueColumn;
private final String key;
private final byte[] oldValue;
private final byte[] newValue;
public MetadataCASUpdate(
String tableName,
String keyColumn,
String valueColumn,
String key,
byte[] oldValue,
byte[] newValue
)
{
this.tableName = tableName;
this.keyColumn = keyColumn;
this.valueColumn = valueColumn;
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
public String getTableName()
{
return tableName;
}
public String getKeyColumn()
{
return keyColumn;
}
public String getValueColumn()
{
return valueColumn;
}
public String getKey()
{
return key;
}
public byte[] getOldValue()
{
return oldValue;
}
public byte[] getNewValue()
{
return newValue;
}
}

View File

@ -19,10 +19,15 @@
package io.druid.metadata;
import java.util.List;
/**
*/
public interface MetadataStorageConnector
{
String CONFIG_TABLE_KEY_COLUMN = "name";
String CONFIG_TABLE_VALUE_COLUMN = "payload";
Void insertOrUpdate(
String tableName,
String keyColumn,
@ -38,6 +43,21 @@ public interface MetadataStorageConnector
String key
);
/**
* Atomic compare-and-swap variant of insertOrUpdate().
*
* @param updates Set of updates to be made. If compare checks succeed for all updates, perform all updates.
* If any compare check fails, reject all updates.
* @return true if updates were made, false otherwise
* @throws Exception
*/
default boolean compareAndSwap(
List<MetadataCASUpdate> updates
) throws Exception
{
throw new UnsupportedOperationException("compareAndSwap is not implemented.");
}
void createDataSourceTable();
void createPendingSegmentsTable();

View File

@ -110,6 +110,8 @@
<argument>io.druid.extensions:druid-examples</argument>
<argument>-c</argument>
<argument>io.druid.extensions:simple-client-sslcontext</argument>
<argument>-c</argument>
<argument>io.druid.extensions:druid-basic-security</argument>
<argument>${druid.distribution.pulldeps.opts}</argument>
</arguments>
</configuration>

View File

@ -0,0 +1,295 @@
---
layout: doc_page
---
# Druid Basic Security
This extension adds:
- an Authenticator which supports [HTTP Basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
- an Authorizer which implements basic role-based access control
Make sure to [include](../../operations/including-extensions.html) `druid-basic-security` as an extension.
Please see [Authentication and Authorization](../../configuration/auth.html) for more information on the extension interfaces being implemented.
## Configuration
The examples in the section will use "MyBasicAuthenticator" and "MyBasicAuthorizer" as names for the Authenticator and Authorizer.
These properties are not tied to specific Authenticator or Authorizer instances.
These configuration properties should be added to the common runtime properties file.
### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.basic.common.pollingPeriod`|Defines in milliseconds how often nodes should poll the coordinator for the current authenticator/authorizer database state.|60000|No|
|`druid.auth.basic.common.maxRandomDelay`|Defines in milliseconds the amount of random delay to add to the pollingPeriod, to spread polling requests across time.|6000|No|
|`druid.auth.basic.common.maxSyncRetries`|Determines how many times a service will retry if the authentication/authorization database state sync with the coordinator fails.|10|No|
|`druid.auth.basic.common.cacheDirectory`|If defined, snapshots of the basic Authenticator and Authorizer database caches will be stored on disk in this directory. If this property is defined, when a service is starting, it will attempt to initialize its caches from these on-disk snapshots, if the service is unable to initialize its state by communicating with the coordinator.|null|No|
### Creating an Authenticator
```
druid.auth.authenticatorChain=["MyBasicAuthenticator"]
druid.auth.authenticator.MyBasicAuthenticator.type=basic
druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword=password1
druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword=password2
druid.auth.authenticator.MyBasicAuthenticator.authorizerName=MyBasicAuthorizer
```
To use the Basic authenticator, add an authenticator with type `basic` to the authenticatorChain.
Configuration of the named authenticator is assigned through properties with the form:
```
druid.auth.authenticator.<authenticatorName>.<authenticatorProperty>
```
The configuration examples in the rest of this document will use "MyBasicAuthenticator" as the name of the authenticator being configured.
#### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created. If the default admin user already exists, setting this property will affect its password.|null|No|
|`druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword`|Initial password for the default internal system user, used for internal node communication. If no password is specified, the default internal system user will not be created. If the default internal system user already exists, setting this property will affect its password.|null|No|
|`druid.auth.authenticator.MyBasicAuthenticator.enableCacheNotifications`|If true, the coordinator will notify Druid nodes whenever a configuration change to this Authenticator occurs, allowing them to immediately update their state without waiting for polling.|true|No|
|`druid.auth.authenticator.MyBasicAuthenticator.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No|
|`druid.auth.authenticator.MyBasicAuthenticator.credentialIterations`|Number of iterations to use for password hashing.|10000|No|
|`druid.auth.authenticator.MyBasicAuthenticator.authorizerName`|Authorizer that requests should be directed to|N/A|Yes|
### Creating an Escalator
```
# Escalator
druid.escalator.type=basic
druid.escalator.internalClientUsername=druid_system
druid.escalator.internalClientPassword=password2
druid.escalator.authorizerName=MyBasicAuthorizer
```
#### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.escalator.internalClientUsername`|The escalator will use this username for requests made as the internal systerm user.|n/a|Yes|
|`druid.escalator.internalClientPassword`|The escalator will use this password for requests made as the internal system user.|n/a|Yes|
|`druid.escalator.authorizerName`|Authorizer that requests should be directed to.|n/a|Yes|
### Creating an Authorizer
```
druid.auth.authorizers=["MyBasicAuthorizer"]
druid.auth.authorizer.MyBasicAuthorizer.type=basic
```
To use the Basic authorizer, add an authenticator with type `basic` to the authorizers list.
Configuration of the named authenticator is assigned through properties with the form:
```
druid.auth.authorizer.<authorizerName>.<authorizerProperty>
```
#### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.authorizer.MyBasicAuthorizer.enableCacheNotifications`|If true, the coordinator will notify Druid nodes whenever a configuration change to this Authorizer occurs, allowing them to immediately update their state without waiting for polling.|true|No|
|`druid.auth.authorizer.MyBasicAuthorizer.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No|
## Usage
### Coordinator Security API
To use these APIs, a user needs read/write permissions for the CONFIG resource type with name "security".
#### Authentication API
Root path: `/druid-ext/basic-security/authentication`
Each API endpoint includes {authenticatorName}, specifying which Authenticator instance is being configured.
##### User/Credential Management
`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users)`
Return a list of all user names.
`GET(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})`
Return the name and credentials information of the user with name {userName}
`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})`
Create a new user with name {userName}
`DELETE(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName})`
Delete the user with name {userName}
`POST(/druid-ext/basic-security/authentication/db/{authenticatorName}/users/{userName}/credentials)`
Assign a password used for HTTP basic authentication for {userName}
Content: JSON password request object
Example request body:
```
{
"password": "helloworld"
}
```
##### Cache Load Status
`GET(/druid-ext/basic-security/authentication/loadStatus)`
Return the current load status of the local caches of the authentication database.
#### Authorization API
Root path: `/druid-ext/basic-security/authorization`
Each API endpoint includes {authorizerName}, specifying which Authorizer instance is being configured.
##### User Creation/Deletion
`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users)`
Return a list of all user names.
`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})`
Return the name and role information of the user with name {userName}
`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})`
Create a new user with name {userName}
`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName})`
Delete the user with name {userName}
#### Role Creation/Deletion
`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles)`
Return a list of all role names.
`GET(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})`
Return name and permissions for the role named {roleName}
`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})`
Create a new role with name {roleName}.
Content: username string
`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName})`
Delete the role with name {roleName}.
#### Role Assignment
`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})`
Assign role {roleName} to user {userName}.
`DELETE(/druid-ext/basic-security/authorization/db/{authorizerName}/users/{userName}/roles/{roleName})`
Unassign role {roleName} from user {userName}
#### Permissions
`POST(/druid-ext/basic-security/authorization/db/{authorizerName}/roles/{roleName}/permissions)`
Set the permissions of {roleName}. This replaces the previous set of permissions on the role.
Content: List of JSON Resource-Action objects, e.g.:
```
[
{
"resource": {
"name": "wiki.*",
"type": "DATASOURCE"
},
"action": "READ"
},
{
"resource": {
"name": "wikiticker",
"type": "DATASOURCE"
},
"action": "WRITE"
}
]
```
The "name" field for resources in the permission definitions are regexes used to match resource names during authorization checks.
Please see [Defining permissions](#defining-permissions) for more details.
##### Cache Load Status
`GET(/druid-ext/basic-security/authorization/loadStatus)`
Return the current load status of the local caches of the authorization database.
## Default user accounts
### Authenticator
If `druid.auth.authenticator.<authenticator-name>.initialAdminPassword` is set, a default admin user named "admin" will be created, with the specified initial password. If this configuration is omitted, the "admin" user will not be created.
If `druid.auth.authenticator.<authenticator-name>.initialInternalClientPassword` is set, a default internal system user named "druid_system" will be created, with the specified initial password. If this configuration is omitted, the "druid_system" user will not be created.
### Authorizer
Each Authorizer will always have a default "admin" and "druid_system" user with full privileges.
## Defining permissions
There are two action types in Druid: READ and WRITE
There are three resource types in Druid: DATASOURCE, CONFIG, and STATE.
### DATASOURCE
Resource names for this type are datasource names. Specifying a datasource permission allows the administrator to grant users access to specific datasources.
### CONFIG
There are two possible resource names for the "CONFIG" resource type, "CONFIG" and "security". Granting a user access to CONFIG resources allows them to access the following endpoints.
"CONFIG" resource name covers the following endpoints:
|Endpoint|Node Type|
|--------|---------|
|`/druid/coordinator/v1/config`|coordinator|
|`/druid/indexer/v1/worker`|overlord|
|`/druid/indexer/v1/worker/history`|overlord|
|`/druid/worker/v1/disable`|middleManager|
|`/druid/worker/v1/enable`|middleManager|
"security" resource name covers the following endpoint:
|Endpoint|Node Type|
|--------|---------|
|`/druid/coordinator/v1/security`|coordinator|
### STATE
There is only one possible resource name for the "STATE" config resource type, "STATE". Granting a user access to STATE resources allows them to access the following endpoints.
"STATE" resource name covers the following endpoints:
|Endpoint|Node Type|
|--------|---------|
|`/druid/coordinator/v1`|coordinator|
|`/druid/coordinator/v1/rules`|coordinator|
|`/druid/coordinator/v1/rules/history`|coordinator|
|`/druid/coordinator/v1/servers`|coordinator|
|`/druid/coordinator/v1/tiers`|coordinator|
|`/druid/broker/v1`|broker|
|`/druid/v2/candidates`|broker|
|`/druid/indexer/v1/leader`|overlord|
|`/druid/indexer/v1/isLeader`|overlord|
|`/druid/indexer/v1/action`|overlord|
|`/druid/indexer/v1/workers`|overlord|
|`/druid/indexer/v1/scaling`|overlord|
|`/druid/worker/v1/enabled`|middleManager|
|`/druid/worker/v1/tasks`|middleManager|
|`/druid/worker/v1/task/{taskid}/shutdown`|middleManager|
|`/druid/worker/v1//task/{taskid}/log`|middleManager|
|`/druid/historical/v1`|historical|
|`/druid-internal/v1/segments/`|historical|
|`/druid-internal/v1/segments/`|peon|
|`/druid-internal/v1/segments/`|realtime|
|`/status`|all nodes|
## Configuration Propagation
To prevent excessive load on the coordinator, the Authenticator and Authorizer user/role database state is cached on each Druid node.
Each node will periodically poll the coordinator for the latest database state, controlled by the `druid.auth.basic.common.pollingPeriod` and `druid.auth.basic.common.maxRandomDelay` properties.
When a configuration update occurs, the coordinator can optionally notify each node with the updated database state. This behavior is controlled by the `enableCacheNotifications` and `cacheNotificationTimeout` properties on Authenticators and Authorizers.
Note that because of the caching, changes made to the user/role database may not be immediately reflected at each Druid node.

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Druid - a distributed column store.
~ Copyright 2012 - 2015 Metamarkets Group Inc.
~
~ Licensed 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.druid.extensions</groupId>
<artifactId>druid-basic-security</artifactId>
<name>druid-basic-security</name>
<description>druid-basic-security</description>
<parent>
<groupId>io.druid</groupId>
<artifactId>druid</artifactId>
<version>0.11.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.druid</groupId>
<artifactId>druid-services</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.druid</groupId>
<artifactId>druid-server</artifactId>
<version>${project.parent.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi</artifactId>
<scope>provided</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.druid</groupId>
<artifactId>druid-server</artifactId>
<version>${project.parent.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,80 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class BasicAuthCommonCacheConfig
{
private static final long DEFAULT_POLLING_PERIOD = 60000;
private static final long DEFAULT_MAX_RANDOM_DELAY = DEFAULT_POLLING_PERIOD / 10;
private static final int DEFAULT_MAX_SYNC_RETRIES = 10;
@JsonProperty
private final long pollingPeriod;
@JsonProperty
private final long maxRandomDelay;
@JsonProperty
private final String cacheDirectory;
@JsonProperty
private final int maxSyncRetries;
@JsonCreator
public BasicAuthCommonCacheConfig(
@JsonProperty("pollingPeriod") Long pollingPeriod,
@JsonProperty("maxRandomDelay") Long maxRandomDelay,
@JsonProperty("cacheDirectory") String cacheDirectory,
@JsonProperty("maxSyncRetries") Integer maxSyncRetries
)
{
this.pollingPeriod = pollingPeriod == null ? DEFAULT_POLLING_PERIOD : pollingPeriod;
this.maxRandomDelay = maxRandomDelay == null ? DEFAULT_MAX_RANDOM_DELAY : maxRandomDelay;
this.cacheDirectory = cacheDirectory;
this.maxSyncRetries = maxSyncRetries == null ? DEFAULT_MAX_SYNC_RETRIES : maxSyncRetries;
}
@JsonProperty
public long getPollingPeriod()
{
return pollingPeriod;
}
@JsonProperty
public long getMaxRandomDelay()
{
return maxRandomDelay;
}
@JsonProperty
public String getCacheDirectory()
{
return cacheDirectory;
}
@JsonProperty
public int getMaxSyncRetries()
{
return maxSyncRetries;
}
}

View File

@ -0,0 +1,71 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
public class BasicAuthDBConfig
{
public static final long DEFAULT_CACHE_NOTIFY_TIMEOUT_MS = 5000;
private final String initialAdminPassword;
private final String initialInternalClientPassword;
private final boolean enableCacheNotifications;
private final long cacheNotificationTimeout;
private final int iterations;
public BasicAuthDBConfig(
final String initialAdminPassword,
final String initialInternalClientPassword,
final Boolean enableCacheNotifications,
final Long cacheNotificationTimeout,
final int iterations
)
{
this.initialAdminPassword = initialAdminPassword;
this.initialInternalClientPassword = initialInternalClientPassword;
this.enableCacheNotifications = enableCacheNotifications;
this.cacheNotificationTimeout = cacheNotificationTimeout;
this.iterations = iterations;
}
public String getInitialAdminPassword()
{
return initialAdminPassword;
}
public String getInitialInternalClientPassword()
{
return initialInternalClientPassword;
}
public boolean isEnableCacheNotifications()
{
return enableCacheNotifications;
}
public long getCacheNotificationTimeout()
{
return cacheNotificationTimeout;
}
public int getIterations()
{
return iterations;
}
}

View File

@ -0,0 +1,235 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.security.basic.authorization.entity.UserAndRoleMap;
import javax.annotation.Nullable;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.Map;
public class BasicAuthUtils
{
private static final Logger log = new Logger(BasicAuthUtils.class);
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
public final static String ADMIN_NAME = "admin";
public final static String INTERNAL_USER_NAME = "druid_system";
// PBKDF2WithHmacSHA512 is chosen since it has built-in support in Java8.
// Argon2 (https://github.com/p-h-c/phc-winner-argon2) is newer but the only presently
// available Java binding is LGPLv3 licensed.
// Key length is 512-bit to match the PBKDF2WithHmacSHA512 algorithm.
// 256-bit salt should be more than sufficient for uniqueness, expected user count is on the order of thousands.
public final static int SALT_LENGTH = 32;
public final static int DEFAULT_KEY_ITERATIONS = 10000;
public final static int KEY_LENGTH = 512;
public final static String ALGORITHM = "PBKDF2WithHmacSHA512";
public static final TypeReference AUTHENTICATOR_USER_MAP_TYPE_REFERENCE =
new TypeReference<Map<String, BasicAuthenticatorUser>>()
{
};
public static final TypeReference AUTHORIZER_USER_MAP_TYPE_REFERENCE =
new TypeReference<Map<String, BasicAuthorizerUser>>()
{
};
public static final TypeReference AUTHORIZER_ROLE_MAP_TYPE_REFERENCE =
new TypeReference<Map<String, BasicAuthorizerRole>>()
{
};
public static final TypeReference AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE =
new TypeReference<UserAndRoleMap>()
{
};
public static String getEncodedCredentials(final String unencodedCreds)
{
return Base64.getEncoder().encodeToString(StringUtils.toUtf8(unencodedCreds));
}
public static byte[] hashPassword(final char[] password, final byte[] salt, final int iterations)
{
try {
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
SecretKey key = keyFactory.generateSecret(
new PBEKeySpec(
password,
salt,
iterations,
KEY_LENGTH
)
);
return key.getEncoded();
}
catch (InvalidKeySpecException ikse) {
log.error("WTF? invalid keyspec");
throw new RuntimeException("WTF? invalid keyspec", ikse);
}
catch (NoSuchAlgorithmException nsae) {
log.error("%s not supported on this system.", ALGORITHM);
throw new RuntimeException(StringUtils.format("%s not supported on this system.", ALGORITHM), nsae);
}
}
public static byte[] generateSalt()
{
byte salt[] = new byte[SALT_LENGTH];
SECURE_RANDOM.nextBytes(salt);
return salt;
}
@Nullable
public static String getBasicUserSecretFromHttpReq(HttpServletRequest httpReq)
{
String authHeader = httpReq.getHeader("Authorization");
if (authHeader == null) {
return null;
}
if (authHeader.length() < 7) {
return null;
}
if (!authHeader.substring(0, 6).equals("Basic ")) {
return null;
}
String encodedUserSecret = authHeader.substring(6);
try {
return StringUtils.fromUtf8(Base64.getDecoder().decode(encodedUserSecret));
}
catch (IllegalArgumentException iae) {
return null;
}
}
public static Map<String, BasicAuthenticatorUser> deserializeAuthenticatorUserMap(
ObjectMapper objectMapper,
byte[] userMapBytes
)
{
Map<String, BasicAuthenticatorUser> userMap;
if (userMapBytes == null) {
userMap = Maps.newHashMap();
} else {
try {
userMap = objectMapper.readValue(userMapBytes, AUTHENTICATOR_USER_MAP_TYPE_REFERENCE);
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
return userMap;
}
public static byte[] serializeAuthenticatorUserMap(
ObjectMapper objectMapper,
Map<String, BasicAuthenticatorUser> userMap
)
{
try {
return objectMapper.writeValueAsBytes(userMap);
}
catch (IOException ioe) {
throw new ISE(ioe, "WTF? Couldn't serialize userMap!");
}
}
public static Map<String, BasicAuthorizerUser> deserializeAuthorizerUserMap(
ObjectMapper objectMapper,
byte[] userMapBytes
)
{
Map<String, BasicAuthorizerUser> userMap;
if (userMapBytes == null) {
userMap = Maps.newHashMap();
} else {
try {
userMap = objectMapper.readValue(userMapBytes, BasicAuthUtils.AUTHORIZER_USER_MAP_TYPE_REFERENCE);
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
return userMap;
}
public static byte[] serializeAuthorizerUserMap(ObjectMapper objectMapper, Map<String, BasicAuthorizerUser> userMap)
{
try {
return objectMapper.writeValueAsBytes(userMap);
}
catch (IOException ioe) {
throw new ISE(ioe, "WTF? Couldn't serialize userMap!");
}
}
public static Map<String, BasicAuthorizerRole> deserializeAuthorizerRoleMap(
ObjectMapper objectMapper,
byte[] roleMapBytes
)
{
Map<String, BasicAuthorizerRole> roleMap;
if (roleMapBytes == null) {
roleMap = Maps.newHashMap();
} else {
try {
roleMap = objectMapper.readValue(roleMapBytes, BasicAuthUtils.AUTHORIZER_ROLE_MAP_TYPE_REFERENCE);
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
return roleMap;
}
public static byte[] serializeAuthorizerRoleMap(ObjectMapper objectMapper, Map<String, BasicAuthorizerRole> roleMap)
{
try {
return objectMapper.writeValueAsBytes(roleMap);
}
catch (IOException ioe) {
throw new ISE(ioe, "WTF? Couldn't serialize roleMap!");
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import io.druid.java.util.common.StringUtils;
/**
* Throw this for invalid resource accesses in the druid-basic-security extension that are likely a result of user error
* (e.g., entry not found, duplicate entries).
*/
public class BasicSecurityDBResourceException extends IllegalArgumentException
{
public BasicSecurityDBResourceException(String formatText, Object... arguments)
{
super(StringUtils.nonStrictFormat(formatText, arguments));
}
public BasicSecurityDBResourceException(Throwable t, String formatText, Object... arguments)
{
super(StringUtils.nonStrictFormat(formatText, arguments), t);
}
}

View File

@ -0,0 +1,183 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.ImmutableList;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Provides;
import com.google.inject.name.Names;
import io.druid.guice.Jerseys;
import io.druid.guice.JsonConfigProvider;
import io.druid.guice.LazySingleton;
import io.druid.guice.LifecycleModule;
import io.druid.initialization.DruidModule;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.BasicHTTPEscalator;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier;
import io.druid.security.basic.authentication.db.cache.CoordinatorBasicAuthenticatorCacheNotifier;
import io.druid.security.basic.authentication.db.cache.CoordinatorPollingBasicAuthenticatorCacheManager;
import io.druid.security.basic.authentication.db.cache.MetadataStoragePollingBasicAuthenticatorCacheManager;
import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResource;
import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResourceHandler;
import io.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler;
import io.druid.security.basic.authentication.endpoint.DefaultBasicAuthenticatorResourceHandler;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier;
import io.druid.security.basic.authorization.db.cache.CoordinatorBasicAuthorizerCacheNotifier;
import io.druid.security.basic.authorization.db.cache.CoordinatorPollingBasicAuthorizerCacheManager;
import io.druid.security.basic.authorization.db.cache.MetadataStoragePollingBasicAuthorizerCacheManager;
import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResource;
import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResourceHandler;
import io.druid.security.basic.authorization.endpoint.CoordinatorBasicAuthorizerResourceHandler;
import io.druid.security.basic.authorization.endpoint.DefaultBasicAuthorizerResourceHandler;
import java.util.List;
public class BasicSecurityDruidModule implements DruidModule
{
@Override
public void configure(Binder binder)
{
JsonConfigProvider.bind(binder, "druid.auth.basic.common", BasicAuthCommonCacheConfig.class);
LifecycleModule.register(binder, BasicAuthenticatorMetadataStorageUpdater.class);
LifecycleModule.register(binder, BasicAuthorizerMetadataStorageUpdater.class);
LifecycleModule.register(binder, BasicAuthenticatorCacheManager.class);
LifecycleModule.register(binder, BasicAuthorizerCacheManager.class);
Jerseys.addResource(binder, BasicAuthenticatorResource.class);
Jerseys.addResource(binder, BasicAuthorizerResource.class);
}
@Provides @LazySingleton
public static BasicAuthenticatorMetadataStorageUpdater createAuthenticatorStorageUpdater(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class);
} else {
return null;
}
}
@Provides @LazySingleton
public static BasicAuthenticatorCacheManager createAuthenticatorCacheManager(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(MetadataStoragePollingBasicAuthenticatorCacheManager.class);
} else {
return injector.getInstance(CoordinatorPollingBasicAuthenticatorCacheManager.class);
}
}
@Provides @LazySingleton
public static BasicAuthenticatorResourceHandler createAuthenticatorResourceHandler(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthenticatorResourceHandler.class);
} else {
return injector.getInstance(DefaultBasicAuthenticatorResourceHandler.class);
}
}
@Provides @LazySingleton
public static BasicAuthenticatorCacheNotifier createAuthenticatorCacheNotifier(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthenticatorCacheNotifier.class);
} else {
return null;
}
}
@Provides @LazySingleton
public static BasicAuthorizerMetadataStorageUpdater createAuthorizerStorageUpdater(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthorizerMetadataStorageUpdater.class);
} else {
return null;
}
}
@Provides @LazySingleton
public static BasicAuthorizerCacheManager createAuthorizerCacheManager(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(MetadataStoragePollingBasicAuthorizerCacheManager.class);
} else {
return injector.getInstance(CoordinatorPollingBasicAuthorizerCacheManager.class);
}
}
@Provides @LazySingleton
public static BasicAuthorizerResourceHandler createAuthorizerResourceHandler(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthorizerResourceHandler.class);
} else {
return injector.getInstance(DefaultBasicAuthorizerResourceHandler.class);
}
}
@Provides @LazySingleton
public static BasicAuthorizerCacheNotifier createAuthorizerCacheNotifier(final Injector injector)
{
if (isCoordinator(injector)) {
return injector.getInstance(CoordinatorBasicAuthorizerCacheNotifier.class);
} else {
return null;
}
}
@Override
public List<? extends Module> getJacksonModules()
{
return ImmutableList.of(
new SimpleModule("BasicDruidSecurity").registerSubtypes(
BasicHTTPAuthenticator.class,
BasicHTTPEscalator.class,
BasicRoleBasedAuthorizer.class
)
);
}
private static boolean isCoordinator(Injector injector)
{
final String serviceName;
try {
serviceName = injector.getInstance(Key.get(String.class, Names.named("serviceName")));
}
catch (Exception e) {
return false;
}
return "druid/coordinator".equals(serviceName);
}
}

View File

@ -0,0 +1,90 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ContainerRequest;
import io.druid.java.util.common.StringUtils;
import io.druid.server.http.security.AbstractResourceFilter;
import io.druid.server.security.Access;
import io.druid.server.security.AuthorizationUtils;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.util.List;
public class BasicSecurityResourceFilter extends AbstractResourceFilter
{
private static final List<String> APPLICABLE_PATHS = ImmutableList.of(
"/druid-ext/basic-security/authentication",
"/druid-ext/basic-security/authorization"
);
private static final String SECURITY_RESOURCE_NAME = "security";
@Inject
public BasicSecurityResourceFilter(
AuthorizerMapper authorizerMapper
)
{
super(authorizerMapper);
}
@Override
public ContainerRequest filter(ContainerRequest request)
{
final ResourceAction resourceAction = new ResourceAction(
new Resource(SECURITY_RESOURCE_NAME, ResourceType.CONFIG),
getAction(request)
);
final Access authResult = AuthorizationUtils.authorizeResourceAction(
getReq(),
resourceAction,
getAuthorizerMapper()
);
if (!authResult.isAllowed()) {
throw new WebApplicationException(
Response.status(Response.Status.FORBIDDEN)
.entity(StringUtils.format("Access-Check-Result: %s", authResult.toString()))
.build()
);
}
return request;
}
@Override
public boolean isApplicable(String requestPath)
{
for (String path : APPLICABLE_PATHS) {
if (requestPath.startsWith(path) && !requestPath.equals(path)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,236 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.metamx.emitter.EmittingLogger;
import com.metamx.http.client.HttpClient;
import com.metamx.http.client.Request;
import com.metamx.http.client.response.ClientResponse;
import com.metamx.http.client.response.HttpResponseHandler;
import com.metamx.http.client.response.StatusResponseHolder;
import io.druid.discovery.DiscoveryDruidNode;
import io.druid.discovery.DruidNodeDiscovery;
import io.druid.discovery.DruidNodeDiscoveryProvider;
import io.druid.java.util.common.Pair;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.concurrent.Execs;
import io.druid.java.util.common.logger.Logger;
import io.druid.server.DruidNode;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.joda.time.Duration;
import javax.ws.rs.core.MediaType;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class CommonCacheNotifier
{
private static final EmittingLogger LOG = new EmittingLogger(CommonCacheNotifier.class);
private static final List<String> NODE_TYPES = Arrays.asList(
DruidNodeDiscoveryProvider.NODE_TYPE_BROKER,
DruidNodeDiscoveryProvider.NODE_TYPE_OVERLORD,
DruidNodeDiscoveryProvider.NODE_TYPE_HISTORICAL,
DruidNodeDiscoveryProvider.NODE_TYPE_PEON,
DruidNodeDiscoveryProvider.NODE_TYPE_ROUTER,
DruidNodeDiscoveryProvider.NODE_TYPE_MM
);
private final DruidNodeDiscoveryProvider discoveryProvider;
private final HttpClient httpClient;
private final BlockingQueue<Pair<String, byte[]>> updateQueue;
private final Map<String, BasicAuthDBConfig> itemConfigMap;
private final String baseUrl;
private final String callerName;
private final ExecutorService exec;
public CommonCacheNotifier(
Map<String, BasicAuthDBConfig> itemConfigMap,
DruidNodeDiscoveryProvider discoveryProvider,
HttpClient httpClient,
String baseUrl,
String callerName
)
{
this.exec = Execs.scheduledSingleThreaded(StringUtils.format("%s-notifierThread-", callerName) + "%d");
this.callerName = callerName;
this.updateQueue = new LinkedBlockingQueue<>();
this.itemConfigMap = itemConfigMap;
this.discoveryProvider = discoveryProvider;
this.httpClient = httpClient;
this.baseUrl = baseUrl;
}
public void start()
{
exec.submit(
() -> {
while (!Thread.interrupted()) {
try {
LOG.debug(callerName + ":Waiting for cache update notification");
Pair<String, byte[]> update = updateQueue.take();
String authorizer = update.lhs;
byte[] serializedMap = update.rhs;
BasicAuthDBConfig authorizerConfig = itemConfigMap.get(update.lhs);
if (!authorizerConfig.isEnableCacheNotifications()) {
continue;
}
LOG.debug(callerName + ":Sending cache update notifications");
// Best effort, if a notification fails, the remote node will eventually poll to update its state
// We wait for responses however, to avoid flooding remote nodes with notifications.
List<ListenableFuture<StatusResponseHolder>> futures = sendUpdate(
authorizer,
serializedMap
);
try {
List<StatusResponseHolder> responses = Futures.allAsList(futures)
.get(
authorizerConfig.getCacheNotificationTimeout(),
TimeUnit.MILLISECONDS
);
for (StatusResponseHolder response : responses) {
LOG.debug(callerName + ":Got status: " + response.getStatus());
}
}
catch (Exception e) {
LOG.makeAlert(e, callerName + ":Failed to get response for cache notification.").emit();
}
LOG.debug(callerName + ":Received responses for cache update notifications.");
}
catch (Throwable t) {
LOG.makeAlert(t, callerName + ":Error occured while handling updates for cachedUserMaps.").emit();
}
}
}
);
}
public void stop()
{
exec.shutdownNow();
}
public void addUpdate(String updatedItemName, byte[] updatedItemData)
{
updateQueue.add(
new Pair<>(updatedItemName, updatedItemData)
);
}
private List<ListenableFuture<StatusResponseHolder>> sendUpdate(String updatedAuthorizerPrefix, byte[] serializedUserMap)
{
List<ListenableFuture<StatusResponseHolder>> futures = new ArrayList<>();
for (String nodeType : NODE_TYPES) {
DruidNodeDiscovery nodeDiscovery = discoveryProvider.getForNodeType(nodeType);
Collection<DiscoveryDruidNode> nodes = nodeDiscovery.getAllNodes();
for (DiscoveryDruidNode node : nodes) {
URL listenerURL = getListenerURL(node.getDruidNode(), baseUrl, updatedAuthorizerPrefix);
// best effort, if this fails, remote node will poll and pick up the update eventually
Request req = new Request(HttpMethod.POST, listenerURL);
req.setContent(MediaType.APPLICATION_JSON, serializedUserMap);
BasicAuthDBConfig itemConfig = itemConfigMap.get(updatedAuthorizerPrefix);
ListenableFuture<StatusResponseHolder> future = httpClient.go(
req,
new ResponseHandler(),
Duration.millis(itemConfig.getCacheNotificationTimeout())
);
futures.add(future);
}
}
return futures;
}
private URL getListenerURL(DruidNode druidNode, String baseUrl, String itemName)
{
try {
return new URL(
druidNode.getServiceScheme(),
druidNode.getHost(),
druidNode.getPortToUse(),
StringUtils.format(baseUrl, itemName)
);
}
catch (MalformedURLException mue) {
LOG.error(callerName + ":WTF? Malformed url for DruidNode[%s] and itemName[%s]", druidNode, itemName);
throw new RuntimeException(mue);
}
}
// Based off StatusResponseHandler, but with response content ignored
private static class ResponseHandler implements HttpResponseHandler<StatusResponseHolder, StatusResponseHolder>
{
protected static final Logger log = new Logger(ResponseHandler.class);
@Override
public ClientResponse<StatusResponseHolder> handleResponse(HttpResponse response)
{
return ClientResponse.unfinished(
new StatusResponseHolder(
response.getStatus(),
null
)
);
}
@Override
public ClientResponse<StatusResponseHolder> handleChunk(
ClientResponse<StatusResponseHolder> response,
HttpChunk chunk
)
{
return response;
}
@Override
public ClientResponse<StatusResponseHolder> done(ClientResponse<StatusResponseHolder> response)
{
return ClientResponse.finished(response.getObj());
}
@Override
public void exceptionCaught(
ClientResponse<StatusResponseHolder> clientResponse, Throwable e
)
{
// Its safe to Ignore as the ClientResponse returned in handleChunk were unfinished
log.error(e, "exceptionCaught in CommonCacheNotifier ResponseHandler.");
}
}
}

View File

@ -0,0 +1,214 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.inject.Provider;
import io.druid.java.util.common.IAE;
import io.druid.security.basic.BasicAuthDBConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.server.security.AuthConfig;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Authenticator;
import javax.annotation.Nullable;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Map;
@JsonTypeName("basic")
public class BasicHTTPAuthenticator implements Authenticator
{
private final Provider<BasicAuthenticatorCacheManager> cacheManager;
private final String name;
private final String authorizerName;
private final BasicAuthDBConfig dbConfig;
@JsonCreator
public BasicHTTPAuthenticator(
@JacksonInject Provider<BasicAuthenticatorCacheManager> cacheManager,
@JsonProperty("name") String name,
@JsonProperty("authorizerName") String authorizerName,
@JsonProperty("initialAdminPassword") String initialAdminPassword,
@JsonProperty("initialInternalClientPassword") String initialInternalClientPassword,
@JsonProperty("enableCacheNotifications") Boolean enableCacheNotifications,
@JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout,
@JsonProperty("credentialIterations") Integer credentialIterations
)
{
this.name = name;
this.authorizerName = authorizerName;
this.dbConfig = new BasicAuthDBConfig(
initialAdminPassword,
initialInternalClientPassword,
enableCacheNotifications == null ? true : enableCacheNotifications,
cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout,
credentialIterations == null ? BasicAuthUtils.DEFAULT_KEY_ITERATIONS : credentialIterations
);
this.cacheManager = cacheManager;
}
@Override
public Filter getFilter()
{
return new BasicHTTPAuthenticationFilter();
}
@Override
public String getAuthChallengeHeader()
{
return "Basic";
}
@Override
@Nullable
public AuthenticationResult authenticateJDBCContext(Map<String, Object> context)
{
String user = (String) context.get("user");
String password = (String) context.get("password");
if (user == null || password == null) {
return null;
}
if (checkCredentials(user, password.toCharArray())) {
return new AuthenticationResult(user, name, null);
} else {
return null;
}
}
@Override
public Class<? extends Filter> getFilterClass()
{
return BasicHTTPAuthenticationFilter.class;
}
@Override
public Map<String, String> getInitParameters()
{
return null;
}
@Override
public String getPath()
{
return "/*";
}
@Override
public EnumSet<DispatcherType> getDispatcherType()
{
return null;
}
public BasicAuthDBConfig getDbConfig()
{
return dbConfig;
}
public class BasicHTTPAuthenticationFilter implements Filter
{
@Override
public void init(FilterConfig filterConfig) throws ServletException
{
}
@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain
) throws IOException, ServletException
{
HttpServletResponse httpResp = (HttpServletResponse) servletResponse;
String userSecret = BasicAuthUtils.getBasicUserSecretFromHttpReq((HttpServletRequest) servletRequest);
if (userSecret == null) {
// Request didn't have HTTP Basic auth credentials, move on to the next filter
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String[] splits = userSecret.split(":");
if (splits.length != 2) {
httpResp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String user = splits[0];
char[] password = splits[1].toCharArray();
if (checkCredentials(user, password)) {
AuthenticationResult authenticationResult = new AuthenticationResult(user, authorizerName, null);
servletRequest.setAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT, authenticationResult);
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy()
{
}
}
private boolean checkCredentials(String username, char[] password)
{
Map<String, BasicAuthenticatorUser> userMap = cacheManager.get().getUserMap(name);
if (userMap == null) {
throw new IAE("No authenticator found with prefix: [%s]", name);
}
BasicAuthenticatorUser user = userMap.get(username);
if (user == null) {
return false;
}
BasicAuthenticatorCredentials credentials = user.getCredentials();
if (credentials == null) {
return false;
}
byte[] recalculatedHash = BasicAuthUtils.hashPassword(
password,
credentials.getSalt(),
credentials.getIterations()
);
return Arrays.equals(recalculatedHash, credentials.getHash());
}
}

View File

@ -0,0 +1,116 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.google.common.base.Throwables;
import com.metamx.http.client.CredentialedHttpClient;
import com.metamx.http.client.HttpClient;
import com.metamx.http.client.auth.BasicCredentials;
import io.druid.java.util.common.StringUtils;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Escalator;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.util.Attributes;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import java.net.URI;
@JsonTypeName("basic")
public class BasicHTTPEscalator implements Escalator
{
private final String internalClientUsername;
private final String internalClientPassword;
private final String authorizerName;
@JsonCreator
public BasicHTTPEscalator(
@JsonProperty("authorizerName") String authorizerName,
@JsonProperty("internalClientUsername") String internalClientUsername,
@JsonProperty("internalClientPassword") String internalClientPassword
)
{
this.authorizerName = authorizerName;
this.internalClientUsername = internalClientUsername;
this.internalClientPassword = internalClientPassword;
}
@Override
public HttpClient createEscalatedClient(HttpClient baseClient)
{
return new CredentialedHttpClient(
new BasicCredentials(internalClientUsername, internalClientPassword),
baseClient
);
}
@Override
public org.eclipse.jetty.client.HttpClient createEscalatedJettyClient(org.eclipse.jetty.client.HttpClient baseClient)
{
baseClient.getAuthenticationStore().addAuthentication(new Authentication()
{
@Override
public boolean matches(String type, URI uri, String realm)
{
return true;
}
@Override
public Result authenticate(
final Request request, ContentResponse response, Authentication.HeaderInfo headerInfo, Attributes context
)
{
return new Result()
{
@Override
public URI getURI()
{
return request.getURI();
}
@Override
public void apply(Request request)
{
try {
final String unencodedCreds = StringUtils.format("%s:%s", internalClientUsername, internalClientPassword);
final String base64Creds = BasicAuthUtils.getEncodedCredentials(unencodedCreds);
request.getHeaders().add(HttpHeaders.Names.AUTHORIZATION, "Basic " + base64Creds);
}
catch (Throwable e) {
Throwables.propagate(e);
}
}
};
}
});
return baseClient;
}
@Override
public AuthenticationResult createEscalatedAuthenticationResult()
{
return new AuthenticationResult(internalClientUsername, authorizerName, null);
}
}

View File

@ -0,0 +1,75 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication;
import com.metamx.http.client.response.ClientResponse;
import com.metamx.http.client.response.FullResponseHolder;
import com.metamx.http.client.response.HttpResponseHandler;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpResponse;
public class BytesFullResponseHandler implements HttpResponseHandler<FullResponseHolder, FullResponseHolder>
{
@Override
public ClientResponse<FullResponseHolder> handleResponse(HttpResponse response)
{
BytesFullResponseHolder holder = new BytesFullResponseHolder(
response.getStatus(),
response,
null
);
holder.addChunk(response.getContent().array());
return ClientResponse.unfinished(
holder
);
}
@Override
public ClientResponse<FullResponseHolder> handleChunk(
ClientResponse<FullResponseHolder> response,
HttpChunk chunk
)
{
BytesFullResponseHolder holder = (BytesFullResponseHolder) response.getObj();
if (holder == null) {
return ClientResponse.finished(null);
}
holder.addChunk(chunk.getContent().array());
return response;
}
@Override
public ClientResponse<FullResponseHolder> done(ClientResponse<FullResponseHolder> response)
{
return ClientResponse.finished(response.getObj());
}
@Override
public void exceptionCaught(
ClientResponse<FullResponseHolder> clientResponse, Throwable e
)
{
// Its safe to Ignore as the ClientResponse returned in handleChunk were unfinished
}
}

View File

@ -0,0 +1,63 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication;
import com.metamx.http.client.response.FullResponseHolder;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class BytesFullResponseHolder extends FullResponseHolder
{
private final List<byte[]> chunks;
public BytesFullResponseHolder(
HttpResponseStatus status,
HttpResponse response,
StringBuilder builder
)
{
super(status, response, builder);
this.chunks = new ArrayList<>();
}
public void addChunk(byte[] chunk)
{
chunks.add(chunk);
}
public byte[] getBytes()
{
int size = 0;
for (byte[] chunk : chunks) {
size += chunk.length;
}
ByteBuffer buf = ByteBuffer.wrap(new byte[size]);
for (byte[] chunk : chunks) {
buf.put(chunk);
}
return buf.array();
}
}

View File

@ -0,0 +1,47 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.cache;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import java.util.Map;
/**
* This class is reponsible for maintaining a cache of the authenticator database state. The BasicHTTPAuthenticator
* uses an injected BasicAuthenticatorCacheManager to make its authentication decisions.
*/
public interface BasicAuthenticatorCacheManager
{
/**
* Update this cache manager's local state with fresh information pushed by the coordinator.
*
* @param authenticatorPrefix The name of the authenticator this update applies to.
* @param serializedUserMap The updated, serialized user map
*/
void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap);
/**
* Return the cache manager's local view of the user map for the authenticator named `authenticatorPrefix`.
*
* @param authenticatorPrefix The name of the authenticator
* @return User map
*/
Map<String, BasicAuthenticatorUser> getUserMap(String authenticatorPrefix);
}

View File

@ -0,0 +1,34 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.cache;
/**
* Sends a notification to druid services, containing updated authenticator user map state.
*/
public interface BasicAuthenticatorCacheNotifier
{
/**
* Send the user map state contained in updatedUserMap to all non-coordinator Druid services
*
* @param updatedAuthenticatorPrefix Name of authenticator being updated
* @param updatedUserMap User map state
*/
void addUpdate(String updatedAuthenticatorPrefix, byte[] updatedUserMap);
}

View File

@ -0,0 +1,123 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.cache;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.metamx.emitter.EmittingLogger;
import com.metamx.http.client.HttpClient;
import io.druid.concurrent.LifecycleLock;
import io.druid.discovery.DruidNodeDiscoveryProvider;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.EscalatedClient;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.security.basic.BasicAuthDBConfig;
import io.druid.security.basic.CommonCacheNotifier;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ManageLifecycle
public class CoordinatorBasicAuthenticatorCacheNotifier implements BasicAuthenticatorCacheNotifier
{
private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthenticatorCacheNotifier.class);
private final LifecycleLock lifecycleLock = new LifecycleLock();
private CommonCacheNotifier cacheNotifier;
@Inject
public CoordinatorBasicAuthenticatorCacheNotifier(
AuthenticatorMapper authenticatorMapper,
DruidNodeDiscoveryProvider discoveryProvider,
@EscalatedClient HttpClient httpClient
)
{
cacheNotifier = new CommonCacheNotifier(
initAuthenticatorConfigMap(authenticatorMapper),
discoveryProvider,
httpClient,
"/druid-ext/basic-security/authentication/listen/%s",
"CoordinatorBasicAuthenticatorCacheNotifier"
);
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
try {
cacheNotifier.start();
lifecycleLock.started();
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
return;
}
try {
cacheNotifier.stop();
}
finally {
lifecycleLock.exitStop();
}
}
@Override
public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap);
}
private Map<String, BasicAuthDBConfig> initAuthenticatorConfigMap(AuthenticatorMapper mapper)
{
Preconditions.checkNotNull(mapper);
Preconditions.checkNotNull(mapper.getAuthenticatorMap());
Map<String, BasicAuthDBConfig> authenticatorConfigMap = new HashMap<>();
for (Map.Entry<String, Authenticator> entry : mapper.getAuthenticatorMap().entrySet()) {
Authenticator authenticator = entry.getValue();
if (authenticator instanceof BasicHTTPAuthenticator) {
String authenticatorName = entry.getKey();
BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator;
BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig();
authenticatorConfigMap.put(authenticatorName, dbConfig);
}
}
return authenticatorConfigMap;
}
}

View File

@ -0,0 +1,282 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.cache;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.metamx.emitter.EmittingLogger;
import com.metamx.http.client.Request;
import io.druid.client.coordinator.Coordinator;
import io.druid.concurrent.LifecycleLock;
import io.druid.discovery.DruidLeaderClient;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.RetryUtils;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.concurrent.Execs;
import io.druid.java.util.common.concurrent.ScheduledExecutors;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.BytesFullResponseHandler;
import io.druid.security.basic.authentication.BytesFullResponseHolder;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.joda.time.Duration;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* Cache manager for non-coordinator services that polls the coordinator for authentication database state.
*/
@ManageLifecycle
public class CoordinatorPollingBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager
{
private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthenticatorCacheManager.class);
private final ConcurrentHashMap<String, Map<String, BasicAuthenticatorUser>> cachedUserMaps;
private final Set<String> authenticatorPrefixes;
private final Injector injector;
private final ObjectMapper objectMapper;
private final LifecycleLock lifecycleLock = new LifecycleLock();
private final DruidLeaderClient druidLeaderClient;
private final BasicAuthCommonCacheConfig commonCacheConfig;
private final ScheduledExecutorService exec;
@Inject
public CoordinatorPollingBasicAuthenticatorCacheManager(
Injector injector,
BasicAuthCommonCacheConfig commonCacheConfig,
@Smile ObjectMapper objectMapper,
@Coordinator DruidLeaderClient druidLeaderClient
)
{
this.exec = Execs.scheduledSingleThreaded("BasicAuthenticatorCacheManager-Exec--%d");
this.injector = injector;
this.commonCacheConfig = commonCacheConfig;
this.objectMapper = objectMapper;
this.cachedUserMaps = new ConcurrentHashMap<>();
this.authenticatorPrefixes = new HashSet<>();
this.druidLeaderClient = druidLeaderClient;
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
LOG.info("Starting DefaultBasicAuthenticatorCacheManager.");
try {
initUserMaps();
ScheduledExecutors.scheduleWithFixedDelay(
exec,
new Duration(commonCacheConfig.getPollingPeriod()),
new Duration(commonCacheConfig.getPollingPeriod()),
() -> {
try {
long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay());
LOG.debug("Inserting random polling delay of [%s] ms", randomDelay);
Thread.sleep(randomDelay);
LOG.debug("Scheduled cache poll is running");
for (String authenticatorPrefix : authenticatorPrefixes) {
Map<String, BasicAuthenticatorUser> userMap = fetchUserMapFromCoordinator(authenticatorPrefix, false);
if (userMap != null) {
cachedUserMaps.put(authenticatorPrefix, userMap);
}
}
LOG.debug("Scheduled cache poll is done");
}
catch (Throwable t) {
LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit();
}
}
);
lifecycleLock.started();
LOG.info("Started DefaultBasicAuthenticatorCacheManager.");
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
throw new ISE("can't stop.");
}
LOG.info("DefaultBasicAuthenticatorCacheManager is stopping.");
exec.shutdown();
LOG.info("DefaultBasicAuthenticatorCacheManager is stopped.");
}
@Override
public void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap)
{
LOG.debug("Received cache update for authenticator [%s].", authenticatorPrefix);
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
try {
cachedUserMaps.put(
authenticatorPrefix,
objectMapper.readValue(
serializedUserMap,
BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE
)
);
if (commonCacheConfig.getCacheDirectory() != null) {
writeUserMapToDisk(authenticatorPrefix, serializedUserMap);
}
}
catch (Exception e) {
LOG.makeAlert(e, "WTF? Could not deserialize user map received from coordinator.").emit();
}
}
@Override
public Map<String, BasicAuthenticatorUser> getUserMap(String authenticatorPrefix)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
return cachedUserMaps.get(authenticatorPrefix);
}
@Nullable
private Map<String, BasicAuthenticatorUser> fetchUserMapFromCoordinator(String prefix, boolean isInit)
{
try {
return RetryUtils.retry(
() -> {
return tryFetchUserMapFromCoordinator(prefix);
},
e -> true,
commonCacheConfig.getMaxSyncRetries()
);
}
catch (Exception e) {
LOG.makeAlert(e, "Encountered exception while fetching user map for authenticator [%s]", prefix);
if (isInit) {
if (commonCacheConfig.getCacheDirectory() != null) {
try {
LOG.info("Attempting to load user map snapshot from disk.");
return loadUserMapFromDisk(prefix);
}
catch (Exception e2) {
e2.addSuppressed(e);
LOG.makeAlert(e2, "Encountered exception while loading user map snapshot for authenticator [%s]", prefix);
}
}
}
return null;
}
}
private String getUserMapFilename(String prefix)
{
return StringUtils.format("%s.authenticator.cache", prefix);
}
@Nullable
private Map<String, BasicAuthenticatorUser> loadUserMapFromDisk(String prefix) throws IOException
{
File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserMapFilename(prefix));
if (!userMapFile.exists()) {
return null;
}
return objectMapper.readValue(
userMapFile,
BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE
);
}
private void writeUserMapToDisk(String prefix, byte[] userMapBytes) throws IOException
{
File cacheDir = new File(commonCacheConfig.getCacheDirectory());
cacheDir.mkdirs();
File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserMapFilename(prefix));
Files.write(userMapBytes, userMapFile);
}
private Map<String, BasicAuthenticatorUser> tryFetchUserMapFromCoordinator(String prefix) throws Exception
{
Request req = druidLeaderClient.makeRequest(
HttpMethod.GET,
StringUtils.format("/druid-ext/basic-security/authentication/db/%s/cachedSerializedUserMap", prefix)
);
BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go(
req,
new BytesFullResponseHandler()
);
byte[] userMapBytes = responseHolder.getBytes();
Map<String, BasicAuthenticatorUser> userMap = objectMapper.readValue(
userMapBytes,
BasicAuthUtils.AUTHENTICATOR_USER_MAP_TYPE_REFERENCE
);
if (userMap != null && commonCacheConfig.getCacheDirectory() != null) {
writeUserMapToDisk(prefix, userMapBytes);
}
return userMap;
}
private void initUserMaps()
{
AuthenticatorMapper authenticatorMapper = injector.getInstance(AuthenticatorMapper.class);
if (authenticatorMapper == null || authenticatorMapper.getAuthenticatorMap() == null) {
return;
}
for (Map.Entry<String, Authenticator> entry : authenticatorMapper.getAuthenticatorMap().entrySet()) {
Authenticator authenticator = entry.getValue();
if (authenticator instanceof BasicHTTPAuthenticator) {
String authenticatorName = entry.getKey();
authenticatorPrefixes.add(authenticatorName);
Map<String, BasicAuthenticatorUser> userMap = fetchUserMapFromCoordinator(authenticatorName, true);
if (userMap != null) {
cachedUserMaps.put(authenticatorName, userMap);
}
}
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.cache;
import com.google.inject.Inject;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import java.util.Map;
/**
* Used on coordinator nodes, reading from a BasicAuthenticatorMetadataStorageUpdater that has direct access to the
* metadata store.
*/
public class MetadataStoragePollingBasicAuthenticatorCacheManager implements BasicAuthenticatorCacheManager
{
private static final Logger log = new Logger(MetadataStoragePollingBasicAuthenticatorCacheManager.class);
private final BasicAuthenticatorMetadataStorageUpdater storageUpdater;
@Inject
public MetadataStoragePollingBasicAuthenticatorCacheManager(
BasicAuthenticatorMetadataStorageUpdater storageUpdater
)
{
this.storageUpdater = storageUpdater;
log.info("Starting coordinator basic authenticator cache manager.");
}
@Override
public void handleAuthenticatorUpdate(String authenticatorPrefix, byte[] serializedUserMap)
{
}
@Override
public Map<String, BasicAuthenticatorUser> getUserMap(String authenticatorPrefix)
{
return storageUpdater.getCachedUserMap(authenticatorPrefix);
}
}

View File

@ -0,0 +1,48 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.updater;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import java.util.Map;
/**
* Implementations of this interface are responsible for connecting directly to the metadata storage,
* modifying the authenticator database state or reading it. This interface is used by the
* MetadataStoragePollingBasicAuthenticatorCacheManager (for reads) and the CoordinatorBasicAuthenticatorResourceHandler
* (for handling configuration read/writes).
*/
public interface BasicAuthenticatorMetadataStorageUpdater
{
void createUser(String prefix, String userName);
void deleteUser(String prefix, String userName);
void setUserCredentials(String prefix, String userName, BasicAuthenticatorCredentialUpdate update);
Map<String, BasicAuthenticatorUser> getCachedUserMap(String prefix);
byte[] getCachedSerializedUserMap(String prefix);
byte[] getCurrentUserMapBytes(String prefix);
void refreshAllNotification();
}

View File

@ -0,0 +1,444 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.db.updater;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.metamx.emitter.EmittingLogger;
import io.druid.common.config.ConfigManager;
import io.druid.concurrent.LifecycleLock;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.concurrent.Execs;
import io.druid.java.util.common.concurrent.ScheduledExecutors;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.metadata.MetadataCASUpdate;
import io.druid.metadata.MetadataStorageConnector;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthDBConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUserMapBundle;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import org.joda.time.Duration;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@ManageLifecycle
public class CoordinatorBasicAuthenticatorMetadataStorageUpdater implements BasicAuthenticatorMetadataStorageUpdater
{
private static final EmittingLogger LOG =
new EmittingLogger(CoordinatorBasicAuthenticatorMetadataStorageUpdater.class);
private static final String USERS = "users";
private static final long UPDATE_RETRY_DELAY = 1000;
private final AuthenticatorMapper authenticatorMapper;
private final MetadataStorageConnector connector;
private final MetadataStorageTablesConfig connectorConfig;
private final BasicAuthCommonCacheConfig commonCacheConfig;
private final ObjectMapper objectMapper;
private final BasicAuthenticatorCacheNotifier cacheNotifier;
private final int numRetries = 5;
private final Map<String, BasicAuthenticatorUserMapBundle> cachedUserMaps;
private final Set<String> authenticatorPrefixes;
private final LifecycleLock lifecycleLock = new LifecycleLock();
private final ScheduledExecutorService exec;
private volatile boolean stopped = false;
@Inject
public CoordinatorBasicAuthenticatorMetadataStorageUpdater(
AuthenticatorMapper authenticatorMapper,
MetadataStorageConnector connector,
MetadataStorageTablesConfig connectorConfig,
BasicAuthCommonCacheConfig commonCacheConfig,
@Smile ObjectMapper objectMapper,
BasicAuthenticatorCacheNotifier cacheNotifier,
ConfigManager configManager // ConfigManager creates the db table we need, set a dependency here
)
{
this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthenticatorMetadataStorageUpdater-Exec--%d");
this.authenticatorMapper = authenticatorMapper;
this.connector = connector;
this.connectorConfig = connectorConfig;
this.commonCacheConfig = commonCacheConfig;
this.objectMapper = objectMapper;
this.cacheNotifier = cacheNotifier;
this.cachedUserMaps = new ConcurrentHashMap<>();
this.authenticatorPrefixes = new HashSet<>();
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
if (authenticatorMapper == null || authenticatorMapper.getAuthenticatorMap() == null) {
return;
}
try {
LOG.info("Starting CoordinatorBasicAuthenticatorMetadataStorageUpdater.");
for (Map.Entry<String, Authenticator> entry : authenticatorMapper.getAuthenticatorMap().entrySet()) {
Authenticator authenticator = entry.getValue();
if (authenticator instanceof BasicHTTPAuthenticator) {
String authenticatorName = entry.getKey();
authenticatorPrefixes.add(authenticatorName);
BasicHTTPAuthenticator basicHTTPAuthenticator = (BasicHTTPAuthenticator) authenticator;
BasicAuthDBConfig dbConfig = basicHTTPAuthenticator.getDbConfig();
byte[] userMapBytes = getCurrentUserMapBytes(authenticatorName);
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
userMapBytes
);
cachedUserMaps.put(authenticatorName, new BasicAuthenticatorUserMapBundle(userMap, userMapBytes));
if (dbConfig.getInitialAdminPassword() != null && !userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) {
createUserInternal(authenticatorName, BasicAuthUtils.ADMIN_NAME);
setUserCredentialsInternal(
authenticatorName,
BasicAuthUtils.ADMIN_NAME,
new BasicAuthenticatorCredentialUpdate(
dbConfig.getInitialAdminPassword(),
BasicAuthUtils.DEFAULT_KEY_ITERATIONS
)
);
}
if (dbConfig.getInitialInternalClientPassword() != null
&& !userMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) {
createUserInternal(authenticatorName, BasicAuthUtils.INTERNAL_USER_NAME);
setUserCredentialsInternal(
authenticatorName,
BasicAuthUtils.INTERNAL_USER_NAME,
new BasicAuthenticatorCredentialUpdate(
dbConfig.getInitialInternalClientPassword(),
BasicAuthUtils.DEFAULT_KEY_ITERATIONS
)
);
}
}
}
ScheduledExecutors.scheduleWithFixedDelay(
exec,
new Duration(commonCacheConfig.getPollingPeriod()),
new Duration(commonCacheConfig.getPollingPeriod()),
new Callable<ScheduledExecutors.Signal>()
{
@Override
public ScheduledExecutors.Signal call() throws Exception
{
if (stopped) {
return ScheduledExecutors.Signal.STOP;
}
try {
LOG.debug("Scheduled db poll is running");
for (String authenticatorPrefix : authenticatorPrefixes) {
byte[] userMapBytes = getCurrentUserMapBytes(authenticatorPrefix);
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
userMapBytes
);
if (userMapBytes != null) {
cachedUserMaps.put(authenticatorPrefix, new BasicAuthenticatorUserMapBundle(userMap, userMapBytes));
}
}
LOG.debug("Scheduled db poll is done");
}
catch (Throwable t) {
LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit();
}
return ScheduledExecutors.Signal.REPEAT;
}
}
);
lifecycleLock.started();
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
throw new ISE("can't stop.");
}
LOG.info("CoordinatorBasicAuthenticatorMetadataStorageUpdater is stopping.");
stopped = true;
LOG.info("CoordinatorBasicAuthenticatorMetadataStorageUpdater is stopped.");
}
@Override
public void createUser(String prefix, String userName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
createUserInternal(prefix, userName);
}
@Override
public void deleteUser(String prefix, String userName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
deleteUserInternal(prefix, userName);
}
@Override
public void setUserCredentials(String prefix, String userName, BasicAuthenticatorCredentialUpdate update)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
setUserCredentialsInternal(prefix, userName, update);
}
@Override
public Map<String, BasicAuthenticatorUser> getCachedUserMap(String prefix)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix);
if (bundle == null) {
return null;
} else {
return bundle.getUserMap();
}
}
@Override
public byte[] getCachedSerializedUserMap(String prefix)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
BasicAuthenticatorUserMapBundle bundle = cachedUserMaps.get(prefix);
if (bundle == null) {
return null;
} else {
return bundle.getSerializedUserMap();
}
}
@Override
public byte[] getCurrentUserMapBytes(String prefix)
{
return connector.lookup(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, USERS)
);
}
@Override
public void refreshAllNotification()
{
cachedUserMaps.forEach(
(authenticatorName, userMapBundle) -> {
cacheNotifier.addUpdate(authenticatorName, userMapBundle.getSerializedUserMap());
}
);
}
private static String getPrefixedKeyColumn(String keyPrefix, String keyName)
{
return StringUtils.format("basic_authentication_%s_%s", keyPrefix, keyName);
}
private boolean tryUpdateUserMap(
String prefix,
Map<String, BasicAuthenticatorUser> userMap,
byte[] oldValue,
byte[] newValue
)
{
try {
MetadataCASUpdate update = new MetadataCASUpdate(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, USERS),
oldValue,
newValue
);
boolean succeeded = connector.compareAndSwap(
Collections.singletonList(update)
);
if (succeeded) {
cachedUserMaps.put(prefix, new BasicAuthenticatorUserMapBundle(userMap, newValue));
cacheNotifier.addUpdate(prefix, newValue);
return true;
} else {
return false;
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
private void createUserInternal(String prefix, String userName)
{
int attempts = 0;
while (attempts < numRetries) {
if (createUserOnce(prefix, userName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not create user[%s] due to concurrent update contention.", userName);
}
private void deleteUserInternal(String prefix, String userName)
{
int attempts = 0;
while (attempts < numRetries) {
if (deleteUserOnce(prefix, userName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not delete user[%s] due to concurrent update contention.", userName);
}
private void setUserCredentialsInternal(String prefix, String userName, BasicAuthenticatorCredentialUpdate update)
{
BasicAuthenticatorCredentials credentials;
// use default iteration count from Authenticator if not specified in request
if (update.getIterations() == -1) {
BasicHTTPAuthenticator authenticator = (BasicHTTPAuthenticator) authenticatorMapper.getAuthenticatorMap().get(
prefix
);
credentials = new BasicAuthenticatorCredentials(
new BasicAuthenticatorCredentialUpdate(
update.getPassword(),
authenticator.getDbConfig().getIterations()
)
);
} else {
credentials = new BasicAuthenticatorCredentials(update);
}
int attempts = 0;
while (attempts < numRetries) {
if (setUserCredentialOnce(prefix, userName, credentials)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not set credentials for user[%s] due to concurrent update contention.", userName);
}
private boolean createUserOnce(String prefix, String userName)
{
byte[] oldValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
oldValue
);
if (userMap.get(userName) != null) {
throw new BasicSecurityDBResourceException("User [%s] already exists.", userName);
} else {
userMap.put(userName, new BasicAuthenticatorUser(userName, null));
}
byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap);
return tryUpdateUserMap(prefix, userMap, oldValue, newValue);
}
private boolean deleteUserOnce(String prefix, String userName)
{
byte[] oldValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
oldValue
);
if (userMap.get(userName) == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
} else {
userMap.remove(userName);
}
byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap);
return tryUpdateUserMap(prefix, userMap, oldValue, newValue);
}
private boolean setUserCredentialOnce(String prefix, String userName, BasicAuthenticatorCredentials credentials)
{
byte[] oldValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
oldValue
);
if (userMap.get(userName) == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
} else {
userMap.put(userName, new BasicAuthenticatorUser(userName, credentials));
}
byte[] newValue = BasicAuthUtils.serializeAuthenticatorUserMap(objectMapper, userMap);
return tryUpdateUserMap(prefix, userMap, oldValue, newValue);
}
}

View File

@ -0,0 +1,235 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.endpoint;
import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ResourceFilters;
import io.druid.guice.LazySingleton;
import io.druid.security.basic.BasicSecurityResourceFilter;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@Path("/druid-ext/basic-security/authentication")
@LazySingleton
public class BasicAuthenticatorResource
{
private final BasicAuthenticatorResourceHandler handler;
@Inject
public BasicAuthenticatorResource(
BasicAuthenticatorResourceHandler handler
)
{
this.handler = handler;
}
/**
* @param req HTTP request
*
* @return Load status of authenticator DB caches
*/
@GET
@Path("/loadStatus")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getLoadStatus(
@Context HttpServletRequest req
)
{
return handler.getLoadStatus();
}
/**
* @param req HTTP request
*
* Sends an "update" notification to all services with the current user database state,
* causing them to refresh their DB cache state.
*/
@GET
@Path("/refreshAll")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response refreshAll(
@Context HttpServletRequest req
)
{
return handler.refreshAll();
}
/**
* @param req HTTP request
*
* @return List of all users
*/
@GET
@Path("/db/{authenticatorName}/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getAllUsers(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName
)
{
return handler.getAllUsers(authenticatorName);
}
/**
* @param req HTTP request
* @param userName Name of user to retrieve information about
*
* @return Name and credentials of the user with userName, 400 error response if user doesn't exist
*/
@GET
@Path("/db/{authenticatorName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getUser(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName,
@PathParam("userName") final String userName
)
{
return handler.getUser(authenticatorName, userName);
}
/**
* Create a new user with name userName
*
* @param req HTTP request
* @param userName Name to assign the new user
*
* @return OK response, or 400 error response if user already exists
*/
@POST
@Path("/db/{authenticatorName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response createUser(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName,
@PathParam("userName") String userName
)
{
return handler.createUser(authenticatorName, userName);
}
/**
* Delete a user
*
* @param req HTTP request
* @param userName Name of user to delete
*
* @return OK response, or 400 error response if user doesn't exist
*/
@DELETE
@Path("/db/{authenticatorName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response deleteUser(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName,
@PathParam("userName") String userName
)
{
return handler.deleteUser(authenticatorName, userName);
}
/**
* Assign credentials for a user
*
* @param req HTTP request
* @param userName Name of user
* @param password Password to assign
*
* @return OK response, 400 error if user doesn't exist
*/
@POST
@Path("/db/{authenticatorName}/users/{userName}/credentials")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response updateUserCredentials(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName,
@PathParam("userName") String userName,
BasicAuthenticatorCredentialUpdate update
)
{
return handler.updateUserCredentials(authenticatorName, userName, update);
}
/**
* @param req HTTP request
*
* @return serialized user map
*/
@GET
@Path("/db/{authenticatorName}/cachedSerializedUserMap")
@Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getCachedSerializedUserMap(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName
)
{
return handler.getCachedSerializedUserMap(authenticatorName);
}
/**
* Listen for update notifications for the auth storage
*
* @param req HTTP request
* @param userName Name to assign the new user
*
* @return OK response, or 400 error response if user already exists
*/
@POST
@Path("/listen/{authenticatorName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response authenticatorUpdateListener(
@Context HttpServletRequest req,
@PathParam("authenticatorName") final String authenticatorName,
byte[] serializedUserMap
)
{
return handler.authenticatorUpdateListener(authenticatorName, serializedUserMap);
}
}

View File

@ -0,0 +1,53 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.endpoint;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import javax.ws.rs.core.Response;
/**
* Handles authenticator-related API calls. Coordinator and non-coordinator methods are combined here because of an
* inability to selectively inject jetty resources in configure(Binder binder) of the extension module based
* on node type.
*/
public interface BasicAuthenticatorResourceHandler
{
// coordinator methods
Response getAllUsers(String authenticatorName);
Response getUser(String authenticatorName, String userName);
Response createUser(String authenticatorName, String userName);
Response deleteUser(String authenticatorName, String userName);
Response updateUserCredentials(String authenticatorName, String userName, BasicAuthenticatorCredentialUpdate update);
Response getCachedSerializedUserMap(String authenticatorName);
Response refreshAll();
// non-coordinator methods
Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap);
// common methods
Response getLoadStatus();
}

View File

@ -0,0 +1,218 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.endpoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.StringUtils;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.db.updater.BasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;
public class CoordinatorBasicAuthenticatorResourceHandler implements BasicAuthenticatorResourceHandler
{
private final BasicAuthenticatorMetadataStorageUpdater storageUpdater;
private final Map<String, BasicHTTPAuthenticator> authenticatorMap;
private final ObjectMapper objectMapper;
@Inject
public CoordinatorBasicAuthenticatorResourceHandler(
BasicAuthenticatorMetadataStorageUpdater storageUpdater,
AuthenticatorMapper authenticatorMapper,
@Smile ObjectMapper objectMapper
)
{
this.storageUpdater = storageUpdater;
this.objectMapper = objectMapper;
this.authenticatorMap = Maps.newHashMap();
for (Map.Entry<String, Authenticator> authenticatorEntry : authenticatorMapper.getAuthenticatorMap().entrySet()) {
final String authenticatorName = authenticatorEntry.getKey();
final Authenticator authenticator = authenticatorEntry.getValue();
if (authenticator instanceof BasicHTTPAuthenticator) {
authenticatorMap.put(
authenticatorName,
(BasicHTTPAuthenticator) authenticator
);
}
}
}
@Override
public Response getAllUsers(
final String authenticatorName
)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authenticatorName)
);
return Response.ok(userMap.keySet()).build();
}
@Override
public Response getUser(String authenticatorName, String userName)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authenticatorName)
);
try {
BasicAuthenticatorUser user = userMap.get(userName);
if (user == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
}
return Response.ok(user).build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response createUser(String authenticatorName, String userName)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
try {
storageUpdater.createUser(authenticatorName, userName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response deleteUser(String authenticatorName, String userName)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
try {
storageUpdater.deleteUser(authenticatorName, userName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response updateUserCredentials(String authenticatorName, String userName, BasicAuthenticatorCredentialUpdate update)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
try {
storageUpdater.setUserCredentials(authenticatorName, userName, update);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response getCachedSerializedUserMap(String authenticatorName)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
return makeResponseForAuthenticatorNotFound(authenticatorName);
}
return Response.ok(storageUpdater.getCachedSerializedUserMap(authenticatorName)).build();
}
@Override
public Response refreshAll()
{
storageUpdater.refreshAllNotification();
return Response.ok().build();
}
@Override
public Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap)
{
return Response.status(Response.Status.NOT_FOUND).build();
}
@Override
public Response getLoadStatus()
{
Map<String, Boolean> loadStatus = new HashMap<>();
authenticatorMap.forEach(
(authenticatorName, authenticator) -> {
loadStatus.put(authenticatorName, storageUpdater.getCachedUserMap(authenticatorName) != null);
}
);
return Response.ok(loadStatus).build();
}
private static Response makeResponseForAuthenticatorNotFound(String authenticatorName)
{
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error",
StringUtils.format("Basic authenticator with name [%s] does not exist.", authenticatorName)
))
.build();
}
private static Response makeResponseForBasicSecurityDBResourceException(BasicSecurityDBResourceException bsre)
{
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error", bsre.getMessage()
))
.build();
}
}

View File

@ -0,0 +1,142 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.endpoint;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheManager;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.server.security.Authenticator;
import io.druid.server.security.AuthenticatorMapper;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;
public class DefaultBasicAuthenticatorResourceHandler implements BasicAuthenticatorResourceHandler
{
private static final Logger log = new Logger(DefaultBasicAuthenticatorResourceHandler.class);
private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build();
private final BasicAuthenticatorCacheManager cacheManager;
private final Map<String, BasicHTTPAuthenticator> authenticatorMap;
@Inject
public DefaultBasicAuthenticatorResourceHandler(
BasicAuthenticatorCacheManager cacheManager,
AuthenticatorMapper authenticatorMapper
)
{
this.cacheManager = cacheManager;
this.authenticatorMap = Maps.newHashMap();
for (Map.Entry<String, Authenticator> authenticatorEntry : authenticatorMapper.getAuthenticatorMap().entrySet()) {
final String authenticatorName = authenticatorEntry.getKey();
final Authenticator authenticator = authenticatorEntry.getValue();
if (authenticator instanceof BasicHTTPAuthenticator) {
authenticatorMap.put(
authenticatorName,
(BasicHTTPAuthenticator) authenticator
);
}
}
}
@Override
public Response getAllUsers(String authenticatorName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getUser(String authenticatorName, String userName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response createUser(String authenticatorName, String userName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response deleteUser(String authenticatorName, String userName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response updateUserCredentials(
String authenticatorName,
String userName,
BasicAuthenticatorCredentialUpdate update
)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getCachedSerializedUserMap(String authenticatorName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response refreshAll()
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response authenticatorUpdateListener(String authenticatorName, byte[] serializedUserMap)
{
final BasicHTTPAuthenticator authenticator = authenticatorMap.get(authenticatorName);
if (authenticator == null) {
String errMsg = StringUtils.format("Received update for unknown authenticator[%s]", authenticatorName);
log.error(errMsg);
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error",
StringUtils.format(errMsg)
))
.build();
}
cacheManager.handleAuthenticatorUpdate(authenticatorName, serializedUserMap);
return Response.ok().build();
}
@Override
public Response getLoadStatus()
{
Map<String, Boolean> loadStatus = new HashMap<>();
authenticatorMap.forEach(
(authenticatorName, authenticator) -> {
loadStatus.put(authenticatorName, cacheManager.getUserMap(authenticatorName) != null);
}
);
return Response.ok(loadStatus).build();
}
}

View File

@ -0,0 +1,55 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.StringUtils;
public class BasicAuthenticatorCredentialUpdate
{
private final String password;
private final int iterations;
@JsonCreator
public BasicAuthenticatorCredentialUpdate(
@JsonProperty("password") String password,
@JsonProperty("iterations") Integer iterations
)
{
Preconditions.checkNotNull(password, "Cannot assign null password.");
Preconditions.checkArgument(!StringUtils.isEmpty(password), "Cannot assign empty password.");
this.password = password;
this.iterations = iterations == null ? -1 : iterations;
}
@JsonProperty
public String getPassword()
{
return password;
}
@JsonProperty
public int getIterations()
{
return iterations;
}
}

View File

@ -0,0 +1,104 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import io.druid.security.basic.BasicAuthUtils;
import java.util.Arrays;
public class BasicAuthenticatorCredentials
{
private final byte[] salt;
private final byte[] hash;
private final int iterations;
@JsonCreator
public BasicAuthenticatorCredentials(
@JsonProperty("salt") byte[] salt,
@JsonProperty("hash") byte[] hash,
@JsonProperty("iterations") int iterations
)
{
Preconditions.checkNotNull(salt);
Preconditions.checkNotNull(hash);
this.salt = salt;
this.hash = hash;
this.iterations = iterations;
}
public BasicAuthenticatorCredentials(BasicAuthenticatorCredentialUpdate update)
{
this.iterations = update.getIterations();
this.salt = BasicAuthUtils.generateSalt();
this.hash = BasicAuthUtils.hashPassword(update.getPassword().toCharArray(), salt, iterations);
}
@JsonProperty
public byte[] getSalt()
{
return salt;
}
@JsonProperty
public byte[] getHash()
{
return hash;
}
@JsonProperty
public int getIterations()
{
return iterations;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthenticatorCredentials that = (BasicAuthenticatorCredentials) o;
if (getIterations() != that.getIterations()) {
return false;
}
if (!Arrays.equals(getSalt(), that.getSalt())) {
return false;
}
return Arrays.equals(getHash(), that.getHash());
}
@Override
public int hashCode()
{
int result = Arrays.hashCode(getSalt());
result = 31 * result + Arrays.hashCode(getHash());
result = 31 * result + getIterations();
return result;
}
}

View File

@ -0,0 +1,77 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class BasicAuthenticatorUser
{
private final String name;
private final BasicAuthenticatorCredentials credentials;
@JsonCreator
public BasicAuthenticatorUser(
@JsonProperty("name") String name,
@JsonProperty("credentials") BasicAuthenticatorCredentials credentials
)
{
this.name = name;
this.credentials = credentials;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public BasicAuthenticatorCredentials getCredentials()
{
return credentials;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthenticatorUser that = (BasicAuthenticatorUser) o;
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
return false;
}
return getCredentials() != null ? getCredentials().equals(that.getCredentials()) : that.getCredentials() == null;
}
@Override
public int hashCode()
{
int result = getName() != null ? getName().hashCode() : 0;
result = 31 * result + (getCredentials() != null ? getCredentials().hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,53 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authentication.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
public class BasicAuthenticatorUserMapBundle
{
private final Map<String, BasicAuthenticatorUser> userMap;
private final byte[] serializedUserMap;
@JsonCreator
public BasicAuthenticatorUserMapBundle(
@JsonProperty("userMap") Map<String, BasicAuthenticatorUser> userMap,
@JsonProperty("serializedUserMap") byte[] serializedUserMap
)
{
this.userMap = userMap;
this.serializedUserMap = serializedUserMap;
}
@JsonProperty
public Map<String, BasicAuthenticatorUser> getUserMap()
{
return userMap;
}
@JsonProperty
public byte[] getSerializedUserMap()
{
return serializedUserMap;
}
}

View File

@ -0,0 +1,125 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.druid.java.util.common.IAE;
import io.druid.security.basic.BasicAuthDBConfig;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager;
import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.server.security.Access;
import io.druid.server.security.Action;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.Authorizer;
import io.druid.server.security.Resource;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@JsonTypeName("basic")
public class BasicRoleBasedAuthorizer implements Authorizer
{
private final BasicAuthorizerCacheManager cacheManager;
private final String name;
private final BasicAuthDBConfig dbConfig;
@JsonCreator
public BasicRoleBasedAuthorizer(
@JacksonInject BasicAuthorizerCacheManager cacheManager,
@JsonProperty("name") String name,
@JsonProperty("enableCacheNotifications") Boolean enableCacheNotifications,
@JsonProperty("cacheNotificationTimeout") Long cacheNotificationTimeout
)
{
this.name = name;
this.cacheManager = cacheManager;
this.dbConfig = new BasicAuthDBConfig(
null,
null,
enableCacheNotifications == null ? true : enableCacheNotifications,
cacheNotificationTimeout == null ? BasicAuthDBConfig.DEFAULT_CACHE_NOTIFY_TIMEOUT_MS : cacheNotificationTimeout,
0
);
}
@Override
public Access authorize(
AuthenticationResult authenticationResult, Resource resource, Action action
)
{
if (authenticationResult == null) {
throw new IAE("WTF? authenticationResult should never be null.");
}
Map<String, BasicAuthorizerUser> userMap = cacheManager.getUserMap(name);
if (userMap == null) {
throw new IAE("Could not load userMap for authorizer [%s]", name);
}
Map<String, BasicAuthorizerRole> roleMap = cacheManager.getRoleMap(name);
if (roleMap == null) {
throw new IAE("Could not load roleMap for authorizer [%s]", name);
}
BasicAuthorizerUser user = userMap.get(authenticationResult.getIdentity());
if (user == null) {
return new Access(false);
}
for (String roleName : user.getRoles()) {
BasicAuthorizerRole role = roleMap.get(roleName);
for (BasicAuthorizerPermission permission : role.getPermissions()) {
if (permissionCheck(resource, action, permission)) {
return new Access(true);
}
}
}
return new Access(false);
}
private boolean permissionCheck(Resource resource, Action action, BasicAuthorizerPermission permission)
{
if (action != permission.getResourceAction().getAction()) {
return false;
}
Resource permissionResource = permission.getResourceAction().getResource();
if (permissionResource.getType() != resource.getType()) {
return false;
}
Pattern resourceNamePattern = permission.getResourceNamePattern();
Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName());
return resourceNameMatcher.matches();
}
public BasicAuthDBConfig getDbConfig()
{
return dbConfig;
}
}

View File

@ -0,0 +1,56 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.cache;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import java.util.Map;
/**
* This class is reponsible for maintaining a cache of the authorization database state. The BasicRBACAuthorizer
* uses an injected BasicAuthorizerCacheManager to make its authorization decisions.
*/
public interface BasicAuthorizerCacheManager
{
/**
* Update this cache manager's local state with fresh information pushed by the coordinator.
*
* @param authorizerPrefix The name of the authorizer this update applies to.
* @param serializedUserAndRoleMap The updated, serialized user and role maps
*/
void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap);
/**
* Return the cache manager's local view of the user map for the authorizer named `authorizerPrefix`.
*
* @param authorizerPrefix The name of the authorizer
* @return User map
*/
Map<String, BasicAuthorizerUser> getUserMap(String authorizerPrefix);
/**
* Return the cache manager's local view of the role map for the authorizer named `authorizerPrefix`.
*
* @param authorizerPrefix The name of the authorizer
* @return Role map
*/
Map<String, BasicAuthorizerRole> getRoleMap(String authorizerPrefix);
}

View File

@ -0,0 +1,34 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.cache;
/**
* Sends a notification to druid services, containing updated authorizer user/role map state.
*/
public interface BasicAuthorizerCacheNotifier
{
/**
* Send the user map state contained in updatedUserMap to all non-coordinator Druid services
*
* @param authorizerPrefix Name of authorizer being updated
* @param userAndRoleMap User/role map state
*/
void addUpdate(String authorizerPrefix, byte[] userAndRoleMap);
}

View File

@ -0,0 +1,122 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.cache;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.metamx.emitter.EmittingLogger;
import com.metamx.http.client.HttpClient;
import io.druid.concurrent.LifecycleLock;
import io.druid.discovery.DruidNodeDiscoveryProvider;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.EscalatedClient;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.security.basic.BasicAuthDBConfig;
import io.druid.security.basic.CommonCacheNotifier;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.server.security.Authorizer;
import io.druid.server.security.AuthorizerMapper;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ManageLifecycle
public class CoordinatorBasicAuthorizerCacheNotifier implements BasicAuthorizerCacheNotifier
{
private static final EmittingLogger LOG = new EmittingLogger(CoordinatorBasicAuthorizerCacheNotifier.class);
private final LifecycleLock lifecycleLock = new LifecycleLock();
private CommonCacheNotifier cacheNotifier;
@Inject
public CoordinatorBasicAuthorizerCacheNotifier(
AuthorizerMapper authorizerMapper,
DruidNodeDiscoveryProvider discoveryProvider,
@EscalatedClient HttpClient httpClient
)
{
cacheNotifier = new CommonCacheNotifier(
getAuthorizerConfigMap(authorizerMapper),
discoveryProvider,
httpClient,
"/druid-ext/basic-security/authorization/listen/%s",
"CoordinatorBasicAuthorizerCacheNotifier"
);
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
try {
cacheNotifier.start();
lifecycleLock.started();
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
return;
}
try {
cacheNotifier.stop();
}
finally {
lifecycleLock.exitStop();
}
}
@Override
public void addUpdate(String updatedAuthorizerPrefix, byte[] updatedUserMap)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
cacheNotifier.addUpdate(updatedAuthorizerPrefix, updatedUserMap);
}
private Map<String, BasicAuthDBConfig> getAuthorizerConfigMap(AuthorizerMapper mapper)
{
Preconditions.checkNotNull(mapper);
Preconditions.checkNotNull(mapper.getAuthorizerMap());
Map<String, BasicAuthDBConfig> authorizerConfigMap = new HashMap<>();
for (Map.Entry<String, Authorizer> entry : mapper.getAuthorizerMap().entrySet()) {
Authorizer authorizer = entry.getValue();
if (authorizer instanceof BasicRoleBasedAuthorizer) {
String authorizerName = entry.getKey();
BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer;
BasicAuthDBConfig dbConfig = basicRoleBasedAuthorizer.getDbConfig();
authorizerConfigMap.put(authorizerName, dbConfig);
}
}
return authorizerConfigMap;
}
}

View File

@ -0,0 +1,293 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.cache;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.io.Files;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.metamx.emitter.EmittingLogger;
import com.metamx.http.client.Request;
import io.druid.client.coordinator.Coordinator;
import io.druid.concurrent.LifecycleLock;
import io.druid.discovery.DruidLeaderClient;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.RetryUtils;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.concurrent.Execs;
import io.druid.java.util.common.concurrent.ScheduledExecutors;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.authentication.BytesFullResponseHandler;
import io.druid.security.basic.authentication.BytesFullResponseHolder;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.security.basic.authorization.entity.UserAndRoleMap;
import io.druid.server.security.Authorizer;
import io.druid.server.security.AuthorizerMapper;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.joda.time.Duration;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@ManageLifecycle
public class CoordinatorPollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager
{
private static final EmittingLogger LOG = new EmittingLogger(CoordinatorPollingBasicAuthorizerCacheManager.class);
private final ConcurrentHashMap<String, Map<String, BasicAuthorizerUser>> cachedUserMaps;
private final ConcurrentHashMap<String, Map<String, BasicAuthorizerRole>> cachedRoleMaps;
private final Set<String> authorizerPrefixes;
private final Injector injector;
private final ObjectMapper objectMapper;
private final LifecycleLock lifecycleLock = new LifecycleLock();
private final DruidLeaderClient druidLeaderClient;
private final BasicAuthCommonCacheConfig commonCacheConfig;
private final ScheduledExecutorService exec;
@Inject
public CoordinatorPollingBasicAuthorizerCacheManager(
Injector injector,
BasicAuthCommonCacheConfig commonCacheConfig,
@Smile ObjectMapper objectMapper,
@Coordinator DruidLeaderClient druidLeaderClient
)
{
this.exec = Execs.scheduledSingleThreaded("CoordinatorPollingBasicAuthorizerCacheManager-Exec--%d");
this.injector = injector;
this.commonCacheConfig = commonCacheConfig;
this.objectMapper = objectMapper;
this.cachedUserMaps = new ConcurrentHashMap<>();
this.cachedRoleMaps = new ConcurrentHashMap<>();
this.authorizerPrefixes = new HashSet<>();
this.druidLeaderClient = druidLeaderClient;
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
LOG.info("Starting CoordinatorPollingBasicAuthorizerCacheManager.");
try {
initUserMaps();
ScheduledExecutors.scheduleWithFixedDelay(
exec,
new Duration(commonCacheConfig.getPollingPeriod()),
new Duration(commonCacheConfig.getPollingPeriod()),
() -> {
try {
long randomDelay = ThreadLocalRandom.current().nextLong(0, commonCacheConfig.getMaxRandomDelay());
LOG.debug("Inserting random polling delay of [%s] ms", randomDelay);
Thread.sleep(randomDelay);
LOG.debug("Scheduled cache poll is running");
for (String authorizerPrefix : authorizerPrefixes) {
UserAndRoleMap userAndRoleMap = fetchUserAndRoleMapFromCoordinator(authorizerPrefix, false);
if (userAndRoleMap != null) {
cachedUserMaps.put(authorizerPrefix, userAndRoleMap.getUserMap());
cachedRoleMaps.put(authorizerPrefix, userAndRoleMap.getRoleMap());
}
}
LOG.debug("Scheduled cache poll is done");
}
catch (Throwable t) {
LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit();
}
}
);
lifecycleLock.started();
LOG.info("Started CoordinatorPollingBasicAuthorizerCacheManager.");
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
throw new ISE("can't stop.");
}
LOG.info("CoordinatorPollingBasicAuthorizerCacheManager is stopping.");
exec.shutdown();
LOG.info("CoordinatorPollingBasicAuthorizerCacheManager is stopped.");
}
@Override
public void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap)
{
LOG.debug("Received cache update for authorizer [%s].", authorizerPrefix);
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
try {
UserAndRoleMap userAndRoleMap = objectMapper.readValue(
serializedUserAndRoleMap,
BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE
);
cachedUserMaps.put(authorizerPrefix, userAndRoleMap.getUserMap());
cachedRoleMaps.put(authorizerPrefix, userAndRoleMap.getRoleMap());
if (commonCacheConfig.getCacheDirectory() != null) {
writeMapToDisk(authorizerPrefix, serializedUserAndRoleMap);
}
}
catch (Exception e) {
LOG.makeAlert(e, "WTF? Could not deserialize user/role map received from coordinator.").emit();
}
}
@Override
public Map<String, BasicAuthorizerUser> getUserMap(String authorizerPrefix)
{
return cachedUserMaps.get(authorizerPrefix);
}
@Override
public Map<String, BasicAuthorizerRole> getRoleMap(String authorizerPrefix)
{
return cachedRoleMaps.get(authorizerPrefix);
}
private String getUserRoleMapFilename(String prefix)
{
return StringUtils.format("%s.authorizer.cache", prefix);
}
@Nullable
private UserAndRoleMap loadUserAndRoleMapFromDisk(String prefix) throws IOException
{
File userAndRoleMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix));
if (!userAndRoleMapFile.exists()) {
return null;
}
return objectMapper.readValue(
userAndRoleMapFile,
BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE
);
}
private void writeMapToDisk(String prefix, byte[] userMapBytes) throws IOException
{
File cacheDir = new File(commonCacheConfig.getCacheDirectory());
cacheDir.mkdirs();
File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix));
Files.write(userMapBytes, userMapFile);
}
@Nullable
private UserAndRoleMap fetchUserAndRoleMapFromCoordinator(String prefix, boolean isInit)
{
try {
return RetryUtils.retry(
() -> {
return tryFetchMapsFromCoordinator(prefix);
},
e -> true,
commonCacheConfig.getMaxSyncRetries()
);
}
catch (Exception e) {
LOG.makeAlert(e, "Encountered exception while fetching user and role map for authorizer [%s]", prefix);
if (isInit) {
if (commonCacheConfig.getCacheDirectory() != null) {
try {
LOG.info("Attempting to load user map snapshot from disk.");
return loadUserAndRoleMapFromDisk(prefix);
}
catch (Exception e2) {
e2.addSuppressed(e);
LOG.makeAlert(e2, "Encountered exception while loading user-role map snapshot for authorizer [%s]", prefix);
}
}
}
return null;
}
}
private UserAndRoleMap tryFetchMapsFromCoordinator(
String prefix
) throws Exception
{
Request req = druidLeaderClient.makeRequest(
HttpMethod.GET,
StringUtils.format("/druid-ext/basic-security/authorization/db/%s/cachedSerializedUserMap", prefix)
);
BytesFullResponseHolder responseHolder = (BytesFullResponseHolder) druidLeaderClient.go(
req,
new BytesFullResponseHandler()
);
byte[] userRoleMapBytes = responseHolder.getBytes();
UserAndRoleMap userAndRoleMap = objectMapper.readValue(
userRoleMapBytes,
BasicAuthUtils.AUTHORIZER_USER_AND_ROLE_MAP_TYPE_REFERENCE
);
if (userAndRoleMap != null && commonCacheConfig.getCacheDirectory() != null) {
writeMapToDisk(prefix, userRoleMapBytes);
}
return userAndRoleMap;
}
private void initUserMaps()
{
AuthorizerMapper authorizerMapper = injector.getInstance(AuthorizerMapper.class);
if (authorizerMapper == null || authorizerMapper.getAuthorizerMap() == null) {
return;
}
for (Map.Entry<String, Authorizer> entry : authorizerMapper.getAuthorizerMap().entrySet()) {
Authorizer authorizer = entry.getValue();
if (authorizer instanceof BasicRoleBasedAuthorizer) {
String authorizerName = entry.getKey();
authorizerPrefixes.add(authorizerName);
UserAndRoleMap userAndRoleMap = fetchUserAndRoleMapFromCoordinator(authorizerName, true);
if (userAndRoleMap != null) {
cachedUserMaps.put(authorizerName, userAndRoleMap.getUserMap());
cachedRoleMaps.put(authorizerName, userAndRoleMap.getRoleMap());
}
}
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.cache;
import com.google.inject.Inject;
import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import java.util.Map;
public class MetadataStoragePollingBasicAuthorizerCacheManager implements BasicAuthorizerCacheManager
{
private final BasicAuthorizerMetadataStorageUpdater storageUpdater;
@Inject
public MetadataStoragePollingBasicAuthorizerCacheManager(
BasicAuthorizerMetadataStorageUpdater storageUpdater
)
{
this.storageUpdater = storageUpdater;
}
@Override
public void handleAuthorizerUpdate(String authorizerPrefix, byte[] serializedUserAndRoleMap)
{
}
@Override
public Map<String, BasicAuthorizerUser> getUserMap(String authorizerPrefix)
{
return storageUpdater.getCachedUserMap(authorizerPrefix);
}
@Override
public Map<String, BasicAuthorizerRole> getRoleMap(String authorizerPrefix)
{
return storageUpdater.getCachedRoleMap(authorizerPrefix);
}
}

View File

@ -0,0 +1,60 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.updater;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.server.security.ResourceAction;
import java.util.List;
import java.util.Map;
/**
* Implementations of this interface are responsible for connecting directly to the metadata storage,
* modifying the authorizer database state or reading it. This interface is used by the
* MetadataStoragePollingBasicAuthorizerCacheManager (for reads) and the CoordinatorBasicAuthorizerResourceHandler
* (for handling configuration read/writes).
*/
public interface BasicAuthorizerMetadataStorageUpdater
{
void createUser(String prefix, String userName);
void deleteUser(String prefix, String userName);
void createRole(String prefix, String roleName);
void deleteRole(String prefix, String roleName);
void assignRole(String prefix, String userName, String roleName);
void unassignRole(String prefix, String userName, String roleName);
void setPermissions(String prefix, String roleName, List<ResourceAction> permissions);
Map<String, BasicAuthorizerUser> getCachedUserMap(String prefix);
Map<String, BasicAuthorizerRole> getCachedRoleMap(String prefix);
byte[] getCurrentUserMapBytes(String prefix);
byte[] getCurrentRoleMapBytes(String prefix);
void refreshAllNotification();
}

View File

@ -0,0 +1,790 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.db.updater;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.metamx.emitter.EmittingLogger;
import io.druid.common.config.ConfigManager;
import io.druid.concurrent.LifecycleLock;
import io.druid.guice.ManageLifecycle;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.concurrent.Execs;
import io.druid.java.util.common.concurrent.ScheduledExecutors;
import io.druid.java.util.common.lifecycle.LifecycleStart;
import io.druid.java.util.common.lifecycle.LifecycleStop;
import io.druid.metadata.MetadataCASUpdate;
import io.druid.metadata.MetadataStorageConnector;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier;
import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleMapBundle;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUserMapBundle;
import io.druid.security.basic.authorization.entity.UserAndRoleMap;
import io.druid.server.security.Action;
import io.druid.server.security.Authorizer;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import org.joda.time.Duration;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@ManageLifecycle
public class CoordinatorBasicAuthorizerMetadataStorageUpdater implements BasicAuthorizerMetadataStorageUpdater
{
private static final EmittingLogger LOG =
new EmittingLogger(CoordinatorBasicAuthorizerMetadataStorageUpdater.class);
private static final long UPDATE_RETRY_DELAY = 1000;
private static final String USERS = "users";
private static final String ROLES = "roles";
public static final List<ResourceAction> SUPERUSER_PERMISSIONS = makeSuperUserPermissions();
private final AuthorizerMapper authorizerMapper;
private final MetadataStorageConnector connector;
private final MetadataStorageTablesConfig connectorConfig;
private final BasicAuthorizerCacheNotifier cacheNotifier;
private final BasicAuthCommonCacheConfig commonCacheConfig;
private final ObjectMapper objectMapper;
private final int numRetries = 5;
private final Map<String, BasicAuthorizerUserMapBundle> cachedUserMaps;
private final Map<String, BasicAuthorizerRoleMapBundle> cachedRoleMaps;
private final Set<String> authorizerNames;
private final LifecycleLock lifecycleLock = new LifecycleLock();
private final ScheduledExecutorService exec;
private volatile boolean stopped = false;
@Inject
public CoordinatorBasicAuthorizerMetadataStorageUpdater(
AuthorizerMapper authorizerMapper,
MetadataStorageConnector connector,
MetadataStorageTablesConfig connectorConfig,
BasicAuthCommonCacheConfig commonCacheConfig,
@Smile ObjectMapper objectMapper,
BasicAuthorizerCacheNotifier cacheNotifier,
ConfigManager configManager // ConfigManager creates the db table we need, set a dependency here
)
{
this.exec = Execs.scheduledSingleThreaded("CoordinatorBasicAuthorizerMetadataStorageUpdater-Exec--%d");
this.authorizerMapper = authorizerMapper;
this.connector = connector;
this.connectorConfig = connectorConfig;
this.commonCacheConfig = commonCacheConfig;
this.objectMapper = objectMapper;
this.cacheNotifier = cacheNotifier;
this.cachedUserMaps = new ConcurrentHashMap<>();
this.cachedRoleMaps = new ConcurrentHashMap<>();
this.authorizerNames = new HashSet<>();
}
@LifecycleStart
public void start()
{
if (!lifecycleLock.canStart()) {
throw new ISE("can't start.");
}
if (authorizerMapper == null || authorizerMapper.getAuthorizerMap() == null) {
return;
}
try {
LOG.info("Starting CoordinatorBasicAuthorizerMetadataStorageUpdater");
for (Map.Entry<String, Authorizer> entry : authorizerMapper.getAuthorizerMap().entrySet()) {
Authorizer authorizer = entry.getValue();
if (authorizer instanceof BasicRoleBasedAuthorizer) {
String authorizerName = entry.getKey();
authorizerNames.add(authorizerName);
BasicRoleBasedAuthorizer basicRoleBasedAuthorizer = (BasicRoleBasedAuthorizer) authorizer;
byte[] userMapBytes = getCurrentUserMapBytes(authorizerName);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
userMapBytes
);
cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes));
byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
roleMapBytes
);
cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes));
initSuperusers(authorizerName, userMap, roleMap);
}
}
ScheduledExecutors.scheduleWithFixedDelay(
exec,
new Duration(commonCacheConfig.getPollingPeriod()),
new Duration(commonCacheConfig.getPollingPeriod()),
new Callable<ScheduledExecutors.Signal>()
{
@Override
public ScheduledExecutors.Signal call() throws Exception
{
if (stopped) {
return ScheduledExecutors.Signal.STOP;
}
try {
LOG.debug("Scheduled db poll is running");
for (String authorizerName : authorizerNames) {
byte[] userMapBytes = getCurrentUserMapBytes(authorizerName);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
userMapBytes
);
if (userMapBytes != null) {
synchronized (cachedUserMaps) {
cachedUserMaps.put(authorizerName, new BasicAuthorizerUserMapBundle(userMap, userMapBytes));
}
}
byte[] roleMapBytes = getCurrentRoleMapBytes(authorizerName);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
roleMapBytes
);
if (roleMapBytes != null) {
synchronized (cachedUserMaps) {
cachedRoleMaps.put(authorizerName, new BasicAuthorizerRoleMapBundle(roleMap, roleMapBytes));
}
}
}
LOG.debug("Scheduled db poll is done");
}
catch (Throwable t) {
LOG.makeAlert(t, "Error occured while polling for cachedUserMaps.").emit();
}
return ScheduledExecutors.Signal.REPEAT;
}
}
);
lifecycleLock.started();
}
finally {
lifecycleLock.exitStart();
}
}
@LifecycleStop
public void stop()
{
if (!lifecycleLock.canStop()) {
throw new ISE("can't stop.");
}
LOG.info("CoordinatorBasicAuthorizerMetadataStorageUpdater is stopping.");
stopped = true;
LOG.info("CoordinatorBasicAuthorizerMetadataStorageUpdater is stopped.");
}
private static String getPrefixedKeyColumn(String keyPrefix, String keyName)
{
return StringUtils.format("basic_authorization_%s_%s", keyPrefix, keyName);
}
private boolean tryUpdateUserMap(
String prefix,
Map<String, BasicAuthorizerUser> userMap,
byte[] oldUserMapValue,
byte[] newUserMapValue
)
{
return tryUpdateUserAndRoleMap(prefix, userMap, oldUserMapValue, newUserMapValue, null, null, null);
}
private boolean tryUpdateRoleMap(
String prefix,
Map<String, BasicAuthorizerRole> roleMap,
byte[] oldRoleMapValue,
byte[] newRoleMapValue
)
{
return tryUpdateUserAndRoleMap(prefix, null, null, null, roleMap, oldRoleMapValue, newRoleMapValue);
}
private boolean tryUpdateUserAndRoleMap(
String prefix,
Map<String, BasicAuthorizerUser> userMap,
byte[] oldUserMapValue,
byte[] newUserMapValue,
Map<String, BasicAuthorizerRole> roleMap,
byte[] oldRoleMapValue,
byte[] newRoleMapValue
)
{
try {
List<MetadataCASUpdate> updates = new ArrayList<>();
if (userMap != null) {
updates.add(
new MetadataCASUpdate(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, USERS),
oldUserMapValue,
newUserMapValue
)
);
}
if (roleMap != null) {
updates.add(
new MetadataCASUpdate(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, ROLES),
oldRoleMapValue,
newRoleMapValue
)
);
}
boolean succeeded = connector.compareAndSwap(updates);
if (succeeded) {
if (userMap != null) {
cachedUserMaps.put(prefix, new BasicAuthorizerUserMapBundle(userMap, newUserMapValue));
}
if (roleMap != null) {
cachedRoleMaps.put(prefix, new BasicAuthorizerRoleMapBundle(roleMap, newRoleMapValue));
}
byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(prefix);
cacheNotifier.addUpdate(prefix, serializedUserAndRoleMap);
return true;
} else {
return false;
}
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void createUser(String prefix, String userName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
createUserInternal(prefix, userName);
}
@Override
public void deleteUser(String prefix, String userName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
deleteUserInternal(prefix, userName);
}
@Override
public void createRole(String prefix, String roleName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
createRoleInternal(prefix, roleName);
}
@Override
public void deleteRole(String prefix, String roleName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
deleteRoleInternal(prefix, roleName);
}
@Override
public void assignRole(String prefix, String userName, String roleName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
assignRoleInternal(prefix, userName, roleName);
}
@Override
public void unassignRole(String prefix, String userName, String roleName)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
unassignRoleInternal(prefix, userName, roleName);
}
@Override
public void setPermissions(
String prefix, String roleName, List<ResourceAction> permissions
)
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
setPermissionsInternal(prefix, roleName, permissions);
}
@Override
@Nullable
public Map<String, BasicAuthorizerUser> getCachedUserMap(String prefix)
{
BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix);
return userMapBundle == null ? null : userMapBundle.getUserMap();
}
@Override
@Nullable
public Map<String, BasicAuthorizerRole> getCachedRoleMap(String prefix)
{
BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix);
return roleMapBundle == null ? null : roleMapBundle.getRoleMap();
}
@Override
public byte[] getCurrentUserMapBytes(String prefix)
{
return connector.lookup(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, USERS)
);
}
@Override
public byte[] getCurrentRoleMapBytes(String prefix)
{
return connector.lookup(
connectorConfig.getConfigTable(),
MetadataStorageConnector.CONFIG_TABLE_KEY_COLUMN,
MetadataStorageConnector.CONFIG_TABLE_VALUE_COLUMN,
getPrefixedKeyColumn(prefix, ROLES)
);
}
@Override
public void refreshAllNotification()
{
authorizerNames.forEach(
(authorizerName) -> {
try {
byte[] serializedUserAndRoleMap = getCurrentUserAndRoleMapSerialized(authorizerName);
cacheNotifier.addUpdate(authorizerName, serializedUserAndRoleMap);
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
);
}
private byte[] getCurrentUserAndRoleMapSerialized(String prefix) throws IOException
{
BasicAuthorizerUserMapBundle userMapBundle = cachedUserMaps.get(prefix);
BasicAuthorizerRoleMapBundle roleMapBundle = cachedRoleMaps.get(prefix);
UserAndRoleMap userAndRoleMap = new UserAndRoleMap(
userMapBundle == null ? null : userMapBundle.getUserMap(),
roleMapBundle == null ? null : roleMapBundle.getRoleMap()
);
return objectMapper.writeValueAsBytes(userAndRoleMap);
}
private void createUserInternal(String prefix, String userName)
{
int attempts = 0;
while (attempts < numRetries) {
if (createUserOnce(prefix, userName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not create user[%s] due to concurrent update contention.", userName);
}
private void deleteUserInternal(String prefix, String userName)
{
int attempts = 0;
while (attempts < numRetries) {
if (deleteUserOnce(prefix, userName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not delete user[%s] due to concurrent update contention.", userName);
}
private void createRoleInternal(String prefix, String roleName)
{
int attempts = 0;
while (attempts < numRetries) {
if (createRoleOnce(prefix, roleName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not create role[%s] due to concurrent update contention.", roleName);
}
private void deleteRoleInternal(String prefix, String roleName)
{
int attempts = 0;
while (attempts < numRetries) {
if (deleteRoleOnce(prefix, roleName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not delete role[%s] due to concurrent update contention.", roleName);
}
private void assignRoleInternal(String prefix, String userName, String roleName)
{
int attempts = 0;
while (attempts < numRetries) {
if (assignRoleOnce(prefix, userName, roleName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not assign role[%s] to user[%s] due to concurrent update contention.", roleName, userName);
}
private void unassignRoleInternal(String prefix, String userName, String roleName)
{
int attempts = 0;
while (attempts < numRetries) {
if (unassignRoleOnce(prefix, userName, roleName)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not unassign role[%s] from user[%s] due to concurrent update contention.", roleName, userName);
}
private void setPermissionsInternal(String prefix, String roleName, List<ResourceAction> permissions)
{
int attempts = 0;
while (attempts < numRetries) {
if (setPermissionsOnce(prefix, roleName, permissions)) {
return;
} else {
attempts++;
}
try {
Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));
}
catch (InterruptedException ie) {
throw new RuntimeException(ie);
}
}
throw new ISE("Could not set permissions for role[%s] due to concurrent update contention.", roleName);
}
private boolean createUserOnce(String prefix, String userName)
{
byte[] oldValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(objectMapper, oldValue);
if (userMap.get(userName) != null) {
throw new BasicSecurityDBResourceException("User [%s] already exists.", userName);
} else {
userMap.put(userName, new BasicAuthorizerUser(userName, null));
}
byte[] newValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);
return tryUpdateUserMap(prefix, userMap, oldValue, newValue);
}
private boolean deleteUserOnce(String prefix, String userName)
{
byte[] oldValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(objectMapper, oldValue);
if (userMap.get(userName) == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
} else {
userMap.remove(userName);
}
byte[] newValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);
return tryUpdateUserMap(prefix, userMap, oldValue, newValue);
}
private boolean createRoleOnce(String prefix, String roleName)
{
byte[] oldValue = getCurrentRoleMapBytes(prefix);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(objectMapper, oldValue);
if (roleMap.get(roleName) != null) {
throw new BasicSecurityDBResourceException("Role [%s] already exists.", roleName);
} else {
roleMap.put(roleName, new BasicAuthorizerRole(roleName, null));
}
byte[] newValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap);
return tryUpdateRoleMap(prefix, roleMap, oldValue, newValue);
}
private boolean deleteRoleOnce(String prefix, String roleName)
{
byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
oldRoleMapValue
);
if (roleMap.get(roleName) == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
} else {
roleMap.remove(roleName);
}
byte[] oldUserMapValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
oldUserMapValue
);
for (BasicAuthorizerUser user : userMap.values()) {
user.getRoles().remove(roleName);
}
byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);
byte[] newRoleMapValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap);
return tryUpdateUserAndRoleMap(
prefix,
userMap, oldUserMapValue, newUserMapValue,
roleMap, oldRoleMapValue, newRoleMapValue
);
}
private boolean assignRoleOnce(String prefix, String userName, String roleName)
{
byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
oldRoleMapValue
);
if (roleMap.get(roleName) == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
}
byte[] oldUserMapValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
oldUserMapValue
);
BasicAuthorizerUser user = userMap.get(userName);
if (userMap.get(userName) == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
}
if (user.getRoles().contains(roleName)) {
throw new BasicSecurityDBResourceException("User [%s] already has role [%s].", userName, roleName);
}
user.getRoles().add(roleName);
byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);
// Role map is unchanged, but submit as an update to ensure that the table didn't change (e.g., role deleted)
return tryUpdateUserAndRoleMap(
prefix,
userMap, oldUserMapValue, newUserMapValue,
roleMap, oldRoleMapValue, oldRoleMapValue
);
}
private boolean unassignRoleOnce(String prefix, String userName, String roleName)
{
byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
oldRoleMapValue
);
if (roleMap.get(roleName) == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
}
byte[] oldUserMapValue = getCurrentUserMapBytes(prefix);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
oldUserMapValue
);
BasicAuthorizerUser user = userMap.get(userName);
if (userMap.get(userName) == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
}
if (!user.getRoles().contains(roleName)) {
throw new BasicSecurityDBResourceException("User [%s] does not have role [%s].", userName, roleName);
}
user.getRoles().remove(roleName);
byte[] newUserMapValue = BasicAuthUtils.serializeAuthorizerUserMap(objectMapper, userMap);
// Role map is unchanged, but submit as an update to ensure that the table didn't change (e.g., role deleted)
return tryUpdateUserAndRoleMap(
prefix,
userMap, oldUserMapValue, newUserMapValue,
roleMap, oldRoleMapValue, oldRoleMapValue
);
}
private boolean setPermissionsOnce(String prefix, String roleName, List<ResourceAction> permissions)
{
byte[] oldRoleMapValue = getCurrentRoleMapBytes(prefix);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
oldRoleMapValue
);
if (roleMap.get(roleName) == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
}
roleMap.put(
roleName,
new BasicAuthorizerRole(roleName, BasicAuthorizerPermission.makePermissionList(permissions))
);
byte[] newRoleMapValue = BasicAuthUtils.serializeAuthorizerRoleMap(objectMapper, roleMap);
return tryUpdateRoleMap(prefix, roleMap, oldRoleMapValue, newRoleMapValue);
}
private void initSuperusers(
String authorizerName,
Map<String, BasicAuthorizerUser> userMap,
Map<String, BasicAuthorizerRole> roleMap
)
{
if (!roleMap.containsKey(BasicAuthUtils.ADMIN_NAME)) {
createRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME);
setPermissionsInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, SUPERUSER_PERMISSIONS);
}
if (!roleMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) {
createRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME);
setPermissionsInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, SUPERUSER_PERMISSIONS);
}
if (!userMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) {
createUserInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME);
assignRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, BasicAuthUtils.INTERNAL_USER_NAME);
}
if (!userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) {
createUserInternal(authorizerName, BasicAuthUtils.ADMIN_NAME);
assignRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.ADMIN_NAME);
}
}
private static List<ResourceAction> makeSuperUserPermissions()
{
ResourceAction datasourceR = new ResourceAction(
new Resource(".*", ResourceType.DATASOURCE),
Action.READ
);
ResourceAction datasourceW = new ResourceAction(
new Resource(".*", ResourceType.DATASOURCE),
Action.WRITE
);
ResourceAction configR = new ResourceAction(
new Resource(".*", ResourceType.CONFIG),
Action.READ
);
ResourceAction configW = new ResourceAction(
new Resource(".*", ResourceType.CONFIG),
Action.WRITE
);
ResourceAction stateR = new ResourceAction(
new Resource(".*", ResourceType.STATE),
Action.READ
);
ResourceAction stateW = new ResourceAction(
new Resource(".*", ResourceType.STATE),
Action.WRITE
);
return Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW);
}
}

View File

@ -0,0 +1,373 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.endpoint;
import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes;
import com.google.inject.Inject;
import com.sun.jersey.spi.container.ResourceFilters;
import io.druid.guice.LazySingleton;
import io.druid.security.basic.BasicSecurityResourceFilter;
import io.druid.server.security.ResourceAction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
@Path("/druid-ext/basic-security/authorization")
@LazySingleton
public class BasicAuthorizerResource
{
private BasicAuthorizerResourceHandler resourceHandler;
@Inject
public BasicAuthorizerResource(
BasicAuthorizerResourceHandler resourceHandler
)
{
this.resourceHandler = resourceHandler;
}
/**
* @param req HTTP request
*
* @return Load status of authenticator DB caches
*/
@GET
@Path("/loadStatus")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getLoadStatus(
@Context HttpServletRequest req
)
{
return resourceHandler.getLoadStatus();
}
/**
* @param req HTTP request
*
* Sends an "update" notification to all services with the current user/role database state,
* causing them to refresh their DB cache state.
*/
@GET
@Path("/refreshAll")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response refreshAll(
@Context HttpServletRequest req
)
{
return resourceHandler.refreshAll();
}
/**
* @param req HTTP request
*
* @return List of all users
*/
@GET
@Path("/db/{authorizerName}/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getAllUsers(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName
)
{
return resourceHandler.getAllUsers(authorizerName);
}
/**
* @param req HTTP request
* @param userName Name of user to retrieve information about
*
* @return Name, roles, and permissions of the user with userName, 400 error response if user doesn't exist
*/
@GET
@Path("/db/{authorizerName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getUser(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("userName") final String userName,
@QueryParam("full") String full
)
{
return resourceHandler.getUser(authorizerName, userName, full != null);
}
/**
* Create a new user with name userName
*
* @param req HTTP request
* @param userName Name to assign the new user
*
* @return OK response, or 400 error response if user already exists
*/
@POST
@Path("/db/{authorizerName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response createUser(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("userName") String userName
)
{
return resourceHandler.createUser(authorizerName, userName);
}
/**
* Delete a user
*
* @param req HTTP request
* @param userName Name of user to delete
*
* @return OK response, or 400 error response if user doesn't exist
*/
@DELETE
@Path("/db/{authorizerName}/users/{userName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response deleteUser(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("userName") String userName
)
{
return resourceHandler.deleteUser(authorizerName, userName);
}
/**
* @param req HTTP request
*
* @return List of all roles
*/
@GET
@Path("/db/{authorizerName}/roles")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getAllRoles(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName
)
{
return resourceHandler.getAllRoles(authorizerName);
}
/**
* Get info about a role
*
* @param req HTTP request
* @param roleName Name of role
*
* @return Role name, users with role, and permissions of role. 400 error if role doesn't exist.
*/
@GET
@Path("/db/{authorizerName}/roles/{roleName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getRole(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("roleName") final String roleName,
@QueryParam("full") String full
)
{
return resourceHandler.getRole(authorizerName, roleName, full != null);
}
/**
* Create a new role.
*
* @param req HTTP request
* @param roleName Name of role
*
* @return OK response, 400 error if role already exists
*/
@POST
@Path("/db/{authorizerName}/roles/{roleName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response createRole(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("roleName") final String roleName
)
{
return resourceHandler.createRole(authorizerName, roleName);
}
/**
* Delete a role.
*
* @param req HTTP request
* @param roleName Name of role
*
* @return OK response, 400 error if role doesn't exist.
*/
@DELETE
@Path("/db/{authorizerName}/roles/{roleName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response deleteRole(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("roleName") String roleName
)
{
return resourceHandler.deleteRole(authorizerName, roleName);
}
/**
* Assign a role to a user.
*
* @param req HTTP request
* @param userName Name of user
* @param roleName Name of role
*
* @return OK response. 400 error if user/role don't exist, or if user already has the role
*/
@POST
@Path("/db/{authorizerName}/users/{userName}/roles/{roleName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response assignRoleToUser(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("userName") String userName,
@PathParam("roleName") String roleName
)
{
return resourceHandler.assignRoleToUser(authorizerName, userName, roleName);
}
/**
* Remove a role from a user.
*
* @param req HTTP request
* @param userName Name of user
* @param roleName Name of role
*
* @return OK response. 400 error if user/role don't exist, or if user does not have the role.
*/
@DELETE
@Path("/db/{authorizerName}/users/{userName}/roles/{roleName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response unassignRoleFromUser(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("userName") String userName,
@PathParam("roleName") String roleName
)
{
return resourceHandler.unassignRoleFromUser(authorizerName, userName, roleName);
}
/**
* Set the permissions of a role. This replaces the previous permissions of the role.
*
* @param req HTTP request
* @param roleName Name of role
* @param resourceActions Permissions to set
*
* @return OK response. 400 error if role doesn't exist.
*/
@POST
@Path("/db/{authorizerName}/roles/{roleName}/permissions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response setRolePermissions(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
@PathParam("roleName") String roleName,
List<ResourceAction> permissions
)
{
return resourceHandler.setRolePermissions(authorizerName, roleName, permissions);
}
/**
* @param req HTTP request
*
* @return serialized user map
*/
@GET
@Path("/db/{authorizerName}/cachedSerializedUserMap")
@Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response getCachedSerializedUserMap(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName
)
{
return resourceHandler.getCachedMaps(authorizerName);
}
/**
* Listen for update notifications for the auth storage
*
* @param req HTTP request
* @param userName Name to assign the new user
*
* @return OK response, or 400 error response if user already exists
*/
@POST
@Path("/listen/{authorizerName}")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@ResourceFilters(BasicSecurityResourceFilter.class)
public Response authorizerUpdateListener(
@Context HttpServletRequest req,
@PathParam("authorizerName") final String authorizerName,
byte[] serializedUserAndRoleMap
)
{
return resourceHandler.authorizerUpdateListener(authorizerName, serializedUserAndRoleMap);
}
}

View File

@ -0,0 +1,66 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.endpoint;
import io.druid.server.security.ResourceAction;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* Handles authorizer-related API calls. Coordinator and non-coordinator methods are combined here because of an
* inability to selectively inject jetty resources in configure(Binder binder) of the extension module based
* on node type.
*/
public interface BasicAuthorizerResourceHandler
{
// coordinator methods
Response getAllUsers(String authorizerName);
Response getUser(String authorizerName, String userName, boolean isFull);
Response createUser(String authorizerName, String userName);
Response deleteUser(String authorizerName, String userName);
Response getAllRoles(String authorizerName);
Response getRole(String authorizerName, String roleName, boolean isFull);
Response createRole(String authorizerName, String roleName);
Response deleteRole(String authorizerName, String roleName);
Response assignRoleToUser(String authorizerName, String userName, String roleName);
Response unassignRoleFromUser(String authorizerName, String userName, String roleName);
Response setRolePermissions(String authorizerName, String roleName, List<ResourceAction> permissions);
Response getCachedMaps(String authorizerName);
Response refreshAll();
// non-coordinator methods
Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap);
// common
Response getLoadStatus();
}

View File

@ -0,0 +1,431 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.endpoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import io.druid.guice.annotations.Smile;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.updater.BasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleFull;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUserFull;
import io.druid.security.basic.authorization.entity.UserAndRoleMap;
import io.druid.server.security.Authorizer;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.ResourceAction;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CoordinatorBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler
{
private static final Logger log = new Logger(CoordinatorBasicAuthorizerResourceHandler.class);
private final BasicAuthorizerMetadataStorageUpdater storageUpdater;
private final Map<String, BasicRoleBasedAuthorizer> authorizerMap;
private final ObjectMapper objectMapper;
@Inject
public CoordinatorBasicAuthorizerResourceHandler(
BasicAuthorizerMetadataStorageUpdater storageUpdater,
AuthorizerMapper authorizerMapper,
@Smile ObjectMapper objectMapper
)
{
this.storageUpdater = storageUpdater;
this.objectMapper = objectMapper;
this.authorizerMap = Maps.newHashMap();
for (Map.Entry<String, Authorizer> authorizerEntry : authorizerMapper.getAuthorizerMap().entrySet()) {
final String authorizerName = authorizerEntry.getKey();
final Authorizer authorizer = authorizerEntry.getValue();
if (authorizer instanceof BasicRoleBasedAuthorizer) {
authorizerMap.put(
authorizerName,
(BasicRoleBasedAuthorizer) authorizer
);
}
}
}
@Override
public Response getAllUsers(String authorizerName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authorizerName)
);
return Response.ok(userMap.keySet()).build();
}
@Override
public Response getUser(String authorizerName, String userName, boolean isFull)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
if (isFull) {
return getUserFull(authorizerName, userName);
} else {
return getUserSimple(authorizerName, userName);
}
}
@Override
public Response createUser(String authorizerName, String userName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.createUser(authorizerName, userName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response deleteUser(String authorizerName, String userName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.deleteUser(authorizerName, userName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response getAllRoles(String authorizerName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
storageUpdater.getCurrentRoleMapBytes(authorizerName)
);
return Response.ok(roleMap.keySet()).build();
}
@Override
public Response getRole(String authorizerName, String roleName, boolean isFull)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
if (isFull) {
return getRoleFull(authorizerName, roleName);
} else {
return getRoleSimple(authorizerName, roleName);
}
}
@Override
public Response createRole(String authorizerName, String roleName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.createRole(authorizerName, roleName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response deleteRole(String authorizerName, String roleName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.deleteRole(authorizerName, roleName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response assignRoleToUser(String authorizerName, String userName, String roleName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.assignRole(authorizerName, userName, roleName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response unassignRoleFromUser(String authorizerName, String userName, String roleName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.unassignRole(authorizerName, userName, roleName);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response setRolePermissions(
String authorizerName, String roleName, List<ResourceAction> permissions
)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
try {
storageUpdater.setPermissions(authorizerName, roleName, permissions);
return Response.ok().build();
}
catch (BasicSecurityDBResourceException cfe) {
return makeResponseForBasicSecurityDBResourceException(cfe);
}
}
@Override
public Response getCachedMaps(String authorizerName)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
return makeResponseForAuthorizerNotFound(authorizerName);
}
UserAndRoleMap userAndRoleMap = new UserAndRoleMap(
storageUpdater.getCachedUserMap(authorizerName),
storageUpdater.getCachedRoleMap(authorizerName)
);
return Response.ok(userAndRoleMap).build();
}
@Override
public Response refreshAll()
{
storageUpdater.refreshAllNotification();
return Response.ok().build();
}
@Override
public Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap)
{
return Response.status(Response.Status.NOT_FOUND).build();
}
@Override
public Response getLoadStatus()
{
Map<String, Boolean> loadStatus = new HashMap<>();
authorizerMap.forEach(
(authorizerName, authorizer) -> {
loadStatus.put(authorizerName, storageUpdater.getCachedUserMap(authorizerName) != null);
}
);
return Response.ok(loadStatus).build();
}
private static Response makeResponseForAuthorizerNotFound(String authorizerName)
{
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error",
StringUtils.format("Basic authorizer with name [%s] does not exist.", authorizerName)
))
.build();
}
private static Response makeResponseForBasicSecurityDBResourceException(BasicSecurityDBResourceException bsre)
{
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error", bsre.getMessage()
))
.build();
}
private Response getUserSimple(String authorizerName, String userName)
{
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authorizerName)
);
try {
BasicAuthorizerUser user = userMap.get(userName);
if (user == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
}
return Response.ok(user).build();
}
catch (BasicSecurityDBResourceException e) {
return makeResponseForBasicSecurityDBResourceException(e);
}
}
private Response getUserFull(String authorizerName, String userName)
{
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authorizerName)
);
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
storageUpdater.getCurrentRoleMapBytes(authorizerName)
);
try {
BasicAuthorizerUser user = userMap.get(userName);
if (user == null) {
throw new BasicSecurityDBResourceException("User [%s] does not exist.", userName);
}
Set<BasicAuthorizerRole> roles = new HashSet<>();
for (String roleName : user.getRoles()) {
BasicAuthorizerRole role = roleMap.get(roleName);
if (role == null) {
log.error("User [%s] had role [%s], but role was not found.", userName, roleName);
} else {
roles.add(role);
}
}
BasicAuthorizerUserFull fullUser = new BasicAuthorizerUserFull(userName, roles);
return Response.ok(fullUser).build();
}
catch (BasicSecurityDBResourceException e) {
return makeResponseForBasicSecurityDBResourceException(e);
}
}
private Response getRoleSimple(String authorizerName, String roleName)
{
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
storageUpdater.getCurrentRoleMapBytes(authorizerName)
);
try {
BasicAuthorizerRole role = roleMap.get(roleName);
if (role == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
}
return Response.ok(role).build();
}
catch (BasicSecurityDBResourceException e) {
return makeResponseForBasicSecurityDBResourceException(e);
}
}
private Response getRoleFull(String authorizerName, String roleName)
{
Map<String, BasicAuthorizerRole> roleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
storageUpdater.getCurrentRoleMapBytes(authorizerName)
);
Map<String, BasicAuthorizerUser> userMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
storageUpdater.getCurrentUserMapBytes(authorizerName)
);
Set<String> users = new HashSet<>();
for (BasicAuthorizerUser user : userMap.values()) {
if (user.getRoles().contains(roleName)) {
users.add(user.getName());
}
}
try {
BasicAuthorizerRole role = roleMap.get(roleName);
if (role == null) {
throw new BasicSecurityDBResourceException("Role [%s] does not exist.", roleName);
}
BasicAuthorizerRoleFull roleFull = new BasicAuthorizerRoleFull(
roleName,
users,
role.getPermissions()
);
return Response.ok(roleFull).build();
}
catch (BasicSecurityDBResourceException e) {
return makeResponseForBasicSecurityDBResourceException(e);
}
}
}

View File

@ -0,0 +1,178 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.endpoint;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheManager;
import io.druid.server.security.Authorizer;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.ResourceAction;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DefaultBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler
{
private static final Logger log = new Logger(DefaultBasicAuthorizerResourceHandler.class);
private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build();
private final BasicAuthorizerCacheManager cacheManager;
private final Map<String, BasicRoleBasedAuthorizer> authorizerMap;
@Inject
public DefaultBasicAuthorizerResourceHandler(
BasicAuthorizerCacheManager cacheManager,
AuthorizerMapper authorizerMapper
)
{
this.cacheManager = cacheManager;
this.authorizerMap = Maps.newHashMap();
for (Map.Entry<String, Authorizer> authorizerEntry : authorizerMapper.getAuthorizerMap().entrySet()) {
final String authorizerName = authorizerEntry.getKey();
final Authorizer authorizer = authorizerEntry.getValue();
if (authorizer instanceof BasicRoleBasedAuthorizer) {
authorizerMap.put(
authorizerName,
(BasicRoleBasedAuthorizer) authorizer
);
}
}
}
@Override
public Response getAllUsers(String authorizerName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getUser(String authorizerName, String userName, boolean isFull)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response createUser(String authorizerName, String userName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response deleteUser(String authorizerName, String userName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getAllRoles(String authorizerName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getRole(String authorizerName, String roleName, boolean isFull)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response createRole(String authorizerName, String roleName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response deleteRole(String authorizerName, String roleName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response assignRoleToUser(String authorizerName, String userName, String roleName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response unassignRoleFromUser(String authorizerName, String userName, String roleName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response setRolePermissions(
String authorizerName, String roleName, List<ResourceAction> permissions
)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response getCachedMaps(String authorizerName)
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response refreshAll()
{
return NOT_FOUND_RESPONSE;
}
@Override
public Response authorizerUpdateListener(String authorizerName, byte[] serializedUserAndRoleMap)
{
final BasicRoleBasedAuthorizer authorizer = authorizerMap.get(authorizerName);
if (authorizer == null) {
String errMsg = StringUtils.format("Received update for unknown authorizer[%s]", authorizerName);
log.error(errMsg);
return Response.status(Response.Status.BAD_REQUEST)
.entity(ImmutableMap.<String, Object>of(
"error",
StringUtils.format(errMsg)
))
.build();
}
cacheManager.handleAuthorizerUpdate(authorizerName, serializedUserAndRoleMap);
return Response.ok().build();
}
@Override
public Response getLoadStatus()
{
Map<String, Boolean> loadStatus = new HashMap<>();
authorizerMap.forEach(
(authorizerName, authorizer) -> {
loadStatus.put(authorizerName, cacheManager.getUserMap(authorizerName) != null);
}
);
return Response.ok(loadStatus).build();
}
}

View File

@ -0,0 +1,122 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.server.security.ResourceAction;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class BasicAuthorizerPermission
{
private final ResourceAction resourceAction;
private final Pattern resourceNamePattern;
@JsonCreator
public BasicAuthorizerPermission(
@JsonProperty("resourceAction") ResourceAction resourceAction,
@JsonProperty("resourceNamePattern") Pattern resourceNamePattern
)
{
this.resourceAction = resourceAction;
this.resourceNamePattern = resourceNamePattern;
}
public BasicAuthorizerPermission(
ResourceAction resourceAction
)
{
this.resourceAction = resourceAction;
try {
this.resourceNamePattern = Pattern.compile(resourceAction.getResource().getName());
}
catch (PatternSyntaxException pse) {
throw new BasicSecurityDBResourceException(
pse,
"Invalid permission, resource name regex[%s] does not compile.",
resourceAction.getResource().getName()
);
}
}
@JsonProperty
public ResourceAction getResourceAction()
{
return resourceAction;
}
@JsonProperty
public Pattern getResourceNamePattern()
{
return resourceNamePattern;
}
public static List<BasicAuthorizerPermission> makePermissionList(List<ResourceAction> resourceActions)
{
List<BasicAuthorizerPermission> permissions = new ArrayList<>();
if (resourceActions == null) {
return permissions;
}
for (ResourceAction resourceAction : resourceActions) {
permissions.add(new BasicAuthorizerPermission(resourceAction));
}
return permissions;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthorizerPermission that = (BasicAuthorizerPermission) o;
if (getResourceAction() != null
? !getResourceAction().equals(that.getResourceAction())
: that.getResourceAction() != null) {
return false;
}
return getResourceNamePattern() != null
? getResourceNamePattern().pattern().equals(that.getResourceNamePattern().pattern())
: that.getResourceNamePattern() == null;
}
@Override
public int hashCode()
{
int result = getResourceAction() != null ? getResourceAction().hashCode() : 0;
result = 31 * result + (getResourceNamePattern().pattern() != null
? getResourceNamePattern().pattern().hashCode()
: 0);
return result;
}
}

View File

@ -0,0 +1,81 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
public class BasicAuthorizerRole
{
private final String name;
private final List<BasicAuthorizerPermission> permissions;
@JsonCreator
public BasicAuthorizerRole(
@JsonProperty("name") String name,
@JsonProperty("permissions") List<BasicAuthorizerPermission> permissions
)
{
this.name = name;
this.permissions = permissions == null ? new ArrayList<>() : permissions;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public List<BasicAuthorizerPermission> getPermissions()
{
return permissions;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthorizerRole role = (BasicAuthorizerRole) o;
if (getName() != null ? !getName().equals(role.getName()) : role.getName() != null) {
return false;
}
return getPermissions() != null ? getPermissions().equals(role.getPermissions()) : role.getPermissions() == null;
}
@Override
public int hashCode()
{
int result = getName() != null ? getName().hashCode() : 0;
result = 31 * result + (getPermissions() != null ? getPermissions().hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,95 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class BasicAuthorizerRoleFull
{
private final String name;
private final Set<String> users;
private final List<BasicAuthorizerPermission> permissions;
@JsonCreator
public BasicAuthorizerRoleFull(
@JsonProperty("name") String name,
@JsonProperty("users") Set<String> users,
@JsonProperty("permissions") List<BasicAuthorizerPermission> permissions
)
{
this.name = name;
this.users = users;
this.permissions = permissions == null ? new ArrayList<>() : permissions;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public List<BasicAuthorizerPermission> getPermissions()
{
return permissions;
}
@JsonProperty
public Set<String> getUsers()
{
return users;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthorizerRoleFull that = (BasicAuthorizerRoleFull) o;
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
return false;
}
if (getUsers() != null ? !getUsers().equals(that.getUsers()) : that.getUsers() != null) {
return false;
}
return getPermissions() != null ? getPermissions().equals(that.getPermissions()) : that.getPermissions() == null;
}
@Override
public int hashCode()
{
int result = getName() != null ? getName().hashCode() : 0;
result = 31 * result + (getUsers() != null ? getUsers().hashCode() : 0);
result = 31 * result + (getPermissions() != null ? getPermissions().hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,53 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
public class BasicAuthorizerRoleMapBundle
{
private final Map<String, BasicAuthorizerRole> roleMap;
private final byte[] serializedRoleMap;
@JsonCreator
public BasicAuthorizerRoleMapBundle(
@JsonProperty("roleMap") Map<String, BasicAuthorizerRole> roleMap,
@JsonProperty("serializedRoleMap") byte[] serializedRoleMap
)
{
this.roleMap = roleMap;
this.serializedRoleMap = serializedRoleMap;
}
@JsonProperty
public Map<String, BasicAuthorizerRole> getRoleMap()
{
return roleMap;
}
@JsonProperty
public byte[] getSerializedRoleMap()
{
return serializedRoleMap;
}
}

View File

@ -0,0 +1,81 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashSet;
import java.util.Set;
public class BasicAuthorizerUser
{
private final String name;
private final Set<String> roles;
@JsonCreator
public BasicAuthorizerUser(
@JsonProperty("name") String name,
@JsonProperty("roles") Set<String> roles
)
{
this.name = name;
this.roles = roles == null ? new HashSet<>() : roles;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public Set<String> getRoles()
{
return roles;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthorizerUser that = (BasicAuthorizerUser) o;
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
return false;
}
return getRoles() != null ? getRoles().equals(that.getRoles()) : that.getRoles() == null;
}
@Override
public int hashCode()
{
int result = getName() != null ? getName().hashCode() : 0;
result = 31 * result + (getRoles() != null ? getRoles().hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,81 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HashSet;
import java.util.Set;
public class BasicAuthorizerUserFull
{
private final String name;
private final Set<BasicAuthorizerRole> roles;
@JsonCreator
public BasicAuthorizerUserFull(
@JsonProperty("name") String name,
@JsonProperty("roles") Set<BasicAuthorizerRole> roles
)
{
this.name = name;
this.roles = roles == null ? new HashSet<>() : roles;
}
@JsonProperty
public String getName()
{
return name;
}
@JsonProperty
public Set<BasicAuthorizerRole> getRoles()
{
return roles;
}
@Override
public boolean equals(Object o)
{
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
BasicAuthorizerUserFull that = (BasicAuthorizerUserFull) o;
if (getName() != null ? !getName().equals(that.getName()) : that.getName() != null) {
return false;
}
return getRoles() != null ? getRoles().equals(that.getRoles()) : that.getRoles() == null;
}
@Override
public int hashCode()
{
int result = getName() != null ? getName().hashCode() : 0;
result = 31 * result + (getRoles() != null ? getRoles().hashCode() : 0);
return result;
}
}

View File

@ -0,0 +1,53 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
public class BasicAuthorizerUserMapBundle
{
private final Map<String, BasicAuthorizerUser> userMap;
private final byte[] serializedUserMap;
@JsonCreator
public BasicAuthorizerUserMapBundle(
@JsonProperty("userMap") Map<String, BasicAuthorizerUser> userMap,
@JsonProperty("serializedUserMap") byte[] serializedUserMap
)
{
this.userMap = userMap;
this.serializedUserMap = serializedUserMap;
}
@JsonProperty
public Map<String, BasicAuthorizerUser> getUserMap()
{
return userMap;
}
@JsonProperty
public byte[] getSerializedUserMap()
{
return serializedUserMap;
}
}

View File

@ -0,0 +1,56 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.basic.authorization.entity;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
public class UserAndRoleMap
{
@JsonProperty
private Map<String, BasicAuthorizerUser> userMap;
@JsonProperty
private Map<String, BasicAuthorizerRole> roleMap;
@JsonCreator
public UserAndRoleMap(
@JsonProperty("userMap") Map<String, BasicAuthorizerUser> userMap,
@JsonProperty("roleMap") Map<String, BasicAuthorizerRole> roleMap
)
{
this.userMap = userMap;
this.roleMap = roleMap;
}
@JsonProperty
public Map<String, BasicAuthorizerUser> getUserMap()
{
return userMap;
}
@JsonProperty
public Map<String, BasicAuthorizerRole> getRoleMap()
{
return roleMap;
}
}

View File

@ -0,0 +1 @@
io.druid.security.basic.BasicSecurityDruidModule

View File

@ -0,0 +1,39 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security;
import io.druid.security.basic.BasicAuthUtils;
import org.junit.Assert;
import org.junit.Test;
public class BasicAuthUtilsTest
{
@Test
public void testHashPassword()
{
char[] password = "HELLO".toCharArray();
int iterations = BasicAuthUtils.DEFAULT_KEY_ITERATIONS;
byte[] salt = BasicAuthUtils.generateSalt();
byte[] hash = BasicAuthUtils.hashPassword(password, salt, iterations);
Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length);
Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length);
}
}

View File

@ -0,0 +1,212 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import io.druid.guice.GuiceInjectors;
import io.druid.guice.JsonConfigProvider;
import io.druid.guice.annotations.Self;
import io.druid.initialization.Initialization;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.metadata.TestDerbyConnector;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.BasicHTTPEscalator;
import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.server.DruidNode;
import io.druid.server.security.AuthenticatorMapper;
import io.druid.server.security.Escalator;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.Map;
public class CoordinatorBasicAuthenticatorMetadataStorageUpdaterTest
{
private final static String AUTHENTICATOR_NAME = "test";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule();
private TestDerbyConnector connector;
private MetadataStorageTablesConfig tablesConfig;
private CoordinatorBasicAuthenticatorMetadataStorageUpdater updater;
private ObjectMapper objectMapper;
@Before
public void setUp() throws Exception
{
objectMapper = new ObjectMapper(new SmileFactory());
connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
updater = new CoordinatorBasicAuthenticatorMetadataStorageUpdater(
new AuthenticatorMapper(
ImmutableMap.of(
"test",
new BasicHTTPAuthenticator(
null,
"test",
"test",
null,
null,
null,
null,
null
)
)
),
connector,
tablesConfig,
new BasicAuthCommonCacheConfig(null, null, null, null),
objectMapper,
new NoopBasicAuthenticatorCacheNotifier(),
null
);
updater.start();
}
@After
public void tearDown() throws Exception
{
updater.stop();
}
@Test
public void createUser()
{
updater.createUser(AUTHENTICATOR_NAME, "druid");
Map<String, BasicAuthenticatorUser> expectedUserMap = ImmutableMap.of(
"druid", new BasicAuthenticatorUser("druid", null)
);
Map<String, BasicAuthenticatorUser> actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
// create duplicate should fail
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] already exists.");
updater.createUser(AUTHENTICATOR_NAME, "druid");
}
@Test
public void deleteUser()
{
updater.createUser(AUTHENTICATOR_NAME, "druid");
updater.deleteUser(AUTHENTICATOR_NAME, "druid");
Map<String, BasicAuthenticatorUser> expectedUserMap = ImmutableMap.of();
Map<String, BasicAuthenticatorUser> actualUserMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
// delete non-existent user should fail
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] does not exist.");
updater.deleteUser(AUTHENTICATOR_NAME, "druid");
}
@Test
public void setCredentials()
{
updater.createUser(AUTHENTICATOR_NAME, "druid");
updater.setUserCredentials(AUTHENTICATOR_NAME, "druid", new BasicAuthenticatorCredentialUpdate("helloworld", null));
Map<String, BasicAuthenticatorUser> userMap = BasicAuthUtils.deserializeAuthenticatorUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHENTICATOR_NAME)
);
BasicAuthenticatorCredentials credentials = userMap.get("druid").getCredentials();
byte[] recalculatedHash = BasicAuthUtils.hashPassword(
"helloworld".toCharArray(),
credentials.getSalt(),
credentials.getIterations()
);
Assert.assertArrayEquals(credentials.getHash(), recalculatedHash);
}
private Injector setupInjector()
{
return Initialization.makeInjectorWithModules(
GuiceInjectors.makeStartupInjector(),
ImmutableList.<Module>of(
new Module()
{
@Override
public void configure(Binder binder)
{
JsonConfigProvider.bindInstance(
binder,
Key.get(DruidNode.class, Self.class),
new DruidNode("test", "localhost", null, null, true, false)
);
binder.bind(Escalator.class).toInstance(
new BasicHTTPEscalator(null, null, null)
);
binder.bind(AuthenticatorMapper.class).toInstance(
new AuthenticatorMapper(
ImmutableMap.of(
"test",
new BasicHTTPAuthenticator(
null,
"test",
"test",
null,
null,
null,
null,
null
)
)
)
);
}
}
)
);
}
}

View File

@ -0,0 +1,323 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Binder;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import io.druid.guice.GuiceInjectors;
import io.druid.guice.JsonConfigProvider;
import io.druid.guice.annotations.Self;
import io.druid.initialization.Initialization;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.metadata.TestDerbyConnector;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.authentication.BasicHTTPAuthenticator;
import io.druid.security.basic.authentication.BasicHTTPEscalator;
import io.druid.security.basic.authentication.db.updater.CoordinatorBasicAuthenticatorMetadataStorageUpdater;
import io.druid.security.basic.authentication.endpoint.BasicAuthenticatorResource;
import io.druid.security.basic.authentication.endpoint.CoordinatorBasicAuthenticatorResourceHandler;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentials;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorUser;
import io.druid.server.DruidNode;
import io.druid.server.security.AuthenticatorMapper;
import io.druid.server.security.Escalator;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import java.util.Map;
import java.util.Set;
public class CoordinatorBasicAuthenticatorResourceTest
{
private final static String AUTHENTICATOR_NAME = "test";
private final static String AUTHENTICATOR_NAME2 = "test2";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule();
private TestDerbyConnector connector;
private MetadataStorageTablesConfig tablesConfig;
private BasicAuthenticatorResource resource;
private CoordinatorBasicAuthenticatorMetadataStorageUpdater storageUpdater;
private HttpServletRequest req;
@Before
public void setUp() throws Exception
{
req = EasyMock.createStrictMock(HttpServletRequest.class);
connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
ObjectMapper objectMapper = new ObjectMapper(new SmileFactory());
AuthenticatorMapper authenticatorMapper = new AuthenticatorMapper(
ImmutableMap.of(
AUTHENTICATOR_NAME,
new BasicHTTPAuthenticator(
null,
AUTHENTICATOR_NAME,
"test",
"druid",
"druid",
null,
null,
null
),
AUTHENTICATOR_NAME2,
new BasicHTTPAuthenticator(
null,
AUTHENTICATOR_NAME2,
"test",
"druid",
"druid",
null,
null,
null
)
)
);
storageUpdater = new CoordinatorBasicAuthenticatorMetadataStorageUpdater(
authenticatorMapper,
connector,
tablesConfig,
new BasicAuthCommonCacheConfig(null, null, null, null),
objectMapper,
new NoopBasicAuthenticatorCacheNotifier(),
null
);
resource = new BasicAuthenticatorResource(
new CoordinatorBasicAuthenticatorResourceHandler(
storageUpdater,
authenticatorMapper,
objectMapper
)
);
storageUpdater.start();
}
@After
public void tearDown() throws Exception
{
storageUpdater.stop();
}
@Test
public void testInvalidAuthenticator()
{
Response response = resource.getAllUsers(req, "invalidName");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(
errorMapWithMsg("Basic authenticator with name [invalidName] does not exist."),
response.getEntity()
);
}
@Test
public void testGetAllUsers()
{
Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), response.getEntity());
resource.createUser(req, AUTHENTICATOR_NAME, "druid");
resource.createUser(req, AUTHENTICATOR_NAME, "druid2");
resource.createUser(req, AUTHENTICATOR_NAME, "druid3");
Set<String> expectedUsers = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid",
"druid2",
"druid3"
);
response = resource.getAllUsers(req, AUTHENTICATOR_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers, response.getEntity());
}
@Test
public void testSeparateDatabaseTables()
{
Response response = resource.getAllUsers(req, AUTHENTICATOR_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME), response.getEntity());
resource.createUser(req, AUTHENTICATOR_NAME, "druid");
resource.createUser(req, AUTHENTICATOR_NAME, "druid2");
resource.createUser(req, AUTHENTICATOR_NAME, "druid3");
resource.createUser(req, AUTHENTICATOR_NAME2, "druid4");
resource.createUser(req, AUTHENTICATOR_NAME2, "druid5");
resource.createUser(req, AUTHENTICATOR_NAME2, "druid6");
Set<String> expectedUsers = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid",
"druid2",
"druid3"
);
Set<String> expectedUsers2 = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid4",
"druid5",
"druid6"
);
response = resource.getAllUsers(req, AUTHENTICATOR_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers, response.getEntity());
response = resource.getAllUsers(req, AUTHENTICATOR_NAME2);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers2, response.getEntity());
}
@Test
public void testCreateDeleteUser()
{
Response response = resource.createUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
BasicAuthenticatorUser expectedUser = new BasicAuthenticatorUser("druid", null);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
response = resource.getUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
}
@Test
public void testUserCredentials()
{
Response response = resource.createUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.updateUserCredentials(
req,
AUTHENTICATOR_NAME,
"druid",
new BasicAuthenticatorCredentialUpdate("helloworld", null)
);
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
BasicAuthenticatorUser actualUser = (BasicAuthenticatorUser) response.getEntity();
Assert.assertEquals("druid", actualUser.getName());
BasicAuthenticatorCredentials credentials = actualUser.getCredentials();
byte[] salt = credentials.getSalt();
byte[] hash = credentials.getHash();
int iterations = credentials.getIterations();
Assert.assertEquals(BasicAuthUtils.SALT_LENGTH, salt.length);
Assert.assertEquals(BasicAuthUtils.KEY_LENGTH / 8, hash.length);
Assert.assertEquals(BasicAuthUtils.DEFAULT_KEY_ITERATIONS, iterations);
byte[] recalculatedHash = BasicAuthUtils.hashPassword(
"helloworld".toCharArray(),
salt,
iterations
);
Assert.assertArrayEquals(recalculatedHash, hash);
response = resource.deleteUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHENTICATOR_NAME, "druid");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
response = resource.updateUserCredentials(
req,
AUTHENTICATOR_NAME,
"druid",
new BasicAuthenticatorCredentialUpdate("helloworld", null)
);
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
}
private static Map<String, String> errorMapWithMsg(String errorMsg)
{
return ImmutableMap.of("error", errorMsg);
}
private Injector setupInjector()
{
return Initialization.makeInjectorWithModules(
GuiceInjectors.makeStartupInjector(),
ImmutableList.<Module>of(
new Module()
{
@Override
public void configure(Binder binder)
{
JsonConfigProvider.bindInstance(
binder,
Key.get(DruidNode.class, Self.class),
new DruidNode("test", "localhost", null, null, true, false)
);
binder.bind(Escalator.class).toInstance(
new BasicHTTPEscalator(null, null, null)
);
}
}
)
);
}
}

View File

@ -0,0 +1,31 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authentication;
import io.druid.security.basic.authentication.db.cache.BasicAuthenticatorCacheNotifier;
public class NoopBasicAuthenticatorCacheNotifier implements BasicAuthenticatorCacheNotifier
{
@Override
public void addUpdate(String updatedAuthenticatorPrefix, byte[] updatedUserMap)
{
}
}

View File

@ -0,0 +1,135 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authorization;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.metadata.TestDerbyConnector;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.cache.MetadataStoragePollingBasicAuthorizerCacheManager;
import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater;
import io.druid.server.security.Access;
import io.druid.server.security.Action;
import io.druid.server.security.AuthenticationResult;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.List;
public class BasicRoleBasedAuthorizerTest
{
private static final String AUTHORIZER_NAME = "test";
@Rule
public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule();
private BasicRoleBasedAuthorizer authorizer;
private TestDerbyConnector connector;
private MetadataStorageTablesConfig tablesConfig;
private CoordinatorBasicAuthorizerMetadataStorageUpdater updater;
@Before
public void setUp() throws Exception
{
connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
updater = new CoordinatorBasicAuthorizerMetadataStorageUpdater(
new AuthorizerMapper(
ImmutableMap.of(
AUTHORIZER_NAME,
new BasicRoleBasedAuthorizer(
null,
AUTHORIZER_NAME,
null,
null
)
)
),
connector,
tablesConfig,
new BasicAuthCommonCacheConfig(null, null, null, null),
new ObjectMapper(new SmileFactory()),
new NoopBasicAuthorizerCacheNotifier(),
null
);
updater.start();
authorizer = new BasicRoleBasedAuthorizer(
new MetadataStoragePollingBasicAuthorizerCacheManager(
updater
),
AUTHORIZER_NAME,
null,
null
);
}
@After
public void tearDown() throws Exception
{
}
@Test
public void testAuth()
{
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druidRole");
updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole");
List<ResourceAction> permissions = Lists.newArrayList(
new ResourceAction(
new Resource("testResource", ResourceType.DATASOURCE),
Action.WRITE
)
);
updater.setPermissions(AUTHORIZER_NAME, "druidRole", permissions);
AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null);
Access access = authorizer.authorize(
authenticationResult,
new Resource("testResource", ResourceType.DATASOURCE),
Action.WRITE
);
Assert.assertTrue(access.isAllowed());
access = authorizer.authorize(
authenticationResult,
new Resource("wrongResource", ResourceType.DATASOURCE),
Action.WRITE
);
Assert.assertFalse(access.isAllowed());
}
}

View File

@ -0,0 +1,377 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authorization;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.metadata.TestDerbyConnector;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.BasicSecurityDBResourceException;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.server.security.Action;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import java.util.List;
import java.util.Map;
public class CoordinatorBasicAuthorizerMetadataStorageUpdaterTest
{
private final static String AUTHORIZER_NAME = "test";
private final static Map<String, BasicAuthorizerUser> BASE_USER_MAP = ImmutableMap.of(
BasicAuthUtils.ADMIN_NAME,
new BasicAuthorizerUser(BasicAuthUtils.ADMIN_NAME, ImmutableSet.of(BasicAuthUtils.ADMIN_NAME)),
BasicAuthUtils.INTERNAL_USER_NAME,
new BasicAuthorizerUser(BasicAuthUtils.INTERNAL_USER_NAME, ImmutableSet.of(
BasicAuthUtils.INTERNAL_USER_NAME))
);
private final static Map<String, BasicAuthorizerRole> BASE_ROLE_MAP = ImmutableMap.of(
BasicAuthUtils.ADMIN_NAME,
new BasicAuthorizerRole(
BasicAuthUtils.ADMIN_NAME,
BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS)
),
BasicAuthUtils.INTERNAL_USER_NAME,
new BasicAuthorizerRole(
BasicAuthUtils.INTERNAL_USER_NAME,
BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS)
)
);
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule();
private TestDerbyConnector connector;
private MetadataStorageTablesConfig tablesConfig;
private CoordinatorBasicAuthorizerMetadataStorageUpdater updater;
private ObjectMapper objectMapper;
@Before
public void setUp() throws Exception
{
objectMapper = new ObjectMapper(new SmileFactory());
connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
updater = new CoordinatorBasicAuthorizerMetadataStorageUpdater(
new AuthorizerMapper(
ImmutableMap.of(
AUTHORIZER_NAME,
new BasicRoleBasedAuthorizer(
null,
AUTHORIZER_NAME,
null,
null
)
)
),
connector,
tablesConfig,
new BasicAuthCommonCacheConfig(null, null, null, null),
objectMapper,
new NoopBasicAuthorizerCacheNotifier(),
null
);
updater.start();
}
// user tests
@Test
public void testCreateDeleteUser() throws Exception
{
updater.createUser(AUTHORIZER_NAME, "druid");
Map<String, BasicAuthorizerUser> expectedUserMap = Maps.newHashMap(BASE_USER_MAP);
expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of()));
Map<String, BasicAuthorizerUser> actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
updater.deleteUser(AUTHORIZER_NAME, "druid");
expectedUserMap.remove("druid");
actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
}
@Test
public void testDeleteNonExistentUser() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] does not exist.");
updater.deleteUser(AUTHORIZER_NAME, "druid");
}
@Test
public void testCreateDuplicateUser() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] already exists.");
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createUser(AUTHORIZER_NAME, "druid");
}
// role tests
@Test
public void testCreateDeleteRole() throws Exception
{
updater.createRole(AUTHORIZER_NAME, "druid");
Map<String, BasicAuthorizerRole> expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP);
expectedRoleMap.put("druid", new BasicAuthorizerRole("druid", ImmutableList.of()));
Map<String, BasicAuthorizerRole> actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
updater.deleteRole(AUTHORIZER_NAME, "druid");
expectedRoleMap.remove("druid");
actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
}
@Test
public void testDeleteNonExistentRole() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("Role [druid] does not exist.");
updater.deleteRole(AUTHORIZER_NAME, "druid");
}
@Test
public void testCreateDuplicateRole() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("Role [druid] already exists.");
updater.createRole(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druid");
}
// role and user tests
@Test
public void testAddAndRemoveRole() throws Exception
{
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druidRole");
updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole");
Map<String, BasicAuthorizerUser> expectedUserMap = Maps.newHashMap(BASE_USER_MAP);
expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole")));
Map<String, BasicAuthorizerRole> expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP);
expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of()));
Map<String, BasicAuthorizerUser> actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Map<String, BasicAuthorizerRole> actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
updater.unassignRole(AUTHORIZER_NAME, "druid", "druidRole");
expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of()));
actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
}
@Test
public void testAddRoleToNonExistentUser() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [nonUser] does not exist.");
updater.createRole(AUTHORIZER_NAME, "druid");
updater.assignRole(AUTHORIZER_NAME, "nonUser", "druid");
}
@Test
public void testAddNonexistentRoleToUser() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("Role [nonRole] does not exist.");
updater.createUser(AUTHORIZER_NAME, "druid");
updater.assignRole(AUTHORIZER_NAME, "druid", "nonRole");
}
@Test
public void testAddExistingRoleToUserFails() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] already has role [druidRole].");
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druidRole");
updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole");
updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole");
}
@Test
public void testUnassignInvalidRoleAssignmentFails() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("User [druid] does not have role [druidRole].");
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druidRole");
Map<String, BasicAuthorizerUser> expectedUserMap = Maps.newHashMap(BASE_USER_MAP);
expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of()));
Map<String, BasicAuthorizerRole> expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP);
expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of()));
Map<String, BasicAuthorizerUser> actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Map<String, BasicAuthorizerRole> actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
updater.unassignRole(AUTHORIZER_NAME, "druid", "druidRole");
}
// role and permission tests
@Test
public void testSetRolePermissions() throws Exception
{
updater.createUser(AUTHORIZER_NAME, "druid");
updater.createRole(AUTHORIZER_NAME, "druidRole");
updater.assignRole(AUTHORIZER_NAME, "druid", "druidRole");
List<ResourceAction> permsToAdd = ImmutableList.of(
new ResourceAction(
new Resource("testResource", ResourceType.DATASOURCE),
Action.WRITE
)
);
updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd);
Map<String, BasicAuthorizerUser> expectedUserMap = Maps.newHashMap(BASE_USER_MAP);
expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole")));
Map<String, BasicAuthorizerRole> expectedRoleMap = Maps.newHashMap(BASE_ROLE_MAP);
expectedRoleMap.put(
"druidRole",
new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(permsToAdd))
);
Map<String, BasicAuthorizerUser> actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap(
objectMapper,
updater.getCurrentUserMapBytes(AUTHORIZER_NAME)
);
Map<String, BasicAuthorizerRole> actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
updater.setPermissions(AUTHORIZER_NAME, "druidRole", null);
expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", null));
actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap(
objectMapper,
updater.getCurrentRoleMapBytes(AUTHORIZER_NAME)
);
Assert.assertEquals(expectedUserMap, actualUserMap);
Assert.assertEquals(expectedRoleMap, actualRoleMap);
}
@Test
public void testAddPermissionToNonExistentRole() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("Role [druidRole] does not exist.");
List<ResourceAction> permsToAdd = ImmutableList.of(
new ResourceAction(
new Resource("testResource", ResourceType.DATASOURCE),
Action.WRITE
)
);
updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd);
}
@Test
public void testAddBadPermission() throws Exception
{
expectedException.expect(BasicSecurityDBResourceException.class);
expectedException.expectMessage("Invalid permission, resource name regex[??????????] does not compile.");
updater.createRole(AUTHORIZER_NAME, "druidRole");
List<ResourceAction> permsToAdd = ImmutableList.of(
new ResourceAction(
new Resource("??????????", ResourceType.DATASOURCE),
Action.WRITE
)
);
updater.setPermissions(AUTHORIZER_NAME, "druidRole", permsToAdd);
}
}

View File

@ -0,0 +1,588 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authorization;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import io.druid.metadata.MetadataStorageTablesConfig;
import io.druid.metadata.TestDerbyConnector;
import io.druid.security.basic.BasicAuthCommonCacheConfig;
import io.druid.security.basic.BasicAuthUtils;
import io.druid.security.basic.authorization.BasicRoleBasedAuthorizer;
import io.druid.security.basic.authorization.db.updater.CoordinatorBasicAuthorizerMetadataStorageUpdater;
import io.druid.security.basic.authorization.endpoint.BasicAuthorizerResource;
import io.druid.security.basic.authorization.endpoint.CoordinatorBasicAuthorizerResourceHandler;
import io.druid.security.basic.authorization.entity.BasicAuthorizerPermission;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRole;
import io.druid.security.basic.authorization.entity.BasicAuthorizerRoleFull;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUser;
import io.druid.security.basic.authorization.entity.BasicAuthorizerUserFull;
import io.druid.server.security.Action;
import io.druid.server.security.AuthorizerMapper;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CoordinatorBasicAuthorizerResourceTest
{
private final static String AUTHORIZER_NAME = "test";
private final static String AUTHORIZER_NAME2 = "test2";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public final TestDerbyConnector.DerbyConnectorRule derbyConnectorRule = new TestDerbyConnector.DerbyConnectorRule();
private TestDerbyConnector connector;
private MetadataStorageTablesConfig tablesConfig;
private BasicAuthorizerResource resource;
private CoordinatorBasicAuthorizerMetadataStorageUpdater storageUpdater;
private HttpServletRequest req;
@Before
public void setUp() throws Exception
{
req = EasyMock.createStrictMock(HttpServletRequest.class);
connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
AuthorizerMapper authorizerMapper = new AuthorizerMapper(
ImmutableMap.of(
AUTHORIZER_NAME,
new BasicRoleBasedAuthorizer(
null,
AUTHORIZER_NAME,
null,
null
),
AUTHORIZER_NAME2,
new BasicRoleBasedAuthorizer(
null,
AUTHORIZER_NAME2,
null,
null
)
)
);
storageUpdater = new CoordinatorBasicAuthorizerMetadataStorageUpdater(
authorizerMapper,
connector,
tablesConfig,
new BasicAuthCommonCacheConfig(null, null, null, null),
new ObjectMapper(new SmileFactory()),
new NoopBasicAuthorizerCacheNotifier(),
null
);
resource = new BasicAuthorizerResource(
new CoordinatorBasicAuthorizerResourceHandler(
storageUpdater,
authorizerMapper,
new ObjectMapper(new SmileFactory())
)
);
storageUpdater.start();
}
@After
public void tearDown() throws Exception
{
storageUpdater.stop();
}
@Test
public void testSeparateDatabaseTables()
{
Response response = resource.getAllUsers(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(
ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME),
response.getEntity()
);
resource.createUser(req, AUTHORIZER_NAME, "druid");
resource.createUser(req, AUTHORIZER_NAME, "druid2");
resource.createUser(req, AUTHORIZER_NAME, "druid3");
resource.createUser(req, AUTHORIZER_NAME2, "druid4");
resource.createUser(req, AUTHORIZER_NAME2, "druid5");
resource.createUser(req, AUTHORIZER_NAME2, "druid6");
Set<String> expectedUsers = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid",
"druid2",
"druid3"
);
Set<String> expectedUsers2 = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid4",
"druid5",
"druid6"
);
response = resource.getAllUsers(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers, response.getEntity());
response = resource.getAllUsers(req, AUTHORIZER_NAME2);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers2, response.getEntity());
}
@Test
public void testInvalidAuthorizer()
{
Response response = resource.getAllUsers(req, "invalidName");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(
errorMapWithMsg("Basic authorizer with name [invalidName] does not exist."),
response.getEntity()
);
}
@Test
public void testGetAllUsers()
{
Response response = resource.getAllUsers(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(
ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME),
response.getEntity()
);
resource.createUser(req, AUTHORIZER_NAME, "druid");
resource.createUser(req, AUTHORIZER_NAME, "druid2");
resource.createUser(req, AUTHORIZER_NAME, "druid3");
Set<String> expectedUsers = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid",
"druid2",
"druid3"
);
response = resource.getAllUsers(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUsers, response.getEntity());
}
@Test
public void testGetAllRoles()
{
Response response = resource.getAllRoles(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(
ImmutableSet.of(BasicAuthUtils.ADMIN_NAME, BasicAuthUtils.INTERNAL_USER_NAME),
response.getEntity()
);
resource.createRole(req, AUTHORIZER_NAME, "druid");
resource.createRole(req, AUTHORIZER_NAME, "druid2");
resource.createRole(req, AUTHORIZER_NAME, "druid3");
Set<String> expectedRoles = ImmutableSet.of(
BasicAuthUtils.ADMIN_NAME,
BasicAuthUtils.INTERNAL_USER_NAME,
"druid",
"druid2",
"druid3"
);
response = resource.getAllRoles(req, AUTHORIZER_NAME);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRoles, response.getEntity());
}
@Test
public void testCreateDeleteUser()
{
Response response = resource.createUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerUser expectedUser = new BasicAuthorizerUser(
"druid",
ImmutableSet.of()
);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.deleteUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.deleteUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("User [druid] does not exist."), response.getEntity());
}
@Test
public void testCreateDeleteRole()
{
Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of());
Assert.assertEquals(expectedRole, response.getEntity());
response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("Role [druidRole] does not exist."), response.getEntity());
}
@Test
public void testRoleAssignment() throws Exception
{
Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.createUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerUser expectedUser = new BasicAuthorizerUser(
"druid",
ImmutableSet.of("druidRole")
);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of());
Assert.assertEquals(expectedRole, response.getEntity());
response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(200, response.getStatus());
expectedUser = new BasicAuthorizerUser(
"druid",
ImmutableSet.of()
);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRole, response.getEntity());
}
@Test
public void testDeleteAssignedRole()
{
Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.createUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.createUser(req, AUTHORIZER_NAME, "druid2");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerUser expectedUser = new BasicAuthorizerUser(
"druid",
ImmutableSet.of("druidRole")
);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.getUser(req, AUTHORIZER_NAME, "druid2", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerUser expectedUser2 = new BasicAuthorizerUser(
"druid2",
ImmutableSet.of("druidRole")
);
Assert.assertEquals(expectedUser2, response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", ImmutableList.of());
Assert.assertEquals(expectedRole, response.getEntity());
response = resource.deleteRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.getUser(req, AUTHORIZER_NAME, "druid", null);
Assert.assertEquals(200, response.getStatus());
expectedUser = new BasicAuthorizerUser(
"druid",
ImmutableSet.of()
);
Assert.assertEquals(expectedUser, response.getEntity());
response = resource.getUser(req, AUTHORIZER_NAME, "druid2", null);
Assert.assertEquals(200, response.getStatus());
expectedUser2 = new BasicAuthorizerUser(
"druid2",
ImmutableSet.of()
);
Assert.assertEquals(expectedUser2, response.getEntity());
}
@Test
public void testRolesAndPerms()
{
Response response = resource.createRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
List<ResourceAction> perms = ImmutableList.of(
new ResourceAction(new Resource("A", ResourceType.DATASOURCE), Action.READ),
new ResourceAction(new Resource("B", ResourceType.DATASOURCE), Action.WRITE),
new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE)
);
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms);
Assert.assertEquals(200, response.getStatus());
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "wrongRole", perms);
Assert.assertEquals(400, response.getStatus());
Assert.assertEquals(errorMapWithMsg("Role [wrongRole] does not exist."), response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms));
Assert.assertEquals(expectedRole, response.getEntity());
List<ResourceAction> newPerms = ImmutableList.of(
new ResourceAction(new Resource("D", ResourceType.DATASOURCE), Action.READ),
new ResourceAction(new Resource("B", ResourceType.DATASOURCE), Action.WRITE),
new ResourceAction(new Resource("F", ResourceType.CONFIG), Action.WRITE)
);
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", newPerms);
Assert.assertEquals(200, response.getStatus());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(newPerms));
Assert.assertEquals(expectedRole, response.getEntity());
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", null);
Assert.assertEquals(200, response.getStatus());
expectedRole = new BasicAuthorizerRole("druidRole", null);
Assert.assertEquals(expectedRole, response.getEntity());
}
@Test
public void testUsersRolesAndPerms()
{
Response response = resource.createUser(req, AUTHORIZER_NAME, "druid");
Assert.assertEquals(200, response.getStatus());
response = resource.createUser(req, AUTHORIZER_NAME, "druid2");
Assert.assertEquals(200, response.getStatus());
response = resource.createRole(req, AUTHORIZER_NAME, "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.createRole(req, AUTHORIZER_NAME, "druidRole2");
Assert.assertEquals(200, response.getStatus());
List<ResourceAction> 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<ResourceAction> perms2 = ImmutableList.of(
new ResourceAction(new Resource("D", ResourceType.STATE), Action.READ),
new ResourceAction(new Resource("E", ResourceType.DATASOURCE), Action.WRITE),
new ResourceAction(new Resource("F", ResourceType.CONFIG), Action.WRITE)
);
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms);
Assert.assertEquals(200, response.getStatus());
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole2", perms2);
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid", "druidRole2");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.assignRoleToUser(req, AUTHORIZER_NAME, "druid2", "druidRole2");
Assert.assertEquals(200, response.getStatus());
BasicAuthorizerRole expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms));
BasicAuthorizerRole expectedRole2 = new BasicAuthorizerRole("druidRole2", BasicAuthorizerPermission.makePermissionList(perms2));
Set<BasicAuthorizerRole> expectedRoles = Sets.newHashSet(expectedRole, expectedRole2);
BasicAuthorizerUserFull expectedUserFull = new BasicAuthorizerUserFull("druid", expectedRoles);
response = resource.getUser(req, AUTHORIZER_NAME, "druid", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull, response.getEntity());
BasicAuthorizerUserFull expectedUserFull2 = new BasicAuthorizerUserFull("druid2", expectedRoles);
response = resource.getUser(req, AUTHORIZER_NAME, "druid2", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull2, response.getEntity());
Set<String> expectedUserSet = Sets.newHashSet("druid", "druid2");
BasicAuthorizerRoleFull expectedRoleFull = new BasicAuthorizerRoleFull(
"druidRole",
expectedUserSet,
BasicAuthorizerPermission.makePermissionList(perms)
);
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRoleFull, response.getEntity());
BasicAuthorizerRoleFull expectedRoleFull2 = new BasicAuthorizerRoleFull(
"druidRole2",
expectedUserSet,
BasicAuthorizerPermission.makePermissionList(perms2)
);
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole2", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRoleFull2, response.getEntity());
perms = ImmutableList.of(
new ResourceAction(new Resource("A", ResourceType.DATASOURCE), Action.READ),
new ResourceAction(new Resource("C", ResourceType.CONFIG), Action.WRITE)
);
perms2 = ImmutableList.of(
new ResourceAction(new Resource("E", ResourceType.DATASOURCE), Action.WRITE)
);
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole", perms);
Assert.assertEquals(200, response.getStatus());
response = resource.setRolePermissions(req, AUTHORIZER_NAME, "druidRole2", perms2);
Assert.assertEquals(200, response.getStatus());
expectedRole = new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(perms));
expectedRole2 = new BasicAuthorizerRole("druidRole2", BasicAuthorizerPermission.makePermissionList(perms2));
expectedRoles = Sets.newHashSet(expectedRole, expectedRole2);
expectedUserFull = new BasicAuthorizerUserFull("druid", expectedRoles);
expectedUserFull2 = new BasicAuthorizerUserFull("druid2", expectedRoles);
response = resource.getUser(req, AUTHORIZER_NAME, "druid", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull, response.getEntity());
response = resource.getUser(req, AUTHORIZER_NAME, "druid2", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull2, response.getEntity());
response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid", "druidRole");
Assert.assertEquals(200, response.getStatus());
response = resource.unassignRoleFromUser(req, AUTHORIZER_NAME, "druid2", "druidRole2");
Assert.assertEquals(200, response.getStatus());
expectedUserFull = new BasicAuthorizerUserFull("druid", Sets.newHashSet(expectedRole2));
expectedUserFull2 = new BasicAuthorizerUserFull("druid2", Sets.newHashSet(expectedRole));
expectedRoleFull = new BasicAuthorizerRoleFull(
"druidRole",
Sets.newHashSet("druid2"),
BasicAuthorizerPermission.makePermissionList(perms)
);
expectedRoleFull2 = new BasicAuthorizerRoleFull(
"druidRole2",
Sets.newHashSet("druid"),
BasicAuthorizerPermission.makePermissionList(perms2)
);
response = resource.getUser(req, AUTHORIZER_NAME, "druid", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull, response.getEntity());
response = resource.getUser(req, AUTHORIZER_NAME, "druid2", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedUserFull2, response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRoleFull, response.getEntity());
response = resource.getRole(req, AUTHORIZER_NAME, "druidRole2", "");
Assert.assertEquals(200, response.getStatus());
Assert.assertEquals(expectedRoleFull2, response.getEntity());
}
private static Map<String, String> errorMapWithMsg(String errorMsg)
{
return ImmutableMap.of("error", errorMsg);
}
}

View File

@ -0,0 +1,31 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.security.authorization;
import io.druid.security.basic.authorization.db.cache.BasicAuthorizerCacheNotifier;
public class NoopBasicAuthorizerCacheNotifier implements BasicAuthorizerCacheNotifier
{
@Override
public void addUpdate(String authorizerPrefix, byte[] userAndRoleMap)
{
}
}

View File

@ -41,5 +41,4 @@ public interface PollingCache<K, V>
* close and clean the resources used by the cache
*/
void close();
}

View File

@ -21,6 +21,19 @@ command=java
-Ddruid.broker.cache.populateCache=true
-Ddruid.cache.type=local
-Ddruid.cache.sizeInBytes=40000000
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/broker
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server broker
redirect_stderr=true

View File

@ -15,6 +15,19 @@ command=java
-Ddruid.metadata.storage.connector.password=diurd
-Ddruid.zk.service.host=druid-zookeeper-kafka
-Ddruid.coordinator.startDelay=PT5S
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/coordinator
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server coordinator
redirect_stderr=true

View File

@ -19,6 +19,19 @@ command=java
-Ddruid.server.http.numThreads=100
-Ddruid.segmentCache.locations="[{\"path\":\"/shared/druid/indexCache\",\"maxSize\":5000000000}]"
-Ddruid.server.maxSize=5000000000
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/historical
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server historical
redirect_stderr=true

View File

@ -22,6 +22,19 @@ command=java
-Ddruid.worker.ip=%(ENV_HOST_IP)s
-Ddruid.selectors.indexing.serviceName=druid/overlord
-Ddruid.indexer.task.chathandler.type=announce
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/middleManager
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server middleManager
redirect_stderr=true

View File

@ -17,6 +17,19 @@ command=java
-Ddruid.indexer.storage.type=metadata
-Ddruid.indexer.logs.directory=/shared/tasklogs
-Ddruid.indexer.runner.type=remote
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/overlord
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server overlord
redirect_stderr=true

View File

@ -10,6 +10,19 @@ command=java
-Ddruid.host=%(ENV_HOST_IP)s
-Ddruid.zk.service.host=druid-zookeeper-kafka
-Ddruid.server.http.numThreads=100
-Ddruid.lookup.numLookupLoadingThreads=1
-Ddruid.auth.authenticatorChain="[\"basic\"]"
-Ddruid.auth.authenticator.basic.type=basic
-Ddruid.auth.authenticator.basic.initialAdminPassword=priest
-Ddruid.auth.authenticator.basic.initialInternalClientPassword=warlock
-Ddruid.auth.authenticator.basic.authorizerName=basic
-Ddruid.auth.basic.common.cacheDirectory=/tmp/authCache/router
-Ddruid.escalator.type=basic
-Ddruid.escalator.internalClientUsername=druid_system
-Ddruid.escalator.internalClientPassword=warlock
-Ddruid.escalator.authorizerName=basic
-Ddruid.auth.authorizers="[\"basic\"]"
-Ddruid.auth.authorizer.basic.type=basic
-cp /shared/docker/lib/*
io.druid.cli.Main server router
redirect_stderr=true

View File

@ -81,6 +81,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.druid.extensions</groupId>
<artifactId>druid-basic-security</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>io.druid</groupId>
<artifactId>druid-services</artifactId>

View File

@ -40,7 +40,7 @@ docker run -d --privileged --name druid-overlord -p 8090:8090 -v $SHARED_DIR:/sh
docker run -d --privileged --name druid-coordinator -p 8081:8081 -v $SHARED_DIR:/shared -v $DOCKERDIR/coordinator.conf:$SUPERVISORDIR/coordinator.conf --link druid-overlord:druid-overlord --link druid-metadata-storage:druid-metadata-storage --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
# Start Historical
docker run -d --privileged --name druid-historical -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
docker run -d --privileged --name druid-historical -p 8083:8083 -v $SHARED_DIR:/shared -v $DOCKERDIR/historical.conf:$SUPERVISORDIR/historical.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka druid/cluster
# Start Middlemanger
docker run -d --privileged --name druid-middlemanager -p 8091:8091 -p 8100:8100 -p 8101:8101 -p 8102:8102 -p 8103:8103 -p 8104:8104 -p 8105:8105 -v $RESOURCEDIR:/resources -v $SHARED_DIR:/shared -v $DOCKERDIR/middlemanager.conf:$SUPERVISORDIR/middlemanager.conf --link druid-zookeeper-kafka:druid-zookeeper-kafka --link druid-overlord:druid-overlord druid/cluster

View File

@ -138,6 +138,12 @@ public class ConfigFileConfigProvider implements IntegrationTestingConfigProvide
return brokerUrl;
}
@Override
public String getHistoricalUrl()
{
return historicalUrl;
}
@Override
public String getMiddleManagerHost()
{

View File

@ -64,6 +64,12 @@ public class DockerConfigProvider implements IntegrationTestingConfigProvider
return "http://" + dockerIp + ":8082";
}
@Override
public String getHistoricalUrl()
{
return "http://" + dockerIp + ":8083";
}
@Override
public String getMiddleManagerHost()
{

View File

@ -31,6 +31,8 @@ public interface IntegrationTestingConfig
String getBrokerUrl();
String getHistoricalUrl();
String getMiddleManagerHost();
String getZookeeperHosts();

View File

@ -72,7 +72,7 @@ public class DruidTestModule implements Module
if (config.getUsername() != null) {
return new CredentialedHttpClient(new BasicCredentials(config.getUsername(), config.getPassword()), delegate);
} else {
return delegate;
return new CredentialedHttpClient(new BasicCredentials("admin", "priest"), delegate);
}
}

View File

@ -122,7 +122,7 @@ public class DruidTestRunnerFactory implements ITestRunnerFactory
() -> {
try {
StatusResponseHolder response = client.go(
new Request(HttpMethod.GET, new URL(StringUtils.format("%s/status", host))),
new Request(HttpMethod.GET, new URL(StringUtils.format("%s/status/health", host))),
handler
).get();

View File

@ -0,0 +1,290 @@
/*
* Licensed to Metamarkets Group Inc. (Metamarkets) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Metamarkets licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package io.druid.tests.security;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.base.Throwables;
import com.google.inject.Inject;
import com.metamx.http.client.CredentialedHttpClient;
import com.metamx.http.client.HttpClient;
import com.metamx.http.client.Request;
import com.metamx.http.client.auth.BasicCredentials;
import com.metamx.http.client.response.StatusResponseHandler;
import com.metamx.http.client.response.StatusResponseHolder;
import io.druid.guice.annotations.Client;
import io.druid.java.util.common.ISE;
import io.druid.java.util.common.StringUtils;
import io.druid.java.util.common.logger.Logger;
import io.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate;
import io.druid.server.security.Action;
import io.druid.server.security.Resource;
import io.druid.server.security.ResourceAction;
import io.druid.server.security.ResourceType;
import io.druid.testing.IntegrationTestingConfig;
import io.druid.testing.guice.DruidTestModuleFactory;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.testng.Assert;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import javax.ws.rs.core.MediaType;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Guice(moduleFactory = DruidTestModuleFactory.class)
public class ITBasicAuthConfigurationTest
{
private static final Logger LOG = new Logger(ITBasicAuthConfigurationTest.class);
private static final TypeReference LOAD_STATUS_TYPE_REFERENCE =
new TypeReference<Map<String, Boolean>>()
{
};
@Inject
IntegrationTestingConfig config;
@Inject
ObjectMapper jsonMapper;
@Inject
@Client
HttpClient httpClient;
StatusResponseHandler responseHandler = new StatusResponseHandler(Charsets.UTF_8);
@Test
public void testAuthConfiguration() throws Exception
{
HttpClient adminClient = new CredentialedHttpClient(
new BasicCredentials("admin", "priest"),
httpClient
);
HttpClient internalSystemClient = new CredentialedHttpClient(
new BasicCredentials("druid_system", "warlock"),
httpClient
);
HttpClient newUserClient = new CredentialedHttpClient(
new BasicCredentials("druid", "helloworld"),
httpClient
);
// check that admin works
checkNodeAccess(adminClient);
// check that internal user works
checkNodeAccess(internalSystemClient);
// create a new user that can read /status
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid",
null
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid/credentials",
jsonMapper.writeValueAsBytes(new BasicAuthenticatorCredentialUpdate("helloworld", 5000))
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid",
null
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/roles/druidrole",
null
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid/roles/druidrole",
null
);
List<ResourceAction> permissions = Arrays.asList(
new ResourceAction(
new Resource(".*", ResourceType.STATE),
Action.READ
)
);
byte[] permissionsBytes = jsonMapper.writeValueAsBytes(permissions);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/roles/druidrole/permissions",
permissionsBytes
);
// check that the new user works
checkNodeAccess(newUserClient);
// check loadStatus
checkLoadStatus(adminClient);
// create 100 users
for (int i = 0; i < 100; i++) {
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid" + i,
null
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid" + i,
null
);
LOG.info("Finished creating user druid" + i);
}
// setup the last of 100 users and check that it works
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authentication/db/basic/users/druid99/credentials",
jsonMapper.writeValueAsBytes(new BasicAuthenticatorCredentialUpdate("helloworld", 5000))
);
makeRequest(
adminClient,
HttpMethod.POST,
config.getCoordinatorUrl() + "/druid-ext/basic-security/authorization/db/basic/users/druid99/roles/druidrole",
null
);
HttpClient newUser99Client = new CredentialedHttpClient(
new BasicCredentials("druid99", "helloworld"),
httpClient
);
LOG.info("Checking access for user druid99.");
checkNodeAccess(newUser99Client);
}
private void checkNodeAccess(HttpClient httpClient)
{
makeRequest(httpClient, HttpMethod.GET, config.getCoordinatorUrl() + "/status", null);
makeRequest(httpClient, HttpMethod.GET, config.getIndexerUrl() + "/status", null);
makeRequest(httpClient, HttpMethod.GET, config.getBrokerUrl() + "/status", null);
makeRequest(httpClient, HttpMethod.GET, config.getHistoricalUrl() + "/status", null);
makeRequest(httpClient, HttpMethod.GET, config.getRouterUrl() + "/status", null);
}
private void checkLoadStatus(HttpClient httpClient) throws Exception
{
checkLoadStatusSingle(httpClient, config.getCoordinatorUrl());
checkLoadStatusSingle(httpClient, config.getIndexerUrl());
checkLoadStatusSingle(httpClient, config.getBrokerUrl());
checkLoadStatusSingle(httpClient, config.getHistoricalUrl());
checkLoadStatusSingle(httpClient, config.getRouterUrl());
}
private void checkLoadStatusSingle(HttpClient httpClient, String baseUrl) throws Exception
{
StatusResponseHolder holder = makeRequest(
httpClient,
HttpMethod.GET,
baseUrl + "/druid-ext/basic-security/authentication/loadStatus",
null
);
String content = holder.getContent();
Map<String, Boolean> loadStatus = jsonMapper.readValue(content, LOAD_STATUS_TYPE_REFERENCE);
Assert.assertNotNull(loadStatus.get("basic"));
Assert.assertTrue(loadStatus.get("basic"));
holder = makeRequest(
httpClient,
HttpMethod.GET,
baseUrl + "/druid-ext/basic-security/authorization/loadStatus",
null
);
content = holder.getContent();
loadStatus = jsonMapper.readValue(content, LOAD_STATUS_TYPE_REFERENCE);
Assert.assertNotNull(loadStatus.get("basic"));
Assert.assertTrue(loadStatus.get("basic"));
}
private StatusResponseHolder makeRequest(HttpClient httpClient, HttpMethod method, String url, byte[] content)
{
try {
Request request = new Request(method, new URL(url));
if (content != null) {
request.setContent(MediaType.APPLICATION_JSON, content);
}
int retryCount = 0;
StatusResponseHolder response;
while (true) {
response = httpClient.go(
request,
responseHandler
).get();
if (!response.getStatus().equals(HttpResponseStatus.OK)) {
String errMsg = StringUtils.format(
"Error while making request to url[%s] status[%s] content[%s]",
url,
response.getStatus(),
response.getContent()
);
// it can take time for the auth config to propagate, so we retry
if (retryCount > 4) {
throw new ISE(errMsg);
} else {
LOG.error(errMsg);
LOG.error("retrying in 3000ms, retryCount: " + retryCount);
retryCount++;
Thread.sleep(3000);
}
} else {
break;
}
}
return response;
}
catch (Exception e) {
throw Throwables.propagate(e);
}
}
}

View File

@ -82,7 +82,6 @@
Need to update Druid to use Jackson 2.6+ -->
<aws.sdk.version>1.10.77</aws.sdk.version>
<caffeine.version>2.5.5</caffeine.version>
<!-- When upgrading ZK, edit docs and integration tests as well (integration-tests/docker-base/setup.sh) -->
<zookeeper.version>3.4.10</zookeeper.version>
</properties>
@ -121,6 +120,7 @@
<module>extensions-core/lookups-cached-single</module>
<module>extensions-core/s3-extensions</module>
<module>extensions-core/simple-client-sslcontext</module>
<module>extensions-core/druid-basic-security</module>
<!-- Community extensions -->
<module>extensions-contrib/azure-extensions</module>
<module>extensions-contrib/cassandra-storage</module>

View File

@ -26,6 +26,7 @@ import com.metamx.http.client.HttpClient;
import com.metamx.http.client.Request;
import com.metamx.http.client.response.FullResponseHandler;
import com.metamx.http.client.response.FullResponseHolder;
import com.metamx.http.client.response.HttpResponseHandler;
import io.druid.client.selector.Server;
import io.druid.concurrent.LifecycleLock;
import io.druid.curator.discovery.ServerDiscoverySelector;
@ -127,10 +128,18 @@ public class DruidLeaderClient
return new Request(httpMethod, new URL(StringUtils.format("%s%s", getCurrentKnownLeader(true), urlPath)));
}
public FullResponseHolder go(Request request) throws IOException, InterruptedException
{
return go(request, new FullResponseHandler(Charsets.UTF_8));
}
/**
* Executes a Request object aimed at the leader. Throws IOException if the leader cannot be located.
*/
public FullResponseHolder go(Request request) throws IOException, InterruptedException
public FullResponseHolder go(
Request request,
HttpResponseHandler<FullResponseHolder, FullResponseHolder> responseHandler
) throws IOException, InterruptedException
{
Preconditions.checkState(lifecycleLock.awaitStarted(1, TimeUnit.MILLISECONDS));
for (int counter = 0; counter < MAX_RETRIES; counter++) {
@ -139,7 +148,7 @@ public class DruidLeaderClient
try {
try {
fullResponseHolder = httpClient.go(request, new FullResponseHandler(Charsets.UTF_8)).get();
fullResponseHolder = httpClient.go(request, responseHandler).get();
}
catch (ExecutionException e) {
// Unwrap IOExceptions and ChannelExceptions, re-throw others

View File

@ -44,6 +44,8 @@ import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLRecoverableException;
import java.sql.SQLTransientException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
@ -446,6 +448,78 @@ public abstract class SQLMetadataConnector implements MetadataStorageConnector
);
}
@Override
public boolean compareAndSwap(
List<MetadataCASUpdate> updates
) throws Exception
{
return getDBI().inTransaction(
new TransactionCallback<Boolean>()
{
@Override
public Boolean inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception
{
List<byte[]> currentValues = new ArrayList<byte[]>();
// Compare
for (MetadataCASUpdate update : updates) {
byte[] currentValue = handle
.createQuery(
StringUtils.format(
"SELECT %1$s FROM %2$s WHERE %3$s = :key",
update.getValueColumn(),
update.getTableName(),
update.getKeyColumn()
)
)
.bind("key", update.getKey())
.map(ByteArrayMapper.FIRST)
.first();
if (!Arrays.equals(currentValue, update.getOldValue())) {
return false;
}
currentValues.add(currentValue);
}
// Swap
for (int i = 0; i < updates.size(); i++) {
MetadataCASUpdate update = updates.get(i);
byte[] currentValue = currentValues.get(i);
if (currentValue == null) {
handle.createStatement(
StringUtils.format(
"INSERT INTO %1$s (%2$s, %3$s) VALUES (:key, :value)",
update.getTableName(),
update.getKeyColumn(),
update.getValueColumn()
)
)
.bind("key", update.getKey())
.bind("value", update.getNewValue())
.execute();
} else {
handle.createStatement(
StringUtils.format(
"UPDATE %1$s SET %3$s=:value WHERE %2$s=:key",
update.getTableName(),
update.getKeyColumn(),
update.getValueColumn()
)
)
.bind("key", update.getKey())
.bind("value", update.getNewValue())
.execute();
}
}
return true;
}
}
);
}
public abstract DBI getDBI();
@Override

View File

@ -37,6 +37,11 @@ public class AuthenticatorMapper
this.authenticatorMap = authenticatorMap;
}
public Map<String, Authenticator> getAuthenticatorMap()
{
return authenticatorMap;
}
public List<Authenticator> getAuthenticatorChain()
{
return Lists.newArrayList(authenticatorMap.values());

View File

@ -39,4 +39,9 @@ public class AuthorizerMapper
{
return authorizerMap.get(name);
}
public Map<String, Authorizer> getAuthorizerMap()
{
return authorizerMap;
}
}

View File

@ -21,6 +21,7 @@ package io.druid.server.security;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.druid.java.util.common.StringUtils;
public class ResourceAction
{
@ -75,4 +76,10 @@ public class ResourceAction
result = 31 * result + getAction().hashCode();
return result;
}
@Override
public String toString()
{
return StringUtils.format("{%s,%s}", resource, action);
}
}

View File

@ -348,6 +348,8 @@ public class CliOverlord extends ServerRunnable
// Can't use /* here because of Guice and Jetty static content conflicts
root.addFilter(GuiceFilter.class, "/druid/*", null);
root.addFilter(GuiceFilter.class, "/druid-ext/*", null);
HandlerList handlerList = new HandlerList();
handlerList.setHandlers(
new Handler[]{

View File

@ -144,6 +144,8 @@ class CoordinatorJettyServerInitializer implements JettyServerInitializer
if (beOverlord) {
root.addFilter(GuiceFilter.class, "/druid/indexer/*", null);
}
root.addFilter(GuiceFilter.class, "/druid-ext/*", null);
// this will be removed in the next major release
root.addFilter(GuiceFilter.class, "/coordinator/*", null);

View File

@ -109,10 +109,10 @@ public class RouterJettyServerInitializer implements JettyServerInitializer
jsonMapper
);
// Can't use '/*' here because of Guice conflicts with AsyncQueryForwardingServlet path
root.addFilter(GuiceFilter.class, "/status/*", null);
root.addFilter(GuiceFilter.class, "/druid/router/*", null);
root.addFilter(GuiceFilter.class, "/druid-ext/*", null);
final HandlerList handlerList = new HandlerList();
handlerList.setHandlers(