com.clevercloud.biscuitpulsar.AuthorizationProviderBiscuit Maven / Gradle / Ivy
package com.clevercloud.biscuitpulsar;
import com.clevercloud.biscuit.error.Error;
import com.clevercloud.biscuit.token.Biscuit;
import com.clevercloud.biscuit.token.Verifier;
import com.clevercloud.biscuit.token.builder.Caveat;
import com.clevercloud.biscuit.token.builder.Fact;
import com.clevercloud.biscuit.token.builder.Predicate;
import io.vavr.control.Either;
import org.apache.pulsar.broker.ServiceConfiguration;
import org.apache.pulsar.broker.authentication.AuthenticationDataSource;
import org.apache.pulsar.broker.authorization.AuthorizationProvider;
import org.apache.pulsar.broker.authorization.PulsarAuthorizationProvider;
import org.apache.pulsar.broker.cache.ConfigurationCacheService;
import org.apache.pulsar.common.naming.NamespaceName;
import org.apache.pulsar.common.naming.TopicName;
import org.apache.pulsar.common.policies.data.AuthAction;
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.TenantOperation;
import org.apache.pulsar.common.policies.data.TopicOperation;
import org.apache.pulsar.common.util.FutureUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import static com.clevercloud.biscuit.token.builder.Utils.*;
import static io.vavr.API.Left;
import static io.vavr.API.Right;
public class AuthorizationProviderBiscuit implements AuthorizationProvider {
private static final Logger log = LoggerFactory.getLogger(AuthorizationProviderBiscuit.class);
public ServiceConfiguration conf;
public ConfigurationCacheService configCache;
private PulsarAuthorizationProvider defaultProvider;
public AuthorizationProviderBiscuit() {
}
public AuthorizationProviderBiscuit(ServiceConfiguration conf, ConfigurationCacheService configCache)
throws IOException {
initialize(conf, configCache);
}
private Fact topic(TopicName topicName) {
return fact("topic", Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName())));
}
private Fact subscription(TopicName topicName, String subscription) {
return fact("subscription", Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), string(subscription)));
}
private Predicate topicRight(TopicName topicName, String right) {
return pred("right", Arrays.asList(s("authority"), s("topic"),
string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s(right)));
}
private Fact namespace(NamespaceName namespaceName) {
return fact("namespace", Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName())));
}
private Predicate namespaceOperationRight(NamespaceName namespaceName, String right) {
return pred("right", Arrays.asList(s("authority"), s("namespace"),
string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s(right)));
}
private Predicate topicSubscriptionRight(TopicName topicName, String subscription, String right) {
return pred("right", Arrays.asList(s("authority"), s("topic"),
string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s(right), string(subscription)));
}
public Either verifierFromBiscuit(String role) {
Either deser = Biscuit.from_sealed(
Base64.getDecoder().decode(role.substring("biscuit:".length())),
AuthenticationProviderBiscuit.SEALING_KEY.getBytes()
);
if (deser.isLeft()) {
Error e = deser.getLeft();
log.error(e.toString());
return Left(e);
}
Biscuit token = deser.get();
Either res = token.verify_sealed();
if (res.isLeft()) {
return res;
}
Verifier verifier = res.get();
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("lookup")),
Arrays.asList(pred("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("produce"))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("lookup")),
Arrays.asList(pred("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume"))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("lookup")),
Arrays.asList(pred("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume"), var(3))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("produce")),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("namespace"), var(0), var(1), s("produce"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2)))
)));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume")),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("namespace"), var(0), var(1), s("consume"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume"), var(3)),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("namespace"), var(0), var(1), s("consume"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2))),
pred("subscription", Arrays.asList(s("ambient"), var(0), var(1), var(2), var(3))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("produce")),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("admin"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume")),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("admin"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2))))));
verifier.add_rule(rule("right", Arrays.asList(s("authority"), s("topic"), var(0), var(1), var(2), s("consume"), var(1)),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("admin"))),
pred("topic", Arrays.asList(s("ambient"), var(0), var(1), var(2))),
pred("subscription", Arrays.asList(s("ambient"), var(0), var(1), var(2), var(3)))
)));
//*check_right(#authority, #namespace, $0, $1, $2) <- !ns_operation(#authority, #namespace, $0, $1, $2), right(#authority, #namespace, $0, $1, $2) et `*check_right(#authority, #topic, $0, $1, $2, $3) <- !topic_operation(#authority, #topic, $0, $1, $2, $3), right(#authority, #namespace, $0, $1, $2, $3)
//log.debug(verifier.print_world());
return Right(verifier);
}
@Override
public void initialize(ServiceConfiguration conf, ConfigurationCacheService configCache) throws IOException {
this.conf = conf;
this.configCache = configCache;
defaultProvider = new PulsarAuthorizationProvider(conf, configCache);
}
@Override
public CompletableFuture canProduceAsync(TopicName topicName, String role, AuthenticationDataSource authenticationData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.canProduceAsync(topicName, role, authenticationData);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
log.error("could not create verifier {}", res.getLeft().toString());
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
//verifier.set_time();
verifier.add_fact(fact("topic_operation",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("produce"))));
verifier.add_fact(fact("topic",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()))));
verifier.add_caveat(new Caveat(Arrays.asList(
rule("check_right",
Arrays.asList(),
Arrays.asList(pred("right",
Arrays.asList(s("authority"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("produce"))))
))));
log.debug(verifier.print_world());
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("produce verifier failure: {}", verifierResult.getLeft());
} else {
log.debug("produce request authorized by biscuit token");
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture canConsumeAsync(TopicName topicName, String role, AuthenticationDataSource authenticationData, String subscription) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.canConsumeAsync(topicName, role, authenticationData, subscription);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
verifier.add_fact(fact("topic_operation",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("consume"))));
verifier.add_fact(fact("topic",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()))));
verifier.add_caveat(new Caveat(Arrays.asList(
rule("check_right",
Arrays.asList(),
Arrays.asList(pred("right",
Arrays.asList(s("authority"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("consume"))))
))));
// add these rules because there are two ways to verify that we can consume: with a right defined on the topic
// or one defined on the subscription
/*verifier.add_rule(rule("can_consume", Arrays.asList(s("authority"), s("topic"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName())),
Arrays.asList(
topicSubscriptionRight(topicName, subscription, "consume"))));
verifier.add_rule(rule("can_consume", Arrays.asList(s("authority"), s("topic"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName())),
Arrays.asList(
topicRight(topicName, "consume"))));
verifier.add_caveat(caveat(rule(
"checked_consume_right",
Arrays.asList(s("topic"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("consume")),
Arrays.asList(
pred("can_consume", Arrays.asList(s("authority"), s("topic"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName())))
)
)));*/
Either deser = Biscuit.from_sealed(
Base64.getDecoder().decode(role.substring("biscuit:".length())),
AuthenticationProviderBiscuit.SEALING_KEY.getBytes()
);
if (deser.isLeft()) {
Error e = deser.getLeft();
log.error(e.toString());
}
log.debug(verifier.print_world());
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("consume verifier failure: {}", verifierResult.getLeft());
} else {
log.debug("consume request authorized by biscuit token");
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture canLookupAsync(TopicName topicName, String role, AuthenticationDataSource authenticationData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.canLookupAsync(topicName, role, authenticationData);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
verifier.add_fact(fact("topic_operation",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("lookup"))));
verifier.add_fact(fact("topic",
Arrays.asList(s("ambient"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()))));
verifier.add_caveat(new Caveat(Arrays.asList(
rule("check_right",
Arrays.asList(),
Arrays.asList(pred("right",
Arrays.asList(s("authority"), string(topicName.getTenant()), string(topicName.getNamespacePortion()), string(topicName.getLocalName()), s("lookup"))))
))));
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("lookup verifier failure: {}", verifierResult.getLeft());
} else {
log.info("lookup authorized by biscuit token");
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture allowFunctionOpsAsync(NamespaceName namespaceName, String role, AuthenticationDataSource authenticationData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.allowFunctionOpsAsync(namespaceName, role, authenticationData);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
verifier.add_fact(fact("namespace", Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()))));
verifier.add_operation("functions");
verifier.set_time();
verifier.add_caveat(caveat(rule(
"checked_allowfunction_right",
Arrays.asList(string(namespaceName.getTenant()), string(namespaceName.getLocalName())),
Arrays.asList(
pred("right", Arrays.asList(s("authority"), s("namespace"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s("functions")))
)
)));
Either verifierResult = verifier.verify();
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture isSuperUser(String role, ServiceConfiguration serviceConfiguration) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.isSuperUser(role, serviceConfiguration);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
verifier.add_caveat(caveat(rule(
"checked_issuperuser_right",
Arrays.asList(s("admin")),
Arrays.asList(pred("right", Arrays.asList(s("authority"), s("admin")))
))));
//log.debug(verifier.print_world());
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("verifier failure: {}", verifierResult.getLeft());
} else {
log.debug("superuser authorized by biscuit token");
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture isSuperUser(String role, AuthenticationDataSource authenticationData, ServiceConfiguration serviceConfiguration) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.isSuperUser(role, serviceConfiguration);
}
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
verifier.add_caveat(caveat(rule(
"checked_issuperuser_right",
Arrays.asList(s("admin")),
Arrays.asList(pred("right", Arrays.asList(s("authority"), s("admin")))
))));
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("verifier failure: {}", verifierResult.getLeft());
} else {
log.debug("superuser authorized by biscuit token");
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture;
}
@Override
public CompletableFuture allowTenantOperationAsync(String tenantName, String originalRole, String role, TenantOperation operation, AuthenticationDataSource authData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.allowTenantOperationAsync(tenantName, originalRole, originalRole, operation, authData);
}
return isSuperUser(role, conf).thenCompose(isSuperUser -> {
if (isSuperUser) {
return CompletableFuture.completedFuture(true);
} else {
return FutureUtil.failedFuture(new IllegalStateException("allowTenantOperationAsync is not implemented for biscuit."));
}
});
}
@Override
public CompletableFuture allowNamespaceOperationAsync(NamespaceName namespaceName, String originalRole, String role, NamespaceOperation operation, AuthenticationDataSource authData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.allowNamespaceOperationAsync(namespaceName, originalRole, originalRole, operation, authData);
}
log.debug(String.format("allowNamespaceOperationAsync [%s] on [%s]...", operation.toString(), namespaceName.toString()));
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
Optional operationName = Stream.of(NamespaceOperation.values()).filter(e -> e == operation).findFirst();
if (operationName.isPresent()) {
// NamespaceOperation CREATE_TOPIC returns operation "create_topic"
verifier.add_fact(fact("namespace_operation",
Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s(operationName.get().toString().toLowerCase()))));
verifier.add_fact(fact("namespace",
Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()))));
verifier.add_caveat(new Caveat(Arrays.asList(
rule("check_right",
Arrays.asList(),
Arrays.asList(pred("right",
Arrays.asList(s("authority"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s(operationName.get().toString().toLowerCase()))))
))));
} else {
throw new IllegalStateException(String.format("allowNamespacePolicyOperationAsync [%s] is not implemented.", operation.toString()));
}
//log.info(verifier.print_world());
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("verifier failure: {}", verifierResult.getLeft());
} else {
log.debug(String.format("allowNamespaceOperationAsync [%s] on [%s] authorized", operation.toString(), namespaceName.toString()));
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture.thenCompose(isAuthorized -> {
if (isAuthorized) {
return CompletableFuture.completedFuture(true);
} else {
return isSuperUser(role, conf);
}
});
}
@Override
public CompletableFuture allowNamespacePolicyOperationAsync(NamespaceName namespaceName, PolicyName policy, PolicyOperation operation, String originalRole, String role, AuthenticationDataSource authData) {
if (!role.startsWith("biscuit:")) {
return defaultProvider.allowNamespacePolicyOperationAsync(namespaceName, policy, operation, originalRole, role, authData);
}
log.debug(String.format("allowNamespacePolicyOperationAsync [%s]:[%s] on [%s]...", policy.toString(), operation.toString(), namespaceName.toString()));
CompletableFuture permissionFuture = new CompletableFuture<>();
Either res = verifierFromBiscuit(role);
if (res.isLeft()) {
permissionFuture.complete(false);
return permissionFuture;
}
Verifier verifier = res.get();
Optional policyName = Stream.of(PolicyName.values()).filter(e -> e == policy).findFirst();
if (policyName.isPresent()) {
// PolicyName OFFLOAD, operation READ returns operation "offload_read"
verifier.add_fact(fact("namespace_operation",
Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s(policyName.get().toString().toLowerCase() + "_" + operation.toString().toLowerCase()))));
verifier.add_fact(fact("namespace",
Arrays.asList(s("ambient"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()))));
verifier.add_caveat(new Caveat(Arrays.asList(
rule("check_right",
Arrays.asList(),
Arrays.asList(pred("right",
Arrays.asList(s("authority"), string(namespaceName.getTenant()), string(namespaceName.getLocalName()), s(policyName.get().toString().toLowerCase() + "_" + operation.toString().toLowerCase()))))
))));
} else {
throw new IllegalStateException(String.format("allowNamespacePolicyOperationAsync [%s] is not implemented.", operation.toString()));
}
//log.info(verifier.print_world());
Either verifierResult = verifier.verify();
if (verifierResult.isLeft()) {
log.error("verifier failure: {}", verifierResult.getLeft());
} else {
log.debug(String.format("allowNamespacePolicyOperationAsync [%s]:[%s] on [%s] authorized.", policy.toString(), operation.toString(), namespaceName.toString()));
}
permissionFuture.complete(verifierResult.isRight());
return permissionFuture.thenCompose(isAuthorized -> {
if (isAuthorized) {
return CompletableFuture.completedFuture(true);
} else {
return isSuperUser(role, conf);
}
});
}
@Override
public CompletableFuture allowTopicOperationAsync(TopicName topicName, String originalRole, String role,
TopicOperation operation,
AuthenticationDataSource authData) {
CompletableFuture isAuthorizedFuture;
switch (operation) {
case LOOKUP:
isAuthorizedFuture = canLookupAsync(topicName, role, authData);
break;
case PRODUCE:
isAuthorizedFuture = canProduceAsync(topicName, role, authData);
break;
case CONSUME:
isAuthorizedFuture = canConsumeAsync(topicName, role, authData, authData.getSubscription());
break;
default:
isAuthorizedFuture = FutureUtil.failedFuture(
new IllegalStateException("TopicOperation is not supported."));
}
return isAuthorizedFuture.thenCompose(isAuthorized -> {
if (isAuthorized) {
return CompletableFuture.completedFuture(true);
} else {
return isSuperUser(role, conf);
}
});
}
// those management functions will be performed outside of the authorization provider
@Override
public CompletableFuture grantPermissionAsync(NamespaceName namespace, Set actions, String role, String authDataJson) {
return defaultProvider.grantPermissionAsync(namespace, actions, role, authDataJson);
}
@Override
public CompletableFuture grantSubscriptionPermissionAsync(NamespaceName namespace, String subscriptionName, Set roles, String authDataJson) {
return defaultProvider.grantSubscriptionPermissionAsync(namespace, subscriptionName, roles, authDataJson);
}
@Override
public CompletableFuture revokeSubscriptionPermissionAsync(NamespaceName namespace, String subscriptionName, String role, String authDataJson) {
return defaultProvider.revokeSubscriptionPermissionAsync(namespace, subscriptionName, role, authDataJson);
}
@Override
public CompletableFuture grantPermissionAsync(TopicName topicName, Set actions, String role, String authDataJson) {
return defaultProvider.grantPermissionAsync(topicName, actions, role, authDataJson);
}
@Override
public void close() throws IOException {
// noop
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy