Skip to content

Commit 2ea8478

Browse files
committed
[java] KUDU-2972: Add Kudu Ranger plugin
This commit adds the Kudu Ranger plugin for retrieving authorization decision from the Ranger service. It also introduces the Ranger subprocess which utilizes the plugin and communicates the authz decision with Kudu master via protobuf message. Change-Id: I0c995ac1a48ebf57667231cd3a82d3794f6ddf8d Reviewed-on: http://gerrit.cloudera.org:8080/15074 Tested-by: Hao Hao <hao.hao@cloudera.com> Reviewed-by: Adar Dembo <adar@cloudera.com> Reviewed-by: Andrew Wong <awong@cloudera.com>
1 parent 148c169 commit 2ea8478

File tree

13 files changed

+728
-102
lines changed

13 files changed

+728
-102
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,7 @@ add_subdirectory(src/kudu/integration-tests)
14171417
add_subdirectory(src/kudu/kserver)
14181418
add_subdirectory(src/kudu/master)
14191419
add_subdirectory(src/kudu/mini-cluster)
1420+
add_subdirectory(src/kudu/ranger)
14201421
add_subdirectory(src/kudu/rebalance)
14211422
add_subdirectory(src/kudu/rpc)
14221423
add_subdirectory(src/kudu/security)

