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 — 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}