|  | @@ -12,6 +12,7 @@ import org.elasticsearch.ElasticsearchParseException;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.Nullable;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.component.AbstractComponent;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.settings.Settings;
 | 
	
		
			
				|  |  | +import org.elasticsearch.common.util.set.Sets;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.xcontent.NamedXContentRegistry;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.xcontent.XContentParser;
 | 
	
	
		
			
				|  | @@ -34,13 +35,16 @@ import java.nio.charset.StandardCharsets;
 | 
	
		
			
				|  |  |  import java.nio.file.Files;
 | 
	
		
			
				|  |  |  import java.nio.file.Path;
 | 
	
		
			
				|  |  |  import java.util.ArrayList;
 | 
	
		
			
				|  |  | +import java.util.Collections;
 | 
	
		
			
				|  |  |  import java.util.HashMap;
 | 
	
		
			
				|  |  |  import java.util.HashSet;
 | 
	
		
			
				|  |  |  import java.util.List;
 | 
	
		
			
				|  |  |  import java.util.Map;
 | 
	
		
			
				|  |  |  import java.util.Objects;
 | 
	
		
			
				|  |  |  import java.util.Set;
 | 
	
		
			
				|  |  | +import java.util.function.Consumer;
 | 
	
		
			
				|  |  |  import java.util.regex.Pattern;
 | 
	
		
			
				|  |  | +import java.util.stream.Collectors;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import static java.util.Collections.emptyMap;
 | 
	
		
			
				|  |  |  import static java.util.Collections.unmodifiableMap;
 | 
	
	
		
			
				|  | @@ -52,16 +56,16 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      private final Path file;
 | 
	
		
			
				|  |  |      private final XPackLicenseState licenseState;
 | 
	
		
			
				|  |  | -    private final List<Runnable> listeners = new ArrayList<>();
 | 
	
		
			
				|  |  | +    private final List<Consumer<Set<String>>> listeners = new ArrayList<>();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      private volatile Map<String, RoleDescriptor> permissions;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, XPackLicenseState licenseState)
 | 
	
		
			
				|  |  |              throws IOException {
 | 
	
		
			
				|  |  | -        this(settings, env, watcherService, () -> {}, licenseState);
 | 
	
		
			
				|  |  | +        this(settings, env, watcherService, null, licenseState);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Runnable listener,
 | 
	
		
			
				|  |  | +    FileRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Consumer<Set<String>> listener,
 | 
	
		
			
				|  |  |                     XPackLicenseState licenseState) throws IOException {
 | 
	
		
			
				|  |  |          super(settings);
 | 
	
		
			
				|  |  |          this.file = resolveFile(env);
 | 
	
	
		
			
				|  | @@ -76,9 +80,10 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public Set<RoleDescriptor> roleDescriptors(Set<String> roleNames) {
 | 
	
		
			
				|  |  | +        final Map<String, RoleDescriptor> localPermissions = permissions;
 | 
	
		
			
				|  |  |          Set<RoleDescriptor> descriptors = new HashSet<>();
 | 
	
		
			
				|  |  |          roleNames.forEach((name) -> {
 | 
	
		
			
				|  |  | -            RoleDescriptor descriptor = permissions.get(name);
 | 
	
		
			
				|  |  | +            RoleDescriptor descriptor = localPermissions.get(name);
 | 
	
		
			
				|  |  |              if (descriptor != null) {
 | 
	
		
			
				|  |  |                  descriptors.add(descriptor);
 | 
	
		
			
				|  |  |              }
 | 
	
	
		
			
				|  | @@ -87,12 +92,13 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public Map<String, Object> usageStats() {
 | 
	
		
			
				|  |  | +        final Map<String, RoleDescriptor> localPermissions = permissions;
 | 
	
		
			
				|  |  |          Map<String, Object> usageStats = new HashMap<>(3);
 | 
	
		
			
				|  |  | -        usageStats.put("size", permissions.size());
 | 
	
		
			
				|  |  | +        usageStats.put("size", localPermissions.size());
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          boolean dls = false;
 | 
	
		
			
				|  |  |          boolean fls = false;
 | 
	
		
			
				|  |  | -        for (RoleDescriptor descriptor : permissions.values()) {
 | 
	
		
			
				|  |  | +        for (RoleDescriptor descriptor : localPermissions.values()) {
 | 
	
		
			
				|  |  |              for (IndicesPrivileges indicesPrivileges : descriptor.getIndicesPrivileges()) {
 | 
	
		
			
				|  |  |                  fls = fls || indicesPrivileges.getGrantedFields() != null || indicesPrivileges.getDeniedFields() != null;
 | 
	
		
			
				|  |  |                  dls = dls || indicesPrivileges.getQuery() != null;
 | 
	
	
		
			
				|  | @@ -107,10 +113,10 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |          return usageStats;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    public void addListener(Runnable runnable) {
 | 
	
		
			
				|  |  | -        Objects.requireNonNull(runnable);
 | 
	
		
			
				|  |  | +    public void addListener(Consumer<Set<String>> consumer) {
 | 
	
		
			
				|  |  | +        Objects.requireNonNull(consumer);
 | 
	
		
			
				|  |  |          synchronized (this) {
 | 
	
		
			
				|  |  | -            listeners.add(runnable);
 | 
	
		
			
				|  |  | +            listeners.add(consumer);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -118,6 +124,11 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |          return file;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    // package private for testing
 | 
	
		
			
				|  |  | +    Set<String> getAllRoleNames() {
 | 
	
		
			
				|  |  | +        return permissions.keySet();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public static Path resolveFile(Environment env) {
 | 
	
		
			
				|  |  |          return XPackPlugin.resolveConfigFile(env, "roles.yml");
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -319,11 +330,13 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          @Override
 | 
	
		
			
				|  |  | -        public void onFileChanged(Path file) {
 | 
	
		
			
				|  |  | +        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);
 | 
	
		
			
				|  |  | -                    logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(), Files.exists(file) ? "changed" : "removed");
 | 
	
		
			
				|  |  | +                    logger.info("updated roles (roles file [{}] {})", file.toAbsolutePath(),
 | 
	
		
			
				|  |  | +                        Files.exists(file) ? "changed" : "removed");
 | 
	
		
			
				|  |  |                  } catch (Exception e) {
 | 
	
		
			
				|  |  |                      logger.error(
 | 
	
		
			
				|  |  |                              (Supplier<?>) () -> new ParameterizedMessage(
 | 
	
	
		
			
				|  | @@ -331,9 +344,13 @@ public class FileRolesStore extends AbstractComponent {
 | 
	
		
			
				|  |  |                      return;
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -                synchronized (FileRolesStore.this) {
 | 
	
		
			
				|  |  | -                    listeners.forEach(Runnable::run);
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | +                final Set<String> changedOrMissingRoles = Sets.difference(previousPermissions.entrySet(), permissions.entrySet())
 | 
	
		
			
				|  |  | +                        .stream()
 | 
	
		
			
				|  |  | +                        .map(Map.Entry::getKey)
 | 
	
		
			
				|  |  | +                        .collect(Collectors.toSet());
 | 
	
		
			
				|  |  | +                final Set<String> addedRoles = Sets.difference(permissions.keySet(), previousPermissions.keySet());
 | 
	
		
			
				|  |  | +                final Set<String> changedRoles = Collections.unmodifiableSet(Sets.union(changedOrMissingRoles, addedRoles));
 | 
	
		
			
				|  |  | +                listeners.forEach(c -> c.accept(changedRoles));
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 |