java/gradle/dependencies.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ versions += [
5151
osdetector : "1.6.2",
5252
parquet : "1.11.0",
5353
protobuf : "3.11.3",
54+
ranger : "2.0.0",
5455
scala : "2.11.12",
5556
scalatest : "3.0.8",
5657
scopt : "3.7.1",
@@ -106,6 +107,7 @@ libs += [
106107
protobufJava : "com.google.protobuf:protobuf-java:$versions.protobuf",
107108
protobufJavaUtil : "com.google.protobuf:protobuf-java-util:$versions.protobuf",
108109
protoc : "com.google.protobuf:protoc:$versions.protobuf",
110+
rangerPlugin : "org.apache.ranger:ranger-plugins-common:$versions.ranger",
109111
scalaLibrary : "org.scala-lang:scala-library:$versions.scala",
110112
scalap : "org.scala-lang:scalap:$versions.scala",
111113
scalatest : "org.scalatest:scalatest_$versions.scalaBase:$versions.scalatest",

java/kudu-subprocess/build.gradle

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,23 @@ apply from: "$rootDir/gradle/protobuf.gradle"
1919
apply from: "$rootDir/gradle/shadow.gradle"
2020

2121
dependencies {
22+
compile libs.hadoopCommon
2223
compile libs.protobufJava
2324
compile libs.protobufJavaUtil
24-
compile libs.hadoopCommon
25+
compile libs.rangerPlugin
2526
compile libs.slf4jApi
2627

28+
// Workaround for RANGER-2749. Remove once resolved.
29+
compile "commons-lang:commons-lang:2.6"
30+
31+
optional libs.jsr305
2732
optional libs.yetusAnnotations
2833

2934
testCompile project(path: ":kudu-test-utils", configuration: "shadow")
3035
testCompile libs.junit
3136
testCompile libs.log4j
3237
testCompile libs.log4jSlf4jImpl
38+
testCompile libs.mockitoCore
3339
}
3440

3541
// Add protobuf files to the proto source set.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.kudu.subprocess.ranger;
19+
20+
import org.apache.kudu.ranger.Ranger.RangerRequestListPB;
21+
import org.apache.kudu.ranger.Ranger.RangerResponseListPB;
22+
import org.apache.kudu.ranger.Ranger.RangerResponsePB;
23+
import org.apache.kudu.subprocess.ProtocolHandler;
24+
import org.apache.kudu.subprocess.ranger.authorization.RangerKuduAuthorizer;
25+
import org.apache.ranger.plugin.policyengine.RangerAccessResult;
26+
import org.apache.yetus.audience.InterfaceAudience;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
/**
31+
* Class that sends requests to Ranger and gets authorization decision
32+
* (e.g. allow or deny) as a response.
33+
*/
34+
@InterfaceAudience.Private
35+
class RangerProtocolHandler extends ProtocolHandler<RangerRequestListPB,
36+
RangerResponseListPB> {
37+
private static final Logger LOG = LoggerFactory.getLogger(RangerProtocolHandler.class);
38+
39+
// The Ranger Kudu authorizer plugin. This field is not final
40+
// as it is used in the mock test.
41+
@InterfaceAudience.LimitedPrivate("Test")
42+
static RangerKuduAuthorizer authz = new RangerKuduAuthorizer();
43+
44+
RangerProtocolHandler() {
45+
authz.init();
46+
}
47+
48+
@Override
49+
protected RangerResponseListPB executeRequest(RangerRequestListPB requests) {
50+
RangerResponseListPB.Builder responses = RangerResponseListPB.newBuilder();
51+
for (RangerAccessResult result : authz.authorize(requests)) {
52+
// The result can be null when Ranger plugin fails to load the policies
53+
// from the Ranger admin server.
54+
// TODO(Hao): add a test for the above case.
55+
boolean isAllowed = (result != null && result.getIsAllowed());
56+
RangerResponsePB.Builder response = RangerResponsePB.newBuilder();
57+
response.setAllowed(isAllowed);
58+
responses.addResponses(response);
59+
if (LOG.isDebugEnabled()) {
60+
LOG.debug(String.format("RangerAccessRequest [%s] receives result [%s]",
61+
result.getAccessRequest().toString(), result.toString()));
62+
}
63+
}
64+
return responses.build();
65+
}
66+
67+
@Override
68+
protected Class<RangerRequestListPB> getRequestClass() {
69+
return RangerRequestListPB.class;
70+
}
71+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.kudu.subprocess.ranger;
19+
20+
import org.apache.kudu.subprocess.SubprocessExecutor;
21+
import org.apache.yetus.audience.InterfaceAudience;
22+
23+
// The Ranger subprocess that wraps the Kudu Ranger plugin. For the
24+
// plugin to successfully connect to the Ranger service, configurations
25+
// such as ranger-kudu-security.xml (and ranger-kudu-policymgr-ssl.xml
26+
// for SSL connection) are required. To enable auditing in Ranger,
27+
// ranger-kudu-security.xml is needed. The plugin also requires
28+
// core-site.xml to use Hadoop UserGroupInformation for user group
29+
// resolution.
30+
@InterfaceAudience.Private
31+
class RangerSubprocessMain {
32+
33+
public static void main(String[] args) throws Exception {
34+
SubprocessExecutor subprocessExecutor = new SubprocessExecutor();
35+
RangerProtocolHandler protocolProcessor = new RangerProtocolHandler();
36+
subprocessExecutor.run(args, protocolProcessor, /* timeoutMs= */-1);
37+
}
38+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.apache.kudu.subprocess.ranger.authorization;
19+
20+
import com.google.common.annotations.VisibleForTesting;
21+
import com.google.common.base.Preconditions;
22+
import org.apache.hadoop.security.UserGroupInformation;
23+
import org.apache.kudu.ranger.Ranger.RangerRequestListPB;
24+
import org.apache.kudu.ranger.Ranger.RangerRequestPB;
25+
import org.apache.ranger.plugin.audit.RangerDefaultAuditHandler;
26+
import org.apache.ranger.plugin.model.RangerServiceDef;
27+
import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
28+
import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
29+
import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
30+
import org.apache.ranger.plugin.policyengine.RangerAccessResult;
31+
import org.apache.ranger.plugin.service.RangerBasePlugin;
32+
import org.apache.yetus.audience.InterfaceAudience;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
36+
import javax.annotation.Nullable;
37+
import java.util.ArrayList;
38+
import java.util.Collection;
39+
import java.util.HashSet;
40+
import java.util.List;
41+
import java.util.Set;
42+
43+
public class RangerKuduAuthorizer {
44+
private static final Logger LOG = LoggerFactory.getLogger(RangerKuduAuthorizer.class);
45+
// The following properties need to match the Kudu service def in Ranger
46+
// (https://github.com/apache/ranger/blob/master/agents-common/src/main/resources/service-defs/ranger-servicedef-kudu.json).
47+
private static final String APP_ID = "kudu";
48+
private static final String RANGER_DB_RESOURCE_NAME = "database";
49+
private static final String RANGER_TABLE_RESOURCE_NAME = "table";
50+
private static final String RANGER_COLUMN_RESOURCE_NAME = "column";
51+
private static final String SERVICE_TYPE = "kudu";
52+
53+
// The Ranger Kudu plugin. This field is not final as it is used in the
54+
// mock test.
55+
@InterfaceAudience.LimitedPrivate("Test")
56+
RangerBasePlugin plugin;
57+
58+
public RangerKuduAuthorizer() {
59+
plugin = new RangerBasePlugin(SERVICE_TYPE, APP_ID);
60+
plugin.setResultProcessor(new RangerDefaultAuditHandler());
61+
}
62+
63+
/**
64+
* Initializes the Ranger Kudu plugin, which has to be called explicitly
65+
* before doing any authorizations.
66+
*/
67+
public void init() {
68+
LOG.info("Initializing Ranger Kudu plugin");
69+
plugin.init();
70+
}
71+
72+
/**
73+
* Authorizes a given <code>RangerRequestListPB</code> in Ranger and returns
74+
* a list of <code>RangerAccessResult</code> which contains the authorization
75+
* decisions. Note that the order of results is determined by the order of
76+
* requests.
77+
*
78+
* @param requests a RangerRequestListPB
79+
* @return a list of RangerAccessResult
80+
*/
81+
@VisibleForTesting
82+
public Collection<RangerAccessResult> authorize(RangerRequestListPB requests) {
83+
Collection<RangerAccessRequest> rangerRequests = createRequests(requests);
84+
// Reject requests if user field is empty.
85+
if (!requests.hasUser() || requests.getUser().isEmpty()) {
86+
Collection<RangerAccessResult> results = new ArrayList<>();
87+
for (RangerAccessRequest request : rangerRequests) {
88+
// Create a 'dummy' RangerAccessResult that denies the request (to have
89+
// a short cut), instead of sending the request to Ranger.
90+
RangerAccessResult result = new RangerAccessResult(
91+
/* policyType= */1, APP_ID,
92+
new RangerServiceDef(), request);
93+
result.setIsAllowed(false);
94+
results.add(result);
95+
}
96+
return results;
97+
}
98+
return plugin.isAccessAllowed(rangerRequests);
99+
}
100+
101+
/**
102+
* Gets a list of authorization decision from Ranger with the specified list
103+
* of ranger access request.
104+
*
105+
* @param requests a list of RangerAccessRequest
106+
* @return a list of RangerAccessResult
107+
*/
108+
@VisibleForTesting
109+
Collection<RangerAccessResult> authorize(Collection<RangerAccessRequest> requests) {
110+
return plugin.isAccessAllowed(requests);
111+
}
112+
113+
/**
114+
* Creates a Ranger access request for the specified user who performs
115+
* the given action on the resource.
116+
*
117+
* @param action action to be authorized on the resource. Note that when it
118+
* is null, Ranger will match to any valid actions
119+
* @param user user who is performing the action
120+
* @param groups the set of groups the user belongs to
121+
* @param db the database name the action is to be performed on
122+
* @param table the table name the action is to be performed on
123+
* @param col the column name the action is to be performed on
124+
* @return the ranger access request
125+
*/
126+
private static RangerAccessRequestImpl createRequest(
127+
@Nullable String action, String user,
128+
@Nullable Set<String> groups, @Nullable String db,
129+
@Nullable String table, @Nullable String col) {
130+
final RangerAccessResourceImpl resource = new RangerAccessResourceImpl();
131+
resource.setValue(RANGER_DB_RESOURCE_NAME, db);
132+
resource.setValue(RANGER_TABLE_RESOURCE_NAME, table);
133+
resource.setValue(RANGER_COLUMN_RESOURCE_NAME, col);
134+
135+
final RangerAccessRequestImpl request = new RangerAccessRequestImpl();
136+
request.setResource(resource);
137+
request.setAccessType(action);
138+
// Add action as it is used for auditing in Ranger.
139+
request.setAction(action);
140+
request.setUser(user);
141+
request.setUserGroups(groups);
142+
return request;
143+
}
144+
145+
/**
146+
* Creates a list of <code>RangerAccessRequest</code> for the given
147+
* <code>RangerRequestListPB</code>.
148+
*
149+
* @param requests the given RangerRequestListPB
150+
* @return a list of RangerAccessRequest
151+
*/
152+
private static List<RangerAccessRequest> createRequests(RangerRequestListPB requests) {
153+
List<RangerAccessRequest> rangerRequests = new ArrayList<>();
154+
Preconditions.checkArgument(requests.hasUser());
155+
Preconditions.checkArgument(!requests.getUser().isEmpty());
156+
final String user = requests.getUser();
157+
Set<String> groups = getUserGroups(user);
158+
for (RangerRequestPB request : requests.getRequestsList()) {
159+
// Action should be lower case to match the Kudu service def in Ranger.
160+
String action = request.getAction().name().toLowerCase();
161+
String db = request.hasDatabase() ? request.getDatabase() : null;
162+
String table = request.hasTable() ? request.getTable() : null;
163+
String column = request.hasColumn() ? request.getColumn() : null;
164+
rangerRequests.add(createRequest(action, user, groups,
165+
db, table, column));
166+
}
167+
return rangerRequests;
168+
}
169+
170+
/**
171+
* Gets the user group mapping from Hadoop. The groups of a user is determined by a
172+
* group mapping service provider. See more detail at
173+
* https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-common/GroupsMapping.html.
174+
*
175+
* @param user the user name
176+
* @return the set of groups the user belongs to
177+
*/
178+
private static Set<String> getUserGroups(String user) {
179+
Preconditions.checkNotNull(user);
180+
Preconditions.checkArgument(!user.isEmpty());
181+
UserGroupInformation ugi;
182+
ugi = UserGroupInformation.createRemoteUser(user);
183+
return new HashSet<>(ugi.getGroups());
184+
}
185+
}

0 commit comments

Comments
 (0)