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}