001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.auth;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.security.*;
022import java.util.*;
023import java.util.logging.Level;
024import java.util.logging.Logger;
025
026/**
027 * Mutable helper that folds a sequence of {@link AuthResult}s into a single result, honoring
028 * {@link AuthResult.MergeMode}.
029 *
030 * <p>
031 * {@link AuthResult.MergeMode#ADD ADD} keeps the first non-<jk>null</jk> principal and unions roles from
032 * later results <b>only when</b> those results carry no principal of their own or the <b>same</b> principal
033 * (compared by {@link Principal#getName()}, null-safe).  A later result with a <i>different</i> non-<jk>null</jk>
034 * principal does not contribute its roles &mdash; a second authenticated identity must not silently elevate the
035 * first.  {@link AuthResult.MergeMode#REPLACE REPLACE} resets the accumulated principal + roles.  Used by both
036 * {@link AuthFilterChain} and the resource-level fold in {@link org.apache.juneau.rest.server.RestContext}.
037 *
038 * <h5 class='section'>See Also:</h5><ul>
039 *    <li class='jc'>{@link AuthResult}
040 *    <li class='jc'>{@link AuthFilterChain}
041 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerAuthenticator">REST Authenticator — Resource-Level Authentication</a>
042 * </ul>
043 *
044 * @since 10.0.0
045 */
046public final class AuthResultAccumulator {
047
048   private static final Logger LOG = Logger.getLogger(AuthResultAccumulator.class.getName());
049
050   private Principal principal;
051   private final Set<String> roles = new LinkedHashSet<>();
052   private boolean any;
053
054   /**
055    * Folds the next result into this accumulator.
056    *
057    * @param r The result to fold in. <jk>null</jk> is ignored.
058    * @return This object.
059    */
060   public AuthResultAccumulator add(AuthResult r) {
061      if (r == null)
062         return this;
063      any = true;
064      if (r.getMode() == AuthResult.MergeMode.REPLACE) {
065         principal = r.getPrincipal();
066         roles.clear();
067         roles.addAll(r.getRoles());
068         return this;
069      }
070      if (principal == null) {
071         principal = r.getPrincipal();  // may still be null (roles-only)
072         roles.addAll(r.getRoles());
073         return this;
074      }
075      var otherPrincipal = r.getPrincipal();
076      if (otherPrincipal == null || eq(principal.getName(), otherPrincipal.getName())) {
077         roles.addAll(r.getRoles());
078      } else {
079         var establishedName = principal.getName();
080         LOG.log(Level.WARNING, () -> "Ignoring roles from a distinct authenticated principal ('"
081            + otherPrincipal.getName() + "'); request identity remains '" + establishedName + "'.");
082      }
083      return this;
084   }
085
086   /**
087    * Returns the folded result.
088    *
089    * @return The folded result, or {@link Optional#empty()} if nothing was added or no principal was ever resolved.
090    */
091   public Optional<AuthResult> result() {
092      if (! any || principal == null)
093         return oe();
094      return o(AuthResult.of(principal, roles));
095   }
096}