001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet; 018 019import org.jspecify.annotations.NonNull; 020 021import javax.annotation.concurrent.ThreadSafe; 022 023import static java.util.Objects.requireNonNull; 024 025/** 026 * Thread-safe application authorization policy for establishing and renewing 027 * MCP subscriptions. Soklet may invoke one authorizer concurrently for 028 * independent subscriptions, but runs at most one authorization callback at a 029 * time for each subscription. 030 * <p> 031 * Authorization is distinct from initial request admission and catalog-item 032 * access. Implementations establish a bounded whole-subscription grant from 033 * current application state. A thrown exception or {@code null} result fails 034 * closed. Soklet invokes callbacks through bounded application execution, 035 * never on an event loop. Invocation features contain the check's cooperative 036 * cancellation token; they do not expose task creation, progress reporting, or 037 * a localization context computed before authorization completes. 038 * <p> 039 * For an established {@code 2026-07-28} listen, denial, expiry or a failed 040 * required check fences delivery and ends the subscription. A usable stream 041 * receives its tagged completion result before closing; diagnostics retain 042 * the authorization reason. A broken writer or partially written revoked 043 * frame may close abruptly. Initial denial still rejects before acknowledgment. 044 * 045 * <p>For exact 2025-era delivery, the authorizer establishes and renews one 046 * session-owned resource URI grant through a real {@code resources/subscribe} 047 * context. That grant is independent of an individual GET connection and 048 * survives its disconnect within configured bounds. GET notification-family 049 * admission is controlled separately by {@link McpSessionTransportAdmissionController}. 050 * Unsubscribe does not invoke this authorizer. 051 * A transient renewal exception or timeout fences delivery and permits at most 052 * three consecutive failed attempts within the existing lease. Successful 053 * authorization resets this retry count. Explicit renewal denial, exhausted 054 * retries, or an expired established grant retires its session: the client must 055 * reinitialize and resubscribe. Legacy protocols have no subscription-ended 056 * notification. Initial subscribe rejection and explicit unsubscribe do not 057 * themselves retire a session. 058 * 059 * @author <a href="https://www.revetkn.com">Mark Allen</a> 060 */ 061@ThreadSafe 062@FunctionalInterface 063public interface McpSubscriptionAuthorizer { 064 /** 065 * Authorizes one initial, renewal, or reconciliation check. 066 * 067 * @param subscriptionAuthorizationContext immutable authorization context 068 * @param invocationFeatures features scoped to this authorization check; 069 * includes its cancellation token 070 * @return a non-null allowed or denied authorization result 071 * @throws Exception if authorization cannot be established; Soklet fails 072 * the check closed 073 */ 074 @NonNull 075 McpSubscriptionAuthorization authorize( 076 @NonNull McpSubscriptionAuthorizationContext 077 subscriptionAuthorizationContext, 078 @NonNull McpInvocationFeatures invocationFeatures) throws Exception; 079 080 /** 081 * Returns the shared authorizer that denies every authorization check. 082 * 083 * @return shared deny-all authorizer 084 */ 085 @NonNull 086 static McpSubscriptionAuthorizer denyAllInstance() { 087 return DenyAllMcpSubscriptionAuthorizer.INSTANCE; 088 } 089} 090 091/** 092 * Thread-safe deny-all MCP subscription authorizer. 093 * 094 * @author <a href="https://www.revetkn.com">Mark Allen</a> 095 */ 096@ThreadSafe 097final class DenyAllMcpSubscriptionAuthorizer 098 implements McpSubscriptionAuthorizer { 099 @NonNull 100 static final DenyAllMcpSubscriptionAuthorizer INSTANCE = 101 new DenyAllMcpSubscriptionAuthorizer(); 102 103 private DenyAllMcpSubscriptionAuthorizer() { 104 } 105 106 @Override 107 @NonNull 108 public McpSubscriptionAuthorization authorize( 109 @NonNull McpSubscriptionAuthorizationContext 110 subscriptionAuthorizationContext, 111 @NonNull McpInvocationFeatures invocationFeatures) { 112 requireNonNull(subscriptionAuthorizationContext); 113 requireNonNull(invocationFeatures); 114 return McpSubscriptionAuthorization.deniedInstance(); 115 } 116}