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; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import java.nio.CharBuffer; 025import java.nio.charset.CodingErrorAction; 026import java.nio.charset.CharacterCodingException; 027import java.nio.charset.StandardCharsets; 028import java.util.Optional; 029 030import static java.util.Objects.requireNonNull; 031 032/** 033 * Shallowly immutable application-supplied identity and stable, opaque 034 * partition keys. The carrier itself is safe for concurrent access; 035 * applications retain responsibility for the thread-safety of principal and 036 * application-context objects they place inside it. 037 * 038 * <p>Instances intentionally retain reference identity. Principal and 039 * application-context objects are opaque security capabilities and do not 040 * participate in framework-defined structural equality. Authorization code 041 * must compare the documented stable partition keys or apply its own 042 * principal policy explicitly. 043 * 044 * @author <a href="https://www.revetkn.com">Mark Allen</a> 045 */ 046@ThreadSafe 047public final class McpAdmissionIdentity { 048 /** Maximum UTF-8 encoding length of either partition key. */ 049 @NonNull 050 public static final Integer MAXIMUM_PARTITION_KEY_SIZE_IN_UTF_8_BYTES = 256; 051 @NonNull 052 private static final McpAdmissionIdentity ANONYMOUS = 053 new Builder("anonymous").build(); 054 @NonNull 055 private final String rateLimitPartitionKey; 056 @Nullable 057 private final String authorizationPartitionKey; 058 @Nullable 059 private final Object principal; 060 @Nullable 061 private final Object applicationContext; 062 063 /** 064 * Returns the canonical anonymous identity. 065 * 066 * @return shared anonymous identity 067 */ 068 @NonNull 069 public static McpAdmissionIdentity anonymousInstance() { 070 return ANONYMOUS; 071 } 072 073 /** 074 * Vends an identity builder primed with a stable rate-limit partition key. 075 * 076 * @param rateLimitPartitionKey opaque, fleet-stable partition key 077 * @return identity builder 078 */ 079 @NonNull 080 public static Builder withRateLimitPartitionKey(@NonNull String rateLimitPartitionKey) { 081 return new Builder(rateLimitPartitionKey); 082 } 083 084 private McpAdmissionIdentity(@NonNull Builder builder) { 085 this.rateLimitPartitionKey = requirePartitionKey( 086 builder.rateLimitPartitionKey, "rateLimitPartitionKey"); 087 this.authorizationPartitionKey = builder.authorizationPartitionKey == null 088 ? null : requirePartitionKey(builder.authorizationPartitionKey, 089 "authorizationPartitionKey"); 090 this.principal = builder.principal; 091 this.applicationContext = builder.applicationContext; 092 if (this.principal != null && this.authorizationPartitionKey == null) 093 throw new IllegalStateException( 094 "authorizationPartitionKey is required when principal is present"); 095 } 096 097 /** @return whether a principal is present */ 098 @NonNull 099 public Boolean isAuthenticated() { 100 return this.principal != null; 101 } 102 103 /** @return application principal, when authenticated */ 104 @NonNull 105 public Optional<@NonNull Object> getPrincipal() { 106 return Optional.ofNullable(this.principal); 107 } 108 109 /** @return optional application context propagated to later MCP hooks */ 110 @NonNull 111 public Optional<@NonNull Object> getApplicationContext() { 112 return Optional.ofNullable(this.applicationContext); 113 } 114 115 /** @return opaque rate-limit partition key */ 116 @NonNull 117 public String getRateLimitPartitionKey() { 118 return this.rateLimitPartitionKey; 119 } 120 121 /** 122 * Returns the opaque authorization partition key. Absence selects Soklet's 123 * endpoint-scoped anonymous authorization partition. 124 * 125 * @return authorization partition key, when supplied 126 */ 127 @NonNull 128 public Optional<@NonNull String> getAuthorizationPartitionKey() { 129 return Optional.ofNullable(this.authorizationPartitionKey); 130 } 131 132 @NonNull 133 private static String requirePartitionKey(@NonNull String value, 134 @NonNull String name) { 135 requireNonNull(value, name); 136 if (value.isBlank()) 137 throw new IllegalArgumentException(name + " must not be blank"); 138 try { 139 int length = StandardCharsets.UTF_8.newEncoder() 140 .onMalformedInput(CodingErrorAction.REPORT) 141 .onUnmappableCharacter(CodingErrorAction.REPORT) 142 .encode(CharBuffer.wrap(value)).remaining(); 143 if (length > MAXIMUM_PARTITION_KEY_SIZE_IN_UTF_8_BYTES) 144 throw new IllegalArgumentException(name + " must contain at most " 145 + MAXIMUM_PARTITION_KEY_SIZE_IN_UTF_8_BYTES + " UTF-8 bytes"); 146 } catch (CharacterCodingException exception) { 147 throw new IllegalArgumentException(name + " must contain valid Unicode text", exception); 148 } 149 return value; 150 } 151 152 /** 153 * Mutable builder for an immutable {@link McpAdmissionIdentity}. 154 * 155 * @author <a href="https://www.revetkn.com">Mark Allen</a> 156 */ 157 @NotThreadSafe 158 public static final class Builder { 159 @NonNull 160 private final String rateLimitPartitionKey; 161 @Nullable 162 private String authorizationPartitionKey; 163 @Nullable 164 private Object principal; 165 @Nullable 166 private Object applicationContext; 167 168 private Builder(@NonNull String rateLimitPartitionKey) { 169 this.rateLimitPartitionKey = requireNonNull(rateLimitPartitionKey); 170 } 171 172 /** 173 * Sets the stable authorization partition key. 174 * 175 * @param authorizationPartitionKey opaque authorization partition 176 * @return this builder 177 */ 178 @NonNull 179 public Builder authorizationPartitionKey(@NonNull String authorizationPartitionKey) { 180 this.authorizationPartitionKey = requireNonNull(authorizationPartitionKey); 181 return this; 182 } 183 184 /** 185 * Sets the authenticated application principal. An authorization partition 186 * key must also be supplied. 187 * 188 * @param principal application principal 189 * @return this builder 190 */ 191 @NonNull 192 public Builder principal(@NonNull Object principal) { 193 this.principal = requireNonNull(principal); 194 return this; 195 } 196 197 /** 198 * Sets optional application context propagated to later MCP hooks. 199 * 200 * @param applicationContext application context 201 * @return this builder 202 */ 203 @NonNull 204 public Builder applicationContext(@NonNull Object applicationContext) { 205 this.applicationContext = requireNonNull(applicationContext); 206 return this; 207 } 208 209 /** @return immutable admission identity */ 210 @NonNull 211 public McpAdmissionIdentity build() { 212 return new McpAdmissionIdentity(this); 213 } 214 } 215}