-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add extension point for file-based role validation #107177
Changes from 1 commit
a970a81
85e8e04
e1d14f6
2fb94a2
1cb9937
d6d5bcf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.security.authz; | ||
|
||
import org.elasticsearch.action.ActionRequestValidationException; | ||
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; | ||
|
||
/** | ||
* Provides a check which will be applied to roles in the file-based roles store. | ||
*/ | ||
@FunctionalInterface | ||
public interface FileRoleValidator { | ||
ActionRequestValidationException validatePredefinedRole(RoleDescriptor roleDescriptor); | ||
|
||
/** | ||
* The default file role validator used in stateful Elasticsearch, a no-op. | ||
*/ | ||
class Default implements FileRoleValidator { | ||
@Override | ||
public ActionRequestValidationException validatePredefinedRole(RoleDescriptor roleDescriptor) { | ||
return null; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.ElasticsearchParseException; | ||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.action.ActionRequestValidationException; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.util.Maps; | ||
import org.elasticsearch.common.util.set.Sets; | ||
|
@@ -36,6 +37,7 @@ | |
import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; | ||
import org.elasticsearch.xpack.core.security.support.NoOpLogger; | ||
import org.elasticsearch.xpack.core.security.support.Validation; | ||
import org.elasticsearch.xpack.security.authz.FileRoleValidator; | ||
|
||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
|
@@ -67,6 +69,7 @@ public class FileRolesStore implements BiConsumer<Set<String>, ActionListener<Ro | |
|
||
private final Settings settings; | ||
private final Path file; | ||
private final FileRoleValidator roleValidator; | ||
private final XPackLicenseState licenseState; | ||
private final NamedXContentRegistry xContentRegistry; | ||
private final List<Consumer<Set<String>>> listeners = new ArrayList<>(); | ||
|
@@ -78,21 +81,24 @@ public FileRolesStore( | |
Environment env, | ||
ResourceWatcherService watcherService, | ||
XPackLicenseState licenseState, | ||
NamedXContentRegistry xContentRegistry | ||
NamedXContentRegistry xContentRegistry, | ||
FileRoleValidator roleValidator | ||
) throws IOException { | ||
this(settings, env, watcherService, null, licenseState, xContentRegistry); | ||
this(settings, env, watcherService, null, roleValidator, licenseState, xContentRegistry); | ||
} | ||
|
||
FileRolesStore( | ||
Settings settings, | ||
Environment env, | ||
ResourceWatcherService watcherService, | ||
Consumer<Set<String>> listener, | ||
FileRoleValidator roleValidator, | ||
XPackLicenseState licenseState, | ||
NamedXContentRegistry xContentRegistry | ||
) throws IOException { | ||
this.settings = settings; | ||
this.file = resolveFile(env); | ||
this.roleValidator = roleValidator; | ||
if (listener != null) { | ||
listeners.add(listener); | ||
} | ||
|
@@ -101,7 +107,7 @@ public FileRolesStore( | |
FileWatcher watcher = new FileWatcher(file.getParent()); | ||
watcher.addListener(new FileListener()); | ||
watcherService.add(watcher, ResourceWatcherService.Frequency.HIGH); | ||
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry); | ||
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry, roleValidator); | ||
} | ||
|
||
@Override | ||
|
@@ -176,27 +182,18 @@ public static Path resolveFile(Environment env) { | |
} | ||
|
||
public static Set<String> parseFileForRoleNames(Path path, Logger logger) { | ||
// EMPTY is safe here because we never use namedObject as we are just parsing role names | ||
return parseRoleDescriptors(path, logger, false, Settings.EMPTY, NamedXContentRegistry.EMPTY).keySet(); | ||
} | ||
|
||
public static Map<String, RoleDescriptor> parseFile( | ||
Path path, | ||
Logger logger, | ||
Settings settings, | ||
XPackLicenseState licenseState, | ||
NamedXContentRegistry xContentRegistry | ||
) { | ||
return parseFile(path, logger, true, settings, licenseState, xContentRegistry); | ||
// EMPTY & default role validator are safe here because we never use namedObject as we are just parsing role names | ||
return parseRoleDescriptors(path, logger, false, Settings.EMPTY, new FileRoleValidator.Default(), NamedXContentRegistry.EMPTY) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional nit: |
||
.keySet(); | ||
} | ||
|
||
public static Map<String, RoleDescriptor> parseFile( | ||
Path path, | ||
Logger logger, | ||
boolean resolvePermission, | ||
Settings settings, | ||
XPackLicenseState licenseState, | ||
NamedXContentRegistry xContentRegistry | ||
NamedXContentRegistry xContentRegistry, | ||
FileRoleValidator roleValidator | ||
) { | ||
if (logger == null) { | ||
logger = NoOpLogger.INSTANCE; | ||
|
@@ -210,7 +207,7 @@ public static Map<String, RoleDescriptor> parseFile( | |
|
||
final boolean isDlsLicensed = DOCUMENT_LEVEL_SECURITY_FEATURE.checkWithoutTracking(licenseState); | ||
for (String segment : roleSegments) { | ||
RoleDescriptor descriptor = parseRoleDescriptor(segment, path, logger, resolvePermission, settings, xContentRegistry); | ||
RoleDescriptor descriptor = parseRoleDescriptor(segment, path, logger, true, settings, xContentRegistry, roleValidator); | ||
if (descriptor != null) { | ||
if (ReservedRolesStore.isReserved(descriptor.getName())) { | ||
logger.warn( | ||
|
@@ -243,11 +240,12 @@ public static Map<String, RoleDescriptor> parseFile( | |
return unmodifiableMap(roles); | ||
} | ||
|
||
public static Map<String, RoleDescriptor> parseRoleDescriptors( | ||
private static Map<String, RoleDescriptor> parseRoleDescriptors( | ||
Path path, | ||
Logger logger, | ||
boolean resolvePermission, | ||
Settings settings, | ||
FileRoleValidator roleValidator, | ||
NamedXContentRegistry xContentRegistry | ||
) { | ||
if (logger == null) { | ||
|
@@ -260,7 +258,15 @@ public static Map<String, RoleDescriptor> parseRoleDescriptors( | |
try { | ||
List<String> roleSegments = roleSegments(path); | ||
for (String segment : roleSegments) { | ||
RoleDescriptor rd = parseRoleDescriptor(segment, path, logger, resolvePermission, settings, xContentRegistry); | ||
RoleDescriptor rd = parseRoleDescriptor( | ||
segment, | ||
path, | ||
logger, | ||
resolvePermission, | ||
settings, | ||
xContentRegistry, | ||
roleValidator | ||
); | ||
if (rd != null) { | ||
roles.put(rd.getName(), rd); | ||
} | ||
|
@@ -280,7 +286,8 @@ static RoleDescriptor parseRoleDescriptor( | |
Logger logger, | ||
boolean resolvePermissions, | ||
Settings settings, | ||
NamedXContentRegistry xContentRegistry | ||
NamedXContentRegistry xContentRegistry, | ||
FileRoleValidator roleValidator | ||
) { | ||
String roleName = null; | ||
XContentParserConfiguration parserConfig = XContentParserConfiguration.EMPTY.withRegistry(xContentRegistry) | ||
|
@@ -311,7 +318,7 @@ static RoleDescriptor parseRoleDescriptor( | |
// we pass true as last parameter because we do not want to reject files if field permissions | ||
// are given in 2.x syntax | ||
RoleDescriptor descriptor = RoleDescriptor.parse(roleName, parser, true, false); | ||
return checkDescriptor(descriptor, path, logger, settings, xContentRegistry); | ||
return checkDescriptor(descriptor, path, logger, settings, xContentRegistry, roleValidator); | ||
} else { | ||
logger.error("invalid role definition [{}] in roles file [{}]. skipping role...", roleName, path.toAbsolutePath()); | ||
return null; | ||
|
@@ -344,7 +351,8 @@ private static RoleDescriptor checkDescriptor( | |
Path path, | ||
Logger logger, | ||
Settings settings, | ||
NamedXContentRegistry xContentRegistry | ||
NamedXContentRegistry xContentRegistry, | ||
FileRoleValidator roleValidator | ||
) { | ||
String roleName = descriptor.getName(); | ||
// first check if FLS/DLS is enabled on the role... | ||
|
@@ -374,6 +382,10 @@ private static RoleDescriptor checkDescriptor( | |
} | ||
} | ||
} | ||
ActionRequestValidationException ex = roleValidator.validatePredefinedRole(descriptor); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might add a test to FileRoleStoreTests with a mocked validator that throws |
||
if (ex != null) { | ||
throw ex; | ||
} | ||
return descriptor; | ||
} | ||
|
||
|
@@ -417,7 +429,7 @@ public synchronized void onFileChanged(Path file) { | |
if (file.equals(FileRolesStore.this.file)) { | ||
final Map<String, RoleDescriptor> previousPermissions = permissions; | ||
try { | ||
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry); | ||
permissions = parseFile(file, logger, settings, licenseState, xContentRegistry, roleValidator); | ||
} catch (Exception e) { | ||
logger.error( | ||
() -> format("could not reload roles file [%s]. Current roles remain unmodified", file.toAbsolutePath()), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I think this came out quite clean!