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

nl.vpro.domain.user.UserService Maven / Gradle / Ivy

Go to download

Domain classes and interfaces related to accountability, users and organizations.

There is a newer version: 8.3.1
Show newest version
/*
 * Copyright (C) 2010 Licensed under the Apache License, Version 2.0
 * VPRO The Netherlands
 */
package nl.vpro.domain.user;

import lombok.Getter;

import java.security.Principal;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.checkerframework.checker.nullness.qual.*;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import nl.vpro.domain.Roles;
import nl.vpro.i18n.Locales;
import nl.vpro.logging.mdc.MDCConstants;
import nl.vpro.logging.simple.SimpleLogger;

import static nl.vpro.logging.mdc.MDCConstants.ON_BEHALF_OF;


/**
 * The user services provides service related to users. This integrates with spring security, and e.g. with keycloak. It may also support saving the users to a local database. If this in unneeded, because the current system does not have a database backing these methods may be left unimplemented
 */
public interface UserService {

    ThreadPoolExecutor ASYNC_EXECUTOR = new ThreadPoolExecutor(1,
        10000,
        600L, TimeUnit.SECONDS,
        new SynchronousQueue<>());


    /**
     * Given an existing user, and a user obtained from sso or so, determins whether calling {@link #update(User)} is important now.
     */
    boolean needsUpdate(T oldUser, T newUser);


    /**
     * Do as a certain user, the 'lastlogin' of the user will not be updated
     * @see #doAs(String, Duration, Callable)
     */
    default  S doAs(String principalId, Callable handler) throws Exception {
        return doAs(principalId, null, handler);
    }

    /**
     * Do as a certain user, the 'lastlogin' of the user will be updated, unless that already happend
     * less that {@code loginAfter} ago, or the implementation doesn't persist the user at all (e.g. in the frontend api)
     * @param loginAfter If the user logged in less than this time ago, the last login will not be updated.
     *                   If {@code null} the last login will never be updated, if {@link Duration#ZERO} it will always be updated.
     * @since 8.1.1
     */
     S doAs(String principalId, Duration loginAfter, Callable handler) throws Exception;


    /**
     *  Do as a certain user, without the need to be logged in already.
     */
     S systemDoAs(String principalId, Callable handler) throws Exception;

     /**
      * Default implementation without consideration of the roles. This can be overridden.
      */
     Logout systemAuthenticate(Trusted trustedSourceToken);
     // THIS used to be the deafult implementation, but that seems to have broken @Transactional on overriding methods.
     /* {
        Optional user = authenticate(trustedSourceToken.getPrincipal());
        Logout logout = new Logout() {
            @Override
            public void close() {
                dropAuthentication();
            }
        };
        logout.setUser(currentUser().orElseThrow(IllegalStateException::new));
        return logout;
    }*/

    /**
     * From a principal object creates the user if not exists and returns it.
     * @since 5.12
     */
    T get(java.security.Principal authentication);

    /**
     * Whether {@link #get(java.security.Principal)} can handle this principal
     * @since 8.2
     */

    default boolean recognized(java.security.Principal authentication) {
        return false;
    }

    Optional get(@NonNull String id);

    /**
     * Just gets a user from local persistence. No implicit creating. This may also give an object that is a bit incomplete, e.g. we don't store roles in the database
     */
    default Optional getOnly(@NonNull String id) {
        throw new UnsupportedOperationException();
    }

    /**
     * Logins in the given principal
     */
    //@Transactional(Transactional.TxType.NEVER) // This make (in mockito 5) this stuff kind of unmockable.
    default T login(java.security.Principal authentication, Instant timestamp) {
        T editor = get(authentication);
        if (timestamp != null) {
            editor.setLastLogin(timestamp);
        }
        return editor;
    }

    /**
     * Searches users in the local database
     * @throws UnsupportedOperationException if not implementable
     */
    List findUsers(String name, int limit);

    /**
     * Updates a user in the local database. If there is no database in the application, it may simply return the argument.
     */
    default T update(T user) {
        return user;
    }

    /**
     * Deletes user from the local database
     * @throws UnsupportedOperationException if not implementable
     */
    void delete(T object);

    default Optional currentUser() {
        return currentUser(false);
    }

    Optional currentUser(boolean mayCreate);


    Optional authenticate(String principalId);

    /**
     * Checks whether current user has at least one of the given roles
     */
    default boolean currentUserHasRole(String... roles) {
        return currentUserHasRole(Arrays.asList(roles));
    }

    default Optional currentPrincipalId() {
        return currentUser().map(User::getPrincipalId);
    }

    boolean currentUserHasRole(Collection roles);

    Principal getAuthentication();

    void restoreAuthentication(Principal authentication);

    default boolean isAuthenticated() {
        return getAuthentication() != null;
    }

    void dropAuthentication();

    default boolean isPrivilegedUser() {
        return currentUserHasRole(Roles.PRIVILEGED);
    }

