ReactiveUserContextCaller.java

/*
 * Copyright 2020 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.security.core;

import java.util.Collections;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
import org.bremersee.exception.ServiceException;
import org.reactivestreams.Publisher;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.validation.annotation.Validated;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * The reactive user context caller.
 *
 * @author Christian Bremer
 */
@Validated
public class ReactiveUserContextCaller {

  /**
   * The constant EMPTY_GROUPS_SUPPLIER.
   */
  public static final Supplier<Mono<Set<String>>> EMPTY_GROUPS_SUPPLIER = () -> Mono
      .just(Collections.emptySet());

  /**
   * The constant EMPTY_USER_CONTEXT_SUPPLIER.
   */
  public static final Supplier<Mono<UserContext>> EMPTY_USER_CONTEXT_SUPPLIER = () -> Mono
      .just(UserContext.newInstance());

  /**
   * The constant FORBIDDEN_SUPPLIER.
   */
  public static final Supplier<Mono<UserContext>> FORBIDDEN_SUPPLIER = () -> Mono
      .error(ServiceException::forbidden);

  private final Function<Authentication, Mono<Set<String>>> groupsFn;

  private final Supplier<Mono<UserContext>> unauthenticatedSupplier;

  /**
   * Instantiates a new reactive user context caller.
   */
  public ReactiveUserContextCaller() {
    this(EMPTY_GROUPS_SUPPLIER, null);
  }

  /**
   * Instantiates a new reactive user context caller.
   *
   * @param groupsSupplier the groups supplier
   * @param unauthenticatedSupplier the unauthenticated supplier
   */
  public ReactiveUserContextCaller(
      @Nullable Supplier<Mono<Set<String>>> groupsSupplier,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    this(
        groupsSupplier != null
            ? authentication -> groupsSupplier.get()
            : authentication -> EMPTY_GROUPS_SUPPLIER.get(),
        unauthenticatedSupplier);
  }

  /**
   * Instantiates a new reactive user context caller.
   *
   * @param groupsFn the groups fn
   * @param unauthenticatedSupplier the unauthenticated supplier
   */
  public ReactiveUserContextCaller(
      @Nullable Function<Authentication, Mono<Set<String>>> groupsFn,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    this.groupsFn = groupsFn != null ? groupsFn : authentication -> EMPTY_GROUPS_SUPPLIER.get();
    this.unauthenticatedSupplier = unauthenticatedSupplier != null
        ? unauthenticatedSupplier
        : FORBIDDEN_SUPPLIER;
  }

  private Set<String> toRoles(Authentication authentication) {
    return authentication.getAuthorities()
        .stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.toSet());
  }

  /**
   * One with user context mono.
   *
   * @param <R> the type parameter
   * @param function the function
   * @return the mono
   */
  public <R> Mono<R> oneWithUserContext(
      @NotNull Function<UserContext, ? extends Mono<R>> function) {
    return ReactiveSecurityContextHolder.getContext()
        .map(SecurityContext::getAuthentication)
        .filter(Authentication::isAuthenticated)
        .zipWhen(authentication -> groupsFn
            .apply(authentication)
            .switchIfEmpty(EMPTY_GROUPS_SUPPLIER.get()))
        .map(tuple -> UserContext.newInstance(
            tuple.getT1().getName(),
            toRoles(tuple.getT1()),
            tuple.getT2()))
        .switchIfEmpty(unauthenticatedSupplier.get())
        .flatMap(function);
  }

  /**
   * One with user context mono.
   *
   * @param <R> the type parameter
   * @param function the function
   * @param groupsFn the groups fn
   * @param unauthenticatedSupplier the unauthenticated supplier
   * @return the mono
   */
  public static <R> Mono<R> oneWithUserContext(
      @NotNull Function<UserContext, ? extends Mono<R>> function,
      @Nullable Function<Authentication, Mono<Set<String>>> groupsFn,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    return new ReactiveUserContextCaller(groupsFn, unauthenticatedSupplier)
        .oneWithUserContext(function);
  }

  /**
   * One with user context mono.
   *
   * @param <R> the type parameter
   * @param function the function
   * @param groupsSupplier the groups supplier
   * @param unauthenticatedSupplier the unauthenticated supplier
   * @return the mono
   */
  public static <R> Mono<R> oneWithUserContext(
      @NotNull Function<UserContext, ? extends Mono<R>> function,
      @Nullable Supplier<Mono<Set<String>>> groupsSupplier,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    return new ReactiveUserContextCaller(groupsSupplier, unauthenticatedSupplier)
        .oneWithUserContext(function);
  }

  /**
   * Many with user context flux.
   *
   * @param <R> the type parameter
   * @param function the function
   * @return the flux
   */
  public <R> Flux<R> manyWithUserContext(
      @NotNull Function<UserContext, ? extends Publisher<R>> function) {
    return ReactiveSecurityContextHolder.getContext()
        .map(SecurityContext::getAuthentication)
        .filter(Authentication::isAuthenticated)
        .zipWhen(authentication -> groupsFn
            .apply(authentication)
            .switchIfEmpty(EMPTY_GROUPS_SUPPLIER.get()))
        .map(tuple -> UserContext.newInstance(
            tuple.getT1().getName(),
            toRoles(tuple.getT1()),
            tuple.getT2()))
        .switchIfEmpty(unauthenticatedSupplier.get())
        .flatMapMany(function);
  }

  /**
   * Many with user context flux.
   *
   * @param <R> the type parameter
   * @param function the function
   * @param groupsFn the groups fn
   * @param unauthenticatedSupplier the unauthenticated supplier
   * @return the flux
   */
  public static <R> Flux<R> manyWithUserContext(
      @NotNull Function<UserContext, ? extends Publisher<R>> function,
      @Nullable Function<Authentication, Mono<Set<String>>> groupsFn,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    return new ReactiveUserContextCaller(groupsFn, unauthenticatedSupplier)
        .manyWithUserContext(function);
  }

  /**
   * Many with user context flux.
   *
   * @param <R> the type parameter
   * @param function the function
   * @param groupsSupplier the groups supplier
   * @param unauthenticatedSupplier the unauthenticated supplier
   * @return the flux
   */
  public static <R> Flux<R> manyWithUserContext(
      @NotNull Function<UserContext, ? extends Publisher<R>> function,
      @Nullable Supplier<Mono<Set<String>>> groupsSupplier,
      @Nullable Supplier<Mono<UserContext>> unauthenticatedSupplier) {
    return new ReactiveUserContextCaller(groupsSupplier, unauthenticatedSupplier)
        .manyWithUserContext(function);
  }

}