Acl.java

/*
 * Copyright 2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.bremersee.acl;

import static java.util.Collections.unmodifiableSortedMap;
import static java.util.Objects.nonNull;
import static org.bremersee.acl.AclUserContext.ANONYMOUS;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.bremersee.acl.model.AccessControlListModifications;

/**
 * The access control list.
 *
 * @author Christian Bremer
 */
public interface Acl {

  /**
   * The constant OWNER.
   */
  String OWNER = "owner";

  /**
   * The constant ENTRIES.
   */
  String ENTRIES = "entries";

  /**
   * Builder acl builder.
   *
   * @return the acl builder
   */
  static AclBuilder builder() {
    return new AclBuilder();
  }

  /**
   * With acl.
   *
   * @param owner the owner
   * @param defaultPermissions the default permissions
   * @param adminRoles the admin roles
   * @return the acl
   */
  static Acl with(
      String owner,
      Collection<String> defaultPermissions,
      Collection<String> adminRoles) {
    return new AclBuilder(owner, defaultPermissions, adminRoles).build();
  }

  /**
   * Gets owner.
   *
   * @return the owner
   */
  String getOwner();

  /**
   * Returns the entries of this access control list. The key of the map is the permission. This map
   * is normally unmodifiable.
   *
   * @return the map
   */
  SortedMap<String, Ace> getPermissionMap();

  /**
   * Modifies the access control list. If the modification forbidden, an empty optional will be
   * returned, otherwise the modified access control list.
   *
   * @param mods the modifications
   * @param userContext the user context
   * @param accessEvaluation the access evaluation
   * @param permissions the permissions
   * @return the optional
   */
  default Optional<Acl> modify(
      AccessControlListModifications mods,
      AclUserContext userContext,
      AccessEvaluation accessEvaluation,
      Collection<String> permissions) {

    boolean hasPermission = AccessEvaluator.of(this)
        .hasPermissions(userContext, accessEvaluation, permissions);
    return hasPermission
        ? Optional.of(builder().from(this).apply(mods).build())
        : Optional.empty();
  }

  /**
   * The al builder.
   *
   * @author Christian Bremer
   */
  @ToString
  @EqualsAndHashCode
  class AclBuilder {

    private String owner;

    private final Map<String, Ace> permissionMap = new HashMap<>();

    /**
     * Instantiates a new acl builder.
     */
    public AclBuilder() {
      super();
    }

    private AclBuilder(
        String owner,
        Collection<String> defaultPermissions,
        Collection<String> adminRoles) {

      owner(owner);
      if (nonNull(defaultPermissions)) {
        defaultPermissions.forEach(permission -> addRoles(permission, adminRoles));
      }
    }

    /**
     * From acl.
     *
     * @param acl the acl
     * @return the acl builder
     */
    public AclBuilder from(Acl acl) {
      if (nonNull(acl)) {
        owner(acl.getOwner());
        permissionMap(acl.getPermissionMap());
      }
      return this;
    }

    /**
     * Owner.
     *
     * @param owner the owner
     * @return the acl builder
     */
    public AclBuilder owner(String owner) {
      this.owner = owner;
      return this;
    }

    /**
     * Permission map.
     *
     * @param permissionMap the permission map
     * @return the acl builder
     */
    public AclBuilder permissionMap(Map<String, ? extends Ace> permissionMap) {
      this.permissionMap.clear();
      if (nonNull(permissionMap)) {
        permissionMap.entrySet().stream()
            .filter(entry -> nonNull(entry.getKey()) && !entry.getKey().isBlank())
            .forEach(entry -> this.permissionMap.put(entry.getKey(), entry.getValue()));
        this.permissionMap.putAll(permissionMap);
      }
      return this;
    }

    private AclBuilder doWithAce(String permission, UnaryOperator<Ace> aceFn) {
      if (nonNull(permission) && !permission.isBlank()) {
        Ace ace = this.permissionMap.getOrDefault(permission, Ace.empty());
        this.permissionMap.put(permission, aceFn.apply(ace));
      }
      return this;
    }

    /**
     * Add permissions.
     *
     * @param permissions the permissions
     * @return the acl builder
     */
    public AclBuilder addPermissions(Collection<String> permissions) {
      return Optional.ofNullable(permissions)
          .stream()
          .flatMap(Collection::stream)
          .map(permission -> doWithAce(permission, ace -> ace))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Remove permissions.
     *
     * @param permissions the permissions
     * @return the acl builder
     */
    public AclBuilder removePermissions(Collection<String> permissions) {
      if (nonNull(permissions)) {
        permissions.forEach(this.permissionMap::remove);
      }
      return this;
    }

    /**
     * Guest.
     *
     * @param guest the guest
     * @return the acl builder
     */
    public AclBuilder guest(boolean guest) {
      return guest(p -> true, guest);
    }

    /**
     * Guest.
     *
     * @param permissionFilter the permission filter
     * @param guest the guest
     * @return the acl builder
     */
    public AclBuilder guest(Predicate<String> permissionFilter, boolean guest) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> guest(permission, guest))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Guest.
     *
     * @param permission the permission
     * @param guest the guest
     * @return the acl builder
     */
    public AclBuilder guest(String permission, boolean guest) {
      return doWithAce(permission, ace -> Ace.builder().from(ace).guest(guest).build());
    }

