Skip to content

Commit

Permalink
✨ mongoAclAuthorizer can manage acl in multiple dbs
Browse files Browse the repository at this point in the history
  • Loading branch information
ujibang committed Nov 13, 2024
1 parent 3ff32bd commit d5b3d92
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 31 deletions.
5 changes: 3 additions & 2 deletions core/src/main/resources/restheart-default-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ fileRealmAuthenticator:
# see https://restheart.org/docs/security/authentication#mongo-realm-authenticator
mongoRealmAuthenticator:
enabled: true
users-db: restheart
override-users-db-header: null # eg. X-Auth-Db; when present override users-db from request header
users-db: restheart
users-collection: users
override-users-db-header: null # eg. X-Auth-Db; when present override users-db from request header
prop-id: _id
prop-password: password
json-path-roles: $.roles
Expand Down Expand Up @@ -160,6 +160,7 @@ mongoAclAuthorizer:
enabled: true
acl-db: restheart
acl-collection: acl
override-acl-db-header: null # eg. X-Auth-Db; when present override acl-db from request header
# clients with root-role can execute any request
root-role: admin
cache-enabled: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import org.restheart.cache.CacheFactory;
import org.restheart.cache.LoadingCache;
import org.restheart.configuration.ConfigurationException;
import org.restheart.exchange.MongoRequest;
import org.restheart.exchange.Request;
import static org.restheart.mongodb.ConnectionChecker.connected;
import org.restheart.plugins.Inject;
Expand Down Expand Up @@ -85,7 +84,7 @@ public class MongoRealmAuthenticator implements Authenticator {
private int minimumPasswordStrength = 3;
private BsonDocument createUserDocument = null;
private Cache.EXPIRE_POLICY cacheExpirePolicy = Cache.EXPIRE_POLICY.AFTER_WRITE;
protected record CacheKey(String id, String db) {};
private record CacheKey(String id, String db) {};
private LoadingCache<CacheKey, MongoRealmAccount> USERS_CACHE = null;
private static final transient Cache<CacheKey, String> USERS_PWDS_CACHE = CacheFactory.createLocalCache(1_000l, Cache.EXPIRE_POLICY.AFTER_READ, 20 * 60 * 1_000l);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,18 @@ public class MongoAclAuthorizer implements Authorizer {

public static final String ACL_COLLECTION_NAME = "acl";

public static final String $UNAUTHENTICATED = "$unauthenticated";
public static final String UNAUTHENTICATED = "$unauthenticated";

String aclDb;
String aclCollection;
String overrideAclDbHeader;
private String rootRole = null;
private boolean cacheEnabled = false;
private Integer cacheSize = 1_000; // 1000 entries
private Integer cacheTTL = 60 * 1_000; // 1 minute
private Cache.EXPIRE_POLICY cacheExpirePolicy = Cache.EXPIRE_POLICY.AFTER_WRITE;

private LoadingCache<String, LinkedHashSet<MongoAclPermission>> acl = null;
private record CacheKey(String role, String db) {};
private LoadingCache<CacheKey, LinkedHashSet<MongoAclPermission>> acl = null;

@Inject("mclient")
private MongoClient mclient;
Expand All @@ -95,9 +96,10 @@ public class MongoAclAuthorizer implements Authorizer {

@OnInit
public void init() {
this.aclDb = arg(config, "acl-db");
this.aclCollection = arg(config, "acl-collection");
this.rootRole = arg(config, "root-role");
this.aclDb = argOrDefault(config, "acl-db", "restheart");
this.aclCollection = argOrDefault(config, "acl-collection", "acl");
this.overrideAclDbHeader = argOrDefault(config, "override-acl-db-header", null);
this.rootRole = argOrDefault(config, "root-role", null);

if (config != null && config.containsKey("cache-enabled")) {
this.cacheEnabled = arg(config, "cache-enabled");
Expand Down Expand Up @@ -147,14 +149,9 @@ public boolean isAllowed(Request<?> request) {

if (this.rootRole != null
&& exchange.getSecurityContext() != null
&& exchange.getSecurityContext()
.getAuthenticatedAccount() != null
&& exchange.getSecurityContext()
.getAuthenticatedAccount()
.getRoles().contains(this.rootRole)) {
LOGGER.debug("allow request for root user {}", exchange
.getSecurityContext()
.getAuthenticatedAccount().getPrincipal().getName());
&& exchange.getSecurityContext().getAuthenticatedAccount() != null
&& exchange.getSecurityContext().getAuthenticatedAccount().getRoles().contains(this.rootRole)) {
LOGGER.debug("allow request for root user {}", exchange.getSecurityContext().getAuthenticatedAccount().getPrincipal().getName());

// for root role add a mongo permissions that allows everything
Set<String> roles = Sets.newHashSet();
Expand All @@ -180,8 +177,9 @@ public boolean isAllowed(Request<?> request) {
if (LOGGER.isDebugEnabled()) {
roles(exchange).forEachOrdered(role -> {
ArrayList<MongoAclPermission> matched = Lists.newArrayListWithCapacity(1);
final var key = new CacheKey(role, aclDb(request));

rolePermissions(role)
rolePermissions(key)
.stream().anyMatch(permission -> {
var resolved = permission.allow(request);

Expand All @@ -208,7 +206,9 @@ public boolean isAllowed(Request<?> request) {

// the applicable permission is the ones that
// resolves the exchange
roles(exchange).forEachOrdered(role -> rolePermissions(role)
roles(exchange)
.map(role -> new CacheKey(role, aclDb(request)))
.forEachOrdered(key -> rolePermissions(key)
.stream()
.anyMatch(r -> {
if (r.allow(request)) {
Expand Down Expand Up @@ -237,7 +237,7 @@ public boolean isAuthenticationRequired(Request request) {

var exchange = request.getExchange();

var ps = rolePermissions($UNAUTHENTICATED);
var ps = rolePermissions(new CacheKey(UNAUTHENTICATED, aclDb(request)));

if (ps != null) {
// this fixes undertow bug 377
Expand All @@ -257,6 +257,14 @@ public boolean isAuthenticationRequired(Request request) {
}
}

private String aclDb(Request<?> req) {
if (this.overrideAclDbHeader != null && req.getHeaders().contains(this.overrideAclDbHeader)) {
return req.getHeader(overrideAclDbHeader);
} else {
return this.aclDb;
}
}

private Stream<String> roles(HttpServerExchange exchange) {
return account(exchange).getRoles().stream();
}
Expand All @@ -271,21 +279,21 @@ private boolean isAuthenticated(Account authenticatedAccount) {
}

/**
* @param role
* @param key the CacheKey(id,db)
* @return the acl
*/
public LinkedHashSet<MongoAclPermission> rolePermissions(String role) {
public LinkedHashSet<MongoAclPermission> rolePermissions(CacheKey key) {
if (this.cacheEnabled) {
// TOFIX pinned thread
var _rolePermissions = this.acl.getLoading(role);
var _rolePermissions = this.acl.getLoading(key);

if (_rolePermissions != null && _rolePermissions.isPresent()) {
return _rolePermissions.get();
} else {
return null;
}
} else {
return findRolePermissions(role);
return findRolePermissions(key);
}
}

Expand All @@ -310,16 +318,16 @@ public Set<String> getRoles() {
private static final BsonDocument PROJECTION = BsonDocument.parse("{\"_id\":1,\"roles\":1,\"predicate\":1,\"writeFilter\":1,\"readFilter\":1,\"priority\":1,\"mongo\":1}");
private static final BsonDocument SORT = BsonDocument.parse("{\"priority\":-1,\"_id\":-1}");

private LinkedHashSet<MongoAclPermission> findRolePermissions(final String role) {
private LinkedHashSet<MongoAclPermission> findRolePermissions(final CacheKey key) {
if (this.mclient == null) {
LOGGER.error("Cannot find acl: mongo service is not enabled.");
return null;
} else {
var permissions = new LinkedHashSet<BsonDocument>();
this.mclient.getDatabase(this.aclDb)
this.mclient.getDatabase(key.db)
.getCollection(this.aclCollection)
.withDocumentClass(BsonDocument.class)
.find(eq("roles", role))
.find(eq("roles", key.role))
.projection(PROJECTION)
.sort(SORT)
.into(permissions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
import java.util.Map;

import org.restheart.configuration.ConfigurationException;
import org.restheart.exchange.MongoRequest;
import org.restheart.exchange.Request;
import org.restheart.exchange.ServiceRequest;
import org.restheart.plugins.Inject;
import org.restheart.plugins.OnInit;
import org.restheart.plugins.PluginsRegistry;
Expand Down Expand Up @@ -156,7 +154,7 @@ public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange,
final AuthenticationMechanismOutcome result;
final Account account;
if (authenticator instanceof MongoRealmAuthenticator mauth) {
account = mauth.verify(Request.of(exchange), userName, credential);
account = mauth.verify(Request.of(exchange), userName, credential);
} else {
account = this.authenticator.verify(userName, credential);
}
Expand Down

0 comments on commit d5b3d92

Please sign in to comment.