    default boolean isProcessUser() {
        return currentUserHasRole(
            Roles.PROCESS_ROLE,
            Roles.SUPERPROCESS_ROLE
        );
    }

    /**
     * See {@link Roles#PUBLISHER_ROLE}
     */
    default boolean isPublisher() {
        return currentUserHasRole(Roles.PUBLISHER_ROLE);
    }

    /**
     * Submits callable in the given {@link ExecutorService}, but makes sure that it is executed as the current user
     */
    default  Future submit(ExecutorService executorService, Callable callable) {
        return submit(executorService, callable, null);
    }

    /**
     * Defaulting version of {@link #async(Callable, SimpleLogger, ExecutorService)}, where the executor service is {@link #ASYNC_EXECUTOR}
     *
     * @param logger If not null catch exceptions and log as error.
     * @since 5.6
     */
    default  CompletableFuture async(Callable callable, SimpleLogger logger) {
        return async(callable, logger, ASYNC_EXECUTOR);
    }

    /**
     * Submits callable (wrapped by {@link #wrap(Callable, SimpleLogger, Boolean, boolean)} )}) in CompletableFuture#supplyAsync.
     * 

* This makes sure that the job is running as the current user, and for example also that the current MDC is copied to the other thread. *

* Note that if you use {@link CompletableFuture#thenAccept(Consumer)} or something similar that these will not be run in the same context. You can wrapp those with {@link #wrap(Callable, SimpleLogger, Boolean, boolean)} yourself. * * @param callable The job to run asynchronously * @param logger If not null catch exceptions and log as error. * * @since 5.16 */ default CompletableFuture async(Callable callable, SimpleLogger logger, ExecutorService executor) { Callable wrapped = wrap(callable, logger, true, true); // Current MDC will be copied and stored and restores just before calling the unwrapped callable Supplier supplier = () -> { try { return wrapped.call(); } catch (RuntimeException rte) { throw rte; } catch (Exception e) { throw new RuntimeException(e); } }; return CompletableFuture.supplyAsync(supplier, executor); } /** * Submits callable in the given {@link ExecutorService}, but makes sure that it is executed as the current user and current {@link MDC} * @param logger If not null catch exceptions and log as error. * @since 5.6 */ default Future submit(ExecutorService executorService, Callable callable, SimpleLogger logger) { return executorService.submit( wrap(callable, logger, null, true) ); } /** * Wraps a callable for use by e.g. {@link #submit(ExecutorService, Callable, SimpleLogger)} and {@link #async(Callable, SimpleLogger)}. This means that current user and {@link MDC} will be restored * before {@link Callable#call()} * @since 5.6 */ default Callable wrap( @NonNull Callable callable, @Nullable SimpleLogger logger, @Nullable Boolean throwExceptions, boolean clearMDC) { final boolean throwExceptionsBoolean = throwExceptions == null ? logger == null : throwExceptions; Principal authentication; try { authentication = getAuthentication(); } catch(Exception e) { LoggerFactory.getLogger(getClass()).error(e.getMessage(), e); authentication = null; } final Principal onBehalfOf = authentication; final Map copy = MDC.getCopyOfContextMap(); if (logger != null) { logger.info("Executing on behalf of {}", onBehalfOf); } final Locale currentLocale = Locales.getDefault(); return () -> { MDC.clear(); // Running in an unknown thread, making sure MDC is clean try (AutoCloseable restore = Locales.with(currentLocale)){ if (onBehalfOf != null) { try { restoreAuthentication(onBehalfOf); } catch (Exception e) { if (logger != null) { logger.error(e.getMessage()); } LoggerFactory.getLogger(getClass()).error(e.getMessage(), e); } } if (copy != null) { // and make sure that copy.forEach(MDC::put); } return callable.call(); } catch (Exception e) { if (logger != null) { logger.error(e.getMessage(), e); } if (throwExceptionsBoolean) { throw e; } else { return null; } } finally { dropAuthentication(); } }; } default Logout restoringAutoClosable() { Principal onBehalfOf = getAuthentication(); if (onBehalfOf != null) { try { Object principal = onBehalfOf.getClass().getMethod("getPrincipal").invoke(onBehalfOf); try { principal = principal.getClass().getMethod("getUsername").invoke(principal); } catch(Exception ignored) { } MDCConstants.onBehalfOf(principal.toString()); } catch (Exception e) { MDCConstants.onBehalfOf(onBehalfOf.toString()); } } return new Logout() { @Override public void close() { try { restoreAuthentication(onBehalfOf); } finally { MDC.remove(ON_BEHALF_OF); } } }; } @Getter abstract class Logout implements AutoCloseable { public static Logout nop() { return new Logout() { @Override public void close() { } }; } /** * The user currently logged in and that will be logout by this. */ @MonotonicNonNull private S user; public Logout() { } @Override public abstract void close(); public Logout withUser(S user) { return new Logout<>() { { setUser(user); } @Override public void close() { Logout.this.close(); } }; } public void setUser(S user) { if (this.user != null) { throw new IllegalArgumentException(); } this.user = user; } } }