io.streamnative.pulsar.handlers.kop.security.auth.SimpleAclAuthorizer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pulsar-protocol-handler-kafka Show documentation
Show all versions of pulsar-protocol-handler-kafka Show documentation
Kafka on Pulsar implemented using Pulsar Protocol Handler
/**
* Copyright (c) 2019 - 2024 StreamNative, Inc.. All Rights Reserved.
*/
/**
* 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.
*/
package io.streamnative.pulsar.handlers.kop.security.auth;
import com.github.benmanes.caffeine.cache.Cache;
import io.streamnative.pulsar.handlers.kop.KafkaServiceConfiguration;
import io.streamnative.pulsar.handlers.kop.security.KafkaPrincipal;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nullable;
import javax.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.pulsar.broker.PulsarService;
import org.apache.pulsar.broker.authentication.AuthenticationDataSource;
import org.apache.pulsar.broker.authorization.AuthorizationService;
import org.apache.pulsar.common.naming.NamespaceName;
import org.apache.pulsar.common.naming.TopicName;
import org.apache.pulsar.common.policies.data.NamespaceOperation;
import org.apache.pulsar.common.policies.data.PolicyName;
import org.apache.pulsar.common.policies.data.PolicyOperation;
import org.apache.pulsar.common.policies.data.TopicOperation;
import org.apache.pulsar.common.util.RestException;
/**
* Simple acl authorizer.
*/
@Slf4j
public class SimpleAclAuthorizer implements Authorizer {
private final PulsarService pulsarService;
private final AuthorizationService authorizationService;
private final boolean forceCheckGroupId;
private final String tenant;
// Cache the authorization results to avoid authorizing PRODUCE or FETCH requests each time.
// key is (topic, role)
private final Cache, Boolean> produceCache;
// key is (topic, role, group)
private final Cache, Boolean> fetchCache;
public SimpleAclAuthorizer(PulsarService pulsarService, KafkaServiceConfiguration config) {
this.pulsarService = pulsarService;
this.authorizationService = pulsarService.getBrokerService().getAuthorizationService();
this.forceCheckGroupId = config.isKafkaEnableAuthorizationForceGroupIdCheck();
this.tenant = config.getKafkaTenant();
this.produceCache = config.getAuthorizationCacheBuilder().build();
this.fetchCache = config.getAuthorizationCacheBuilder().build();
}
protected PulsarService getPulsarService() {
return this.pulsarService;
}
private CompletableFuture authorizeTenantPermission(KafkaPrincipal principal, Resource resource) {
CompletableFuture permissionFuture = new CompletableFuture<>();
// we can only check if the tenant exists
String tenant = resource.getName();
getPulsarService()
.getPulsarResources()
.getTenantResources()
.getTenantAsync(tenant)
.thenAccept(tenantInfo -> {
permissionFuture.complete(tenantInfo.isPresent());
}).exceptionally(ex -> {
if (log.isDebugEnabled()) {
log.debug("Client with Principal - {} failed to get permissions for resource - {}. {}",
principal, resource, ex.getMessage());
}
permissionFuture.completeExceptionally(ex);
return null;
});
return permissionFuture;
}
@Override
public CompletableFuture canAccessTenantAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TENANT);
CompletableFuture canAccessFuture = new CompletableFuture<>();
authorizeTenantPermission(principal, resource).whenComplete((hasPermission, ex) -> {
if (ex != null) {
if (log.isDebugEnabled()) {
log.debug(
"Resource [{}] Principal [{}] exception occurred while trying to "
+ "check Tenant permissions. {}",
resource, principal, ex.getMessage());
}
canAccessFuture.completeExceptionally(ex);
return;
}
canAccessFuture.complete(hasPermission);
});
return canAccessFuture;
}
@Override
public CompletableFuture canCreateTopicAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
return authorizationService.allowNamespaceOperationAsync(
topicName.getNamespaceObject(),
NamespaceOperation.CREATE_TOPIC,
principal.getName(),
principal.getAuthenticationData());
}
@Override
public CompletableFuture canDeleteTopicAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
return authorizationService.allowNamespaceOperationAsync(
topicName.getNamespaceObject(),
NamespaceOperation.DELETE_TOPIC,
principal.getName(),
principal.getAuthenticationData());
}
@Override
public CompletableFuture canAlterTopicAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
return authorizationService.allowTopicPolicyOperationAsync(
topicName, PolicyName.PARTITION, PolicyOperation.WRITE,
principal.getName(), principal.getAuthenticationData());
}
@Override
public CompletableFuture canManageTenantAsync(KafkaPrincipal principal, @Nullable Resource resource) {
final String tenant;
if (resource != null) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
tenant = topicName.getTenant();
} else if (principal.getTenantSpec() != null) {
String tenantSpec = principal.getTenantSpec();
if (tenantSpec.contains("/")) {
tenant = tenantSpec.substring(0, tenantSpec.indexOf('/'));
} else {
tenant = tenantSpec;
}
} else {
tenant = this.tenant;
}
String role = principal.getName();
AuthenticationDataSource authData = principal.getAuthenticationData();
return authorizationService.isSuperUser(role, authData)
.thenCompose(isSuperUserOrAdmin -> isSuperUserOrAdmin
? CompletableFuture.completedFuture(true)
: getPulsarService()
.getPulsarResources()
.getTenantResources()
.getTenantAsync(tenant)
.thenCompose(op -> {
if (op.isPresent()) {
return authorizationService.isTenantAdmin(tenant, role, op.get(), authData);
} else {
return CompletableFuture.failedFuture(new RestException(Response.Status.NOT_FOUND,
"Tenant does not exist"));
}
}));
}
@Override
public CompletableFuture canLookupAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
return authorizationService.allowTopicOperationAsync(
topicName, TopicOperation.LOOKUP, principal.getName(), principal.getAuthenticationData());
}
@Override
public CompletableFuture canGetTopicList(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.NAMESPACE);
return authorizationService.allowNamespaceOperationAsync(
NamespaceName.get(resource.getName()),
NamespaceOperation.GET_TOPICS,
principal.getName(),
principal.getAuthenticationData());
}
@Override
public CompletableFuture canProduceAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
final Pair key = Pair.of(topicName, principal.getName());
final Boolean authorized = produceCache.getIfPresent(key);
if (authorized != null) {
return CompletableFuture.completedFuture(authorized);
}
return authorizationService.allowTopicOperationAsync(
topicName, TopicOperation.PRODUCE, principal.getName(), principal.getAuthenticationData())
.thenApply(__ -> {
produceCache.put(key, __);
return __;
});
}
@Override
public CompletableFuture canConsumeAsync(KafkaPrincipal principal, Resource resource) {
checkResourceType(resource, ResourceType.TOPIC);
TopicName topicName = TopicName.get(resource.getName());
if (forceCheckGroupId && StringUtils.isBlank(principal.getGroupId())) {
return CompletableFuture.completedFuture(false);
}
final Triple key = Triple.of(topicName, principal.getName(), principal.getGroupId());
final Boolean authorized = fetchCache.getIfPresent(key);
if (authorized != null) {
return CompletableFuture.completedFuture(authorized);
}
return authorizationService.allowTopicOperationAsync(
topicName, TopicOperation.CONSUME, principal.getName(), principal.getAuthenticationData())
.thenApply(__ -> {
fetchCache.put(key, __);
return __;
});
}
@Override
public CompletableFuture canDescribeConsumerGroup(KafkaPrincipal principal, Resource resource) {
if (!forceCheckGroupId) {
return CompletableFuture.completedFuture(true);
}
if (StringUtils.isBlank(principal.getGroupId())) {
return CompletableFuture.completedFuture(false);
}
boolean isSameGroup = Objects.equals(principal.getGroupId(), resource.getName());
if (log.isDebugEnabled()) {
log.debug("Principal [{}] for resource [{}] isSameGroup [{}]", principal, resource, isSameGroup);
}
return CompletableFuture.completedFuture(isSameGroup);
}
private void checkResourceType(Resource actual, ResourceType expected) {
if (actual.getResourceType() != expected) {
throw new IllegalArgumentException(
String.format("Expected resource type is [%s], but have [%s]",
expected, actual.getResourceType()));
}
}
}