UserController.java

/*
 * Copyright (C) 2023 B3Partners B.V.
 *
 * SPDX-License-Identifier: MIT
 */
package org.tailormap.api.controller;

import java.io.Serializable;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.tailormap.api.persistence.Upload;
import org.tailormap.api.persistence.helper.UploadHelper;
import org.tailormap.api.security.OIDCRepository;
import org.tailormap.api.security.TailormapAdditionalProperty;
import org.tailormap.api.security.TailormapUserDetails;
import org.tailormap.api.viewer.model.AdditionalProperty;
import org.tailormap.api.viewer.model.LoginConfiguration;
import org.tailormap.api.viewer.model.LoginConfigurationSsoLinksInner;
import org.tailormap.api.viewer.model.UserResponse;

/** Provides user and login information */
@RestController
public class UserController {
  private final OIDCRepository oidcRepository;
  private final UploadHelper uploadHelper;

  @Value("${tailormap-api.password-reset.enabled:false}")
  private boolean passwordResetEnabled;

  public UserController(OIDCRepository oidcRepository, UploadHelper uploadHelper) {
    this.oidcRepository = oidcRepository;
    this.uploadHelper = uploadHelper;
  }

  /**
   * Get user login information.
   *
   * @return isAuthenticated, username, roles
   */
  @GetMapping(path = "${tailormap-api.base-path}/user", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<Serializable> getUser() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    boolean isAuthenticated = authentication != null && !(authentication instanceof AnonymousAuthenticationToken);

    UserResponse userResponse = new UserResponse().isAuthenticated(isAuthenticated);
    if (isAuthenticated) {
      userResponse.username(authentication.getName());
      userResponse.setRoles(authentication.getAuthorities().stream()
          .map(GrantedAuthority::getAuthority)
          .collect(Collectors.toSet()));

      if (authentication.getPrincipal() instanceof TailormapUserDetails userProperties) {
        userResponse.setOrganisation(userProperties.getOrganisation());

        // Public user and group properties are meant for a (modified) frontend to implement custom
        // logic depending on who's logged in. When used for authorization for something, the check
        // should also be performed server-side, possibly in an extra microservice.

        // Authentication may be external (OIDC), and a user may not exist in the Tailormap database.
        // For users from the Tailormap database, we support additional user properties. Only return
        // public ones to the frontend.

        // Group properties are also supported for OIDC logins with roles that map to groups in the
        // Tailormap database.

        Function<TailormapAdditionalProperty, AdditionalProperty> mapToPublicProperty =
            ap -> new AdditionalProperty().key(ap.key()).value(ap.value());
        userProperties.getAdditionalProperties().stream()
            .filter(TailormapAdditionalProperty::isPublic)
            .map(mapToPublicProperty)
            .forEach(userResponse::addPropertiesItem);

        userProperties.getAdditionalGroupProperties().stream()
            .filter(TailormapAdditionalProperty::isPublic)
            .map(mapToPublicProperty)
            .forEach(userResponse::addGroupPropertiesItem);
      }
    }
    return ResponseEntity.status(HttpStatus.OK).body(userResponse);
  }

  @GetMapping(path = "${tailormap-api.base-path}/login/configuration", produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<LoginConfiguration> getLoginConfiguration() {
    LoginConfiguration result = new LoginConfiguration();

    for (ClientRegistration reg : oidcRepository) {
      OIDCRepository.OIDCRegistrationMetadata metadata =
          oidcRepository.getMetadataForRegistrationId(reg.getRegistrationId());
      result.addSsoLinksItem(new LoginConfigurationSsoLinksInner()
          .name(reg.getClientName())
          .url("/api/oauth2/authorization/" + reg.getRegistrationId())
          .showForViewer(metadata.getShowForViewer())
          .image(uploadHelper.getUrlForImage(metadata.getImage(), Upload.CATEGORY_SSO_IMAGE)));
    }

    result.enablePasswordReset(passwordResetEnabled);

    return ResponseEntity.status(HttpStatus.OK).body(result);
  }
}