    /**
     * Add users.
     *
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder addUsers(Collection<String> users) {
      return addUsers(p -> true, users);
    }

    /**
     * Add users.
     *
     * @param permissionFilter the permission filter
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder addUsers(Predicate<String> permissionFilter, Collection<String> users) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> addUsers(permission, users))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Add users.
     *
     * @param permission the permission
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder addUsers(String permission, Collection<String> users) {
      if (nonNull(users)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).addUsers(users).build());
      }
      return this;
    }

    /**
     * Remove users.
     *
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder removeUsers(Collection<String> users) {
      return removeUsers(p -> true, users);
    }

    /**
     * Remove users.
     *
     * @param permissionFilter the permission filter
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder removeUsers(Predicate<String> permissionFilter, Collection<String> users) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> removeUsers(permission, users))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Remove users.
     *
     * @param permission the permission
     * @param users the users
     * @return the acl builder
     */
    public AclBuilder removeUsers(String permission, Collection<String> users) {
      if (nonNull(users)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).removeUsers(users).build());
      }
      return this;
    }

    /**
     * Add roles.
     *
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder addRoles(Collection<String> roles) {
      return addRoles(p -> true, roles);
    }

    /**
     * Add roles.
     *
     * @param permissionFilter the permission filter
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder addRoles(Predicate<String> permissionFilter, Collection<String> roles) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> addRoles(permission, roles))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Add roles.
     *
     * @param permission the permission
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder addRoles(String permission, Collection<String> roles) {
      if (nonNull(roles)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).addRoles(roles).build());
      }
      return this;
    }

    /**
     * Remove roles.
     *
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder removeRoles(Collection<String> roles) {
      return removeRoles(p -> true, roles);
    }

    /**
     * Remove roles.
     *
     * @param permissionFilter the permission filter
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder removeRoles(Predicate<String> permissionFilter, Collection<String> roles) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> removeRoles(permission, roles))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Remove roles.
     *
     * @param permission the permission
     * @param roles the roles
     * @return the acl builder
     */
    public AclBuilder removeRoles(String permission, Collection<String> roles) {
      if (nonNull(roles)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).removeRoles(roles).build());
      }
      return this;
    }

    /**
     * Add groups.
     *
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder addGroups(Collection<String> groups) {
      return addGroups(p -> true, groups);
    }

    /**
     * Add groups.
     *
     * @param permissionFilter the permission filter
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder addGroups(Predicate<String> permissionFilter, Collection<String> groups) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> addGroups(permission, groups))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Add groups.
     *
     * @param permission the permission
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder addGroups(String permission, Collection<String> groups) {
      if (nonNull(groups)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).addGroups(groups).build());
      }
      return this;
    }

    /**
     * Remove groups.
     *
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder removeGroups(Collection<String> groups) {
      return removeGroups(p -> true, groups);
    }

    /**
     * Remove groups.
     *
     * @param permissionFilter the permission filter
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder removeGroups(Predicate<String> permissionFilter, Collection<String> groups) {
      Predicate<String> filter = nonNull(permissionFilter) ? permissionFilter : p -> true;
      return permissionMap.keySet().stream()
          .filter(filter)
          .map(permission -> removeGroups(permission, groups))
          .reduce((first, second) -> second)
          .orElse(this);
    }

    /**
     * Remove groups.
     *
     * @param permission the permission
     * @param groups the groups
     * @return the acl builder
     */
    public AclBuilder removeGroups(String permission, Collection<String> groups) {
      if (nonNull(groups)) {
        return doWithAce(permission, ace -> Ace.builder().from(ace).removeGroups(groups).build());
      }
      return this;
    }

    /**
     * Apply modifications.
     *
     * @param modifications the modifications
     * @return the acl builder
     */
    public AclBuilder apply(AccessControlListModifications modifications) {
      if (nonNull(modifications)) {
        modifications.getModificationsDistinct().forEach(aceMods -> {
          guest(aceMods.getPermission(), aceMods.isGuest());
          addUsers(aceMods.getPermission(), aceMods.getAddUsers());
          removeUsers(aceMods.getPermission(), aceMods.getRemoveUsers());
          addRoles(aceMods.getPermission(), aceMods.getAddRoles());
          removeRoles(aceMods.getPermission(), aceMods.getRemoveRoles());
          addGroups(aceMods.getPermission(), aceMods.getAddGroups());
          removeGroups(aceMods.getPermission(), aceMods.getRemoveGroups());
        });
      }
      return this;
    }

    /**
     * Build acl.
     *
     * @return the acl
     */
    public Acl build() {
      return new AclImpl(owner, permissionMap);
    }
  }

  /**
   * The acl implementation.
   *
   * @author Christian Bremer
   */
  @ToString
  @EqualsAndHashCode
  @SuppressWarnings("ClassCanBeRecord")
  class AclImpl implements Acl {

    private final String ownerName;

    private final SortedMap<String, Ace> permissionMap;

    private AclImpl(String owner, Map<String, Ace> permissionMap) {
      this.ownerName = nonNull(owner) && !owner.isBlank() ? owner : ANONYMOUS;
      this.permissionMap = unmodifiableSortedMap(permissionMap.entrySet().stream()
          .filter(entry -> nonNull(entry.getKey()) && !entry.getKey().isBlank())
          .collect(Collectors.toMap(
              Entry::getKey,
              Entry::getValue,
              (a, b) -> a,
              () -> new TreeMap<>(String::compareToIgnoreCase))));
    }

    @Override
    public String getOwner() {
      return ownerName;
    }

    @Override
    public SortedMap<String, Ace> getPermissionMap() {
      return permissionMap;
    }

  }

}