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

io.takari.builder.internal.BuilderContext Maven / Gradle / Ivy

package io.takari.builder.internal;

import static io.takari.builder.enforcer.ComposableSecurityManagerPolicy.getContextPolicy;
import static io.takari.builder.enforcer.ComposableSecurityManagerPolicy.registerContextPolicy;
import static io.takari.builder.enforcer.ComposableSecurityManagerPolicy.unregisterContextPolicy;
import static io.takari.builder.internal.pathmatcher.PathNormalizer.normalize0;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;

import io.takari.builder.Messages;
import io.takari.builder.enforcer.Policy;
import io.takari.builder.enforcer.internal.EnforcerViolation;
import io.takari.builder.enforcer.internal.EnforcerViolationType;
import io.takari.builder.internal.BuilderExecutionState.InprogressStateWriter;
import io.takari.builder.internal.pathmatcher.FileMatcher;
import io.takari.builder.internal.pathmatcher.PathMatcher;
import io.takari.builder.internal.pathmatcher.PathNormalizer;

public class BuilderContext {

  // value is documented in System.setProperty(String, String)
  static final String PROPERTY_WRITE_ACTION = "write";

  // value is documented in SecurityManager.checkPropertyAccess(String)
  static final String PROPERTY_READ_ACTION = "read";

  // value is documented SecurityManager.checkPropertiesAccess()
  static final String PROPERTY_RW_ACTION = "read,write";

  private static final Object KEY_CONTEXT = BuilderContextPolicy.class;

  public void enter() {
    registerContextPolicy(KEY_CONTEXT, new BuilderContextPolicy(this));
  }

  public void leave() {
    BuilderContextPolicy policy = (BuilderContextPolicy) unregisterContextPolicy(KEY_CONTEXT);
    if (policy.getBuilderContext() != this) {
      throw new IllegalStateException();
    }
    policy.inScope.set(false);
  }

  private static BuilderContext getCurrentContext() {
    BuilderContextPolicy policy = (BuilderContextPolicy) getContextPolicy(KEY_CONTEXT);
    if (policy == null) {
      throw new IllegalStateException();
    }
    return policy.getBuilderContext();
  }

  //
  // enforcement
  //

  static class BuilderContextPolicy implements Policy {

    private final BuilderContext ctx;

    public BuilderContextPolicy(BuilderContext ctx) {
      this.ctx = ctx;
    }

    public BuilderContext getBuilderContext() {
      return ctx;
    }

    private final ThreadLocal readPrivileged =
        ThreadLocal.withInitial(() -> Boolean.FALSE);
    private AtomicBoolean inScope = new AtomicBoolean(true);

    @Override
    public void checkWrite(String file) {
      checkScope();
      if (!ctx.checkAndRecordWrite(file)) {
        handleViolation(ctx, EnforcerViolationType.WRITE, file);
      }
    }

    @Override
    public void checkSocketPermission() {
      checkScope();
      if (!ctx.checkSockets()) {
        throw new SecurityException();
      }
    }

    @Override
    public void checkRead(String file) {
      checkScope();
      if (readPrivileged.get() == Boolean.TRUE) {
        return;
      }
      try {
        // TODO evaluate performance overhead of setting/resetting on each invocation
        // this is necessary because builder policy performs secondary filesystem check
        // originally, the filesystem check was performed separately and far less frequently
        // the two checks were collapsed into single policy call for Policy API clarity
        readPrivileged.set(Boolean.TRUE);
        if (!ctx.checkRead(file)) {
          handleViolation(ctx, EnforcerViolationType.READ, normalize0(file));
        }
      } finally {
        readPrivileged.set(Boolean.FALSE);
      }
    }

    @Override
    public void checkPropertyPermission(String action, String name) {
      checkScope();
      if (!ctx.checkAndRecordProperty(action, name)) {
        throw new SecurityException();
      }
    }

    @Override
    public void checkExec(String cmd) {
      checkScope();
      if (!ctx.checkExec(cmd)) {
        handleViolation(ctx, EnforcerViolationType.EXECUTE, cmd);
      }
    }

    //
    // private methods
    //

    private void checkScope() {
      if (!inScope.get()) {
        throw new IllegalStateException("BuilderContext is no longer in scope");
      }
    }

    private void handleViolation(BuilderContext ctx, EnforcerViolationType violationType,
        String path) {
      ctx.addViolation(new EnforcerViolation(violationType, path));
    }

  }

  //
  // Messages
  //

  public static Messages MESSAGES = new Messages() {

    private Messages getCurrentMessages() {
      return getCurrentContext().getMessages();
    }

    @Override
    public void warn(File resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().warn(resource, line, column, message, cause);
    }

    @Override
    public void info(File resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().info(resource, line, column, message, cause);
    }

    @Override
    public void error(File resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().error(resource, line, column, message, cause);
    }

    @Override
    public void warn(Path resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().warn(resource, line, column, message, cause);
    }

    @Override
    public void info(Path resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().info(resource, line, column, message, cause);
    }

    @Override
    public void error(Path resource, int line, int column, String message, Throwable cause) {
      getCurrentMessages().error(resource, line, column, message, cause);
    }
  };

  //
  // Construction
  //

  public static class Builder {

    private final Logger log;
    private final String id;
    private final Path sessionBasedir;
    private final PathNormalizer normalizer;
    private final PathMatcher.Builder readMatcherBuilder;
    private final PathMatcher.Builder readAndTrackMatcherBuilder;
    private final PathMatcher.Builder writeMatcherBuilder;
    private final PathMatcher.Builder tempMatcherBuilder;
    private final MessageCollector messages;
    private final Collection execExceptions = new LinkedHashSet<>();
    private boolean networkAccessAllowed;
    private final Collection readExceptions = new LinkedHashSet<>();
    private final Collection writeExceptions = new LinkedHashSet<>();
    private InprogressStateWriter inprogressWriter = BuilderExecutionState.NOOP_INPROGRESSWRITER;
    private final BuilderWorkspace workspace;

    private Builder(Logger log, String id, Path sessionBasedir, MessageCollector messages,
        BuilderWorkspace workspace) {
      this.log = log;
      this.id = id;
      this.sessionBasedir = sessionBasedir;
      this.messages = messages;
      this.normalizer = new PathNormalizer(sessionBasedir);
      this.readMatcherBuilder = PathMatcher.builder(normalizer).excludeRoot();
      this.readAndTrackMatcherBuilder = PathMatcher.builder(normalizer).excludeRoot();
      this.writeMatcherBuilder = PathMatcher.builder(normalizer).excludeRoot();
      this.tempMatcherBuilder = PathMatcher.builder(normalizer).excludeRoot();
      this.workspace = workspace;
    }

    public Builder addInputMatcher(PathMatcher matcher) {
      readMatcherBuilder.addMatcher(matcher);
      return this;
    }

    public Builder addInputFiles(Collection inputFiles) {
      inputFiles.forEach(f -> readMatcherBuilder.includePath(f.toAbsolutePath().toString()));
      return this;
    }

    public Builder addInputFile(Path file) {
      readMatcherBuilder.includePath(file.toAbsolutePath().toString());
      return this;
    }

    public Builder addInputDirectory(Path directory) {
      readMatcherBuilder.includePrefix(directory.toAbsolutePath().toString());
      return this;
    }

    public Builder addOutputDirectory(Path directory) {
      final String path = directory.toAbsolutePath().toString();

      writeMatcherBuilder.includePrefix(path);
      tempMatcherBuilder.excludePrefix(path);

      return this;
    }

    public Builder addOutputFile(Path file) {
      final String path = file.toAbsolutePath().toString();

      writeMatcherBuilder.includePath(path);
      tempMatcherBuilder.excludePath(path);

      return this;
    }

    public Builder addTemporaryDirectory(Path directory) {
      final String path = directory.toAbsolutePath().toString();

      tempMatcherBuilder.includePrefix(path);
      writeMatcherBuilder.excludePrefix(path);

      return this;
    }

    public Builder addReadExceptions(Collection readExceptions) {
      this.readExceptions.addAll(readExceptions);

      return this;
    }

    public Builder addReadAndTrackExceptions(Collection exceptions) {
      exceptions.forEach(p -> readAndTrackMatcherBuilder.includePath(p));

      return this;
    }

    public Builder addWriteExceptions(Collection writeExceptions) {
      this.writeExceptions.addAll(writeExceptions);

      return this;
    }

    public Builder addExecExceptions(Collection execExceptions) {
      this.execExceptions.addAll(execExceptions);

      return this;
    }

    public Builder setNetworkAccessAllowed(boolean networkAccessAllowed) {
      this.networkAccessAllowed = networkAccessAllowed;

      return this;
    }

    public Builder setInprogressWriter(InprogressStateWriter inprogressWriter) {
      this.inprogressWriter = inprogressWriter;

      return this;
    }

    public BuilderContext build() {
      // tests often create test projects under temp directory
      // therefore explicitly exclude session basedir from temp matcher
      tempMatcherBuilder.excludePrefix(normalize0(sessionBasedir));

      PathMatcher readMatcher = readMatcherBuilder.build();
      PathMatcher writeMatcher = writeMatcherBuilder.build();
      PathMatcher tempMatcher = tempMatcherBuilder.build();
      PathMatcher readAndTrackExceptionsMatcher = readAndTrackMatcherBuilder.build();

      return new BuilderContext(log, normalizer, id, readMatcher, writeMatcher, tempMatcher,
          messages, execExceptions, networkAccessAllowed, readExceptions, writeExceptions,
          readAndTrackExceptionsMatcher, inprogressWriter, workspace);
    }
  }

  public static Builder builder(Logger log, String id, Path sessionBasedir,
      MessageCollector messages, BuilderWorkspace workspace) {
    return new Builder(log, id, sessionBasedir, messages, workspace);
  }

  //
  // Runtime behaviour
  //

  private final Logger log;

  private final String id;

  private final PathNormalizer normalizer;
  private final PathMatcher readMatcher;
  private final PathMatcher writeMatcher;
  private final PathMatcher tempMatcher;
  private final Collection execExceptions;
  private final boolean networkAccessAllowed;
  private final FileMatcher readExceptionsMatcher;
  private final FileMatcher writeExceptionsMatcher;
  private final PathMatcher readAndTrackExceptionsMatcher;

  // mutable context state (below) can be accessed from multiple threads
  // all collections are concurrent-safe to allow reads and writes
  // additionally, access to writes/tempWrites is guarded by writeLock

  private final Set violations = ConcurrentHashMap.newKeySet();
  private final MessageCollector messages;
  private final BuilderWorkspace workspace;

  /*
   * this lock guarantees that "file created by this builder" and "file exists" checks are in sync
   * with each other as observed by checkRead method. failure to synchronize the two checks results
   * in checkRead false negative if "file exists" check becomes true before
   * "created by this builder".
   * 
   * @see io.takari.builder.internal.BuilderContextTest.testConcurrentReadWriteCheck()
   */
  // igorf: more elaborate ReadWriteLock implementation does not provide measurable performance
  // benefits, going with simpler approach
  private final Object writeLock = new Object();

  private final InprogressStateWriter inprogressWriter;

  // output files written by the builder
  private final Set writes = ConcurrentHashMap.newKeySet();

  // temporary files written by the builder
  private final Set tempWrites = ConcurrentHashMap.newKeySet();

  // system properties read by the builder
  private final Set properties = ConcurrentHashMap.newKeySet();

  private BuilderContext(Logger log, PathNormalizer normalizer, String id, PathMatcher readMatcher,
      PathMatcher writeMatcher, PathMatcher tempMatcher, MessageCollector messages,
      Collection execExceptions, boolean networkAccessAllowed,
      Collection readExceptions, Collection writeExceptions,
      PathMatcher readAndTrackExceptionsMatcher, InprogressStateWriter inprogressWriter,
      BuilderWorkspace workspace) {
    this.log = log;
    this.id = id;
    this.readMatcher = readMatcher;
    this.writeMatcher = writeMatcher;
    this.tempMatcher = tempMatcher;
    this.messages = messages;
    this.normalizer = normalizer;
    this.execExceptions = execExceptions;
    this.networkAccessAllowed = networkAccessAllowed;
    this.inprogressWriter = inprogressWriter;
    this.readExceptionsMatcher = getExceptionsMatcher(readExceptions);
    this.writeExceptionsMatcher = getExceptionsMatcher(writeExceptions);
    this.readAndTrackExceptionsMatcher = readAndTrackExceptionsMatcher;
    this.workspace = workspace;
  }

  private static FileMatcher getExceptionsMatcher(Collection exceptions) {
    return exceptions != null && !exceptions.isEmpty()
        ? FileMatcher.absoluteMatcher(Paths.get("/"), exceptions, null)
        : FileMatcher.absoluteMatcher(Paths.get("/"), null, Arrays.asList("*"));
  }

  @Override
  public String toString() {
    return id;
  }

  public final boolean checkRead(String file) {
    String normalized = normalizer.normalize(file);

    if (readExceptionsMatcher.matches(normalized)
        || readAndTrackExceptionsMatcher.includes(normalized)) {
      return true;
    }

    synchronized (writeLock) {
      if (writes.contains(normalized) || tempWrites.contains(normalized)) {
        return true;
      }

      if (!readMatcher.includes(normalized)) {
        // still allow reads of files that do not exist
        return !workspace.isRegularFile(Paths.get(normalized));
      }
    }
    return true;
  }

  public final boolean checkAndRecordWrite(String file) {
    String normalized = normalizer.normalize(file);

    if (writeExceptionsMatcher.matches(normalized)) {
      return true;
    }

    synchronized (writeLock) {
      if (writes.contains(normalized) || tempWrites.contains(normalized)) {
        return true;
      }

      // Do not allow writes to existing files, unless the existing file is an input (whitelisted in
      // builder-enforcer.config)
      if (workspace.isRegularFile(Paths.get(normalized))
          && !readAndTrackExceptionsMatcher.includes(normalized)) {
        return false;
      }

      if (writeMatcher.includes(normalized)) {
        writes.add(normalized);
        recordInprogressWrite(normalized);
        workspace.processOutput(Paths.get(normalized));
        return true;
      }

      if (tempMatcher.includes(normalized)) {
        tempWrites.add(normalized);
        recordInprogressWrite(normalized);
        return true;
      }
    }

    return false;
  }

  private void recordInprogressWrite(String normalized) {
    if (!readAndTrackExceptionsMatcher.includes(normalized)) {
      inprogressWriter.writePath(normalized);
    }
  }

  public final boolean checkExec(String command) {
    return execExceptions.contains(command);
  }

  public final boolean checkSockets() {
    return networkAccessAllowed;
  }

  public boolean checkAndRecordProperty(String action, String name) {
    // Read properties are considered builder inputs and must be tracked
    if (action.equals(PROPERTY_READ_ACTION) || action.equals(PROPERTY_RW_ACTION)) {
      properties.add(name);
      return true;
    }

    // Allow write of property values.
    // This is necessary because some standard Java library classes, like TimeZone, set system
    // properties, something Builders cannot control.
    // As the downside, this may result unnecessary builder executions during no-change incremental
    // build, triggered by properties not being written by skipped builders (hope this makes sense).
    if (action.equals(PROPERTY_WRITE_ACTION)) {
      return true;
    }

    return false;
  }

  public boolean addViolation(EnforcerViolation violation) {
    boolean added = violations.add(violation);
    if (added) {
      StringBuilder msg = new StringBuilder();
      msg.append(String.format("Access to an undeclared resource detected in builder: %s",
          this.toString()));
      msg.append("\n   " + violation.getFormattedViolation());
      violation.getStackTrace().forEach(s -> msg.append("\n   | ").append(s));
      log.error(msg.toString());
    }
    return added;
  }

  public Set getViolations() {
    return violations;
  }

  /** returns normalized paths of written files */
  public Collection getWrittenFiles() {
    return writes;
  }

  /** returns normalized paths of written temporary files */
  public Collection getTemporaryFiles() {
    return tempWrites;
  }

  /** returns system properties read by the builder */
  public Collection getReadProperties() {
    return properties;
  }

  public String getId() {
    return id;
  }

  public MessageCollector getMessages() {
    return messages;
  }

  public boolean wasWhitelistedException(String file) {
    return readAndTrackExceptionsMatcher.includes(normalizer.normalize(file));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy