All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.streamnative.pulsar.handlers.kop.security.auth.SimpleAclAuthorizer Maven / Gradle / Ivy

The newest version!
/**
 * 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()));
        }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy