Skip to content

Commit ca29d5c

Browse files
adrwAlexSzlavik
andauthored
Add AllowAnyUser annotation and wildcards in access annotations (#3213)
* Add AllowAnyUser annotation and wildcards in access annotations * Move to explicit boolean for allowAny* * Add missing test cases, cleanup AllowAnyUser annotation * Update API * Accept breaking API change --------- Co-authored-by: Alex Szlavik <[email protected]>
1 parent 9331701 commit ca29d5c

File tree

8 files changed

+116
-13
lines changed

8 files changed

+116
-13
lines changed

.palantir/revapi.yml

+42
Original file line numberDiff line numberDiff line change
@@ -6192,6 +6192,48 @@ acceptedBreaks:
61926192
old: "parameter void misk.redis.RealPipelinedRedis::<init>(===misk.redis.JedisPipeline===)"
61936193
new: "parameter void misk.redis.RealPipelinedRedis::<init>(===redis.clients.jedis.AbstractPipeline===)"
61946194
justification: "RealPipelinedRedis is internal"
6195+
"2024.03.26.112919-01b20b7":
6196+
com.squareup.misk:misk-redis:
6197+
- code: "java.method.addedToInterface"
6198+
new: "method java.util.function.Supplier<java.lang.Double> misk.redis.DeferredRedis::zscore(java.lang.String,\
6199+
\ java.lang.String)"
6200+
justification: "Additive change only"
6201+
- code: "java.method.addedToInterface"
6202+
new: "method java.util.function.Supplier<java.lang.Long> misk.redis.DeferredRedis::zadd(java.lang.String,\
6203+
\ double, java.lang.String, misk.redis.Redis.ZAddOptions[])"
6204+
justification: "Additive change only"
6205+
- code: "java.method.addedToInterface"
6206+
new: "method java.util.function.Supplier<java.lang.Long> misk.redis.DeferredRedis::zadd(java.lang.String,\
6207+
\ java.util.Map<java.lang.String, java.lang.Double>, misk.redis.Redis.ZAddOptions[])"
6208+
justification: "Additive change only"
6209+
- code: "java.method.addedToInterface"
6210+
new: "method java.util.function.Supplier<java.lang.Long> misk.redis.DeferredRedis::zcard(java.lang.String)"
6211+
justification: "Additive change only"
6212+
- code: "java.method.addedToInterface"
6213+
new: "method java.util.function.Supplier<java.lang.Long> misk.redis.DeferredRedis::zremRangeByRank(java.lang.String,\
6214+
\ misk.redis.Redis.ZRangeRankMarker, misk.redis.Redis.ZRangeRankMarker)"
6215+
justification: "Additive change only"
6216+
- code: "java.method.addedToInterface"
6217+
new: "method java.util.function.Supplier<java.util.List<kotlin.Pair<okio.ByteString,\
6218+
\ java.lang.Double>>> misk.redis.DeferredRedis::zrangeWithScores(java.lang.String,\
6219+
\ misk.redis.Redis.ZRangeType, misk.redis.Redis.ZRangeMarker, misk.redis.Redis.ZRangeMarker,\
6220+
\ boolean, misk.redis.Redis.ZRangeLimit)"
6221+
justification: "Additive change only"
6222+
- code: "java.method.addedToInterface"
6223+
new: "method java.util.function.Supplier<java.util.List<okio.ByteString>> misk.redis.DeferredRedis::zrange(java.lang.String,\
6224+
\ misk.redis.Redis.ZRangeType, misk.redis.Redis.ZRangeMarker, misk.redis.Redis.ZRangeMarker,\
6225+
\ boolean, misk.redis.Redis.ZRangeLimit)"
6226+
justification: "Additive change only"
6227+
"2024.04.05.204820-9331701":
6228+
com.squareup.misk:misk:
6229+
- code: "java.method.numberOfParametersChanged"
6230+
old: "method misk.security.authz.AccessAnnotationEntry misk.security.authz.AccessAnnotationEntry::copy(kotlin.reflect.KClass<?\
6231+
\ extends java.lang.annotation.Annotation>, java.util.List<java.lang.String>,\
6232+
\ java.util.List<java.lang.String>)"
6233+
new: "method misk.security.authz.AccessAnnotationEntry misk.security.authz.AccessAnnotationEntry::copy(kotlin.reflect.KClass<?\
6234+
\ extends java.lang.annotation.Annotation>, java.util.List<java.lang.String>,\
6235+
\ java.util.List<java.lang.String>, boolean, boolean)"
6236+
justification: "New parameters are optional"
61956237
misk-0.18.0:
61966238
com.squareup.misk:misk-gcp:
61976239
- code: "java.method.numberOfParametersChanged"

misk-actions/api/misk-actions.api

+2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ public abstract interface annotation class misk/security/authz/AllowAnyService :
135135
}
136136

137137
public abstract interface annotation class misk/security/authz/Authenticated : java/lang/annotation/Annotation {
138+
public abstract fun allowAnyService ()Z
139+
public abstract fun allowAnyUser ()Z
138140
public abstract fun capabilities ()[Ljava/lang/String;
139141
public abstract fun services ()[Ljava/lang/String;
140142
}

misk-actions/src/main/kotlin/misk/security/authz/Authenticated.kt

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ annotation class Authenticated(
1111
val services: Array<String> = [],
1212

1313
/** Calling users must have at least one of these capabilities to be authenticated */
14-
val capabilities: Array<String> = []
14+
val capabilities: Array<String> = [],
15+
16+
/** Allow any service to be authenticated. */
17+
val allowAnyService: Boolean = false,
18+
19+
/** Allow any user to be authenticated. */
20+
val allowAnyUser: Boolean = false
1521
)
1622

1723
/**
@@ -24,6 +30,7 @@ annotation class Unauthenticated
2430
/**
2531
* Annotation indicating that any authenticated service is allowed to access this endpoint.
2632
*/
33+
@Deprecated("Use Authenticated(allowAnyService = true) instead.")
2734
@Retention(AnnotationRetention.RUNTIME)
2835
@Target(AnnotationTarget.FUNCTION)
2936
annotation class AllowAnyService

misk/api/misk.api

+10-4
Original file line numberDiff line numberDiff line change
@@ -948,13 +948,19 @@ public final class misk/security/authz/AccessAnnotationEntry {
948948
public fun <init> (Lkotlin/reflect/KClass;)V
949949
public fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;)V
950950
public fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;)V
951-
public synthetic fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
951+
public fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;Z)V
952+
public fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ZZ)V
953+
public synthetic fun <init> (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
952954
public final fun component1 ()Lkotlin/reflect/KClass;
953955
public final fun component2 ()Ljava/util/List;
954956
public final fun component3 ()Ljava/util/List;
955-
public final fun copy (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;)Lmisk/security/authz/AccessAnnotationEntry;
956-
public static synthetic fun copy$default (Lmisk/security/authz/AccessAnnotationEntry;Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lmisk/security/authz/AccessAnnotationEntry;
957+
public final fun component4 ()Z
958+
public final fun component5 ()Z
959+
public final fun copy (Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ZZ)Lmisk/security/authz/AccessAnnotationEntry;
960+
public static synthetic fun copy$default (Lmisk/security/authz/AccessAnnotationEntry;Lkotlin/reflect/KClass;Ljava/util/List;Ljava/util/List;ZZILjava/lang/Object;)Lmisk/security/authz/AccessAnnotationEntry;
957961
public fun equals (Ljava/lang/Object;)Z
962+
public final fun getAllowAnyService ()Z
963+
public final fun getAllowAnyUser ()Z
958964
public final fun getAnnotation ()Lkotlin/reflect/KClass;
959965
public final fun getCapabilities ()Ljava/util/List;
960966
public final fun getServices ()Ljava/util/List;
@@ -969,7 +975,7 @@ public final class misk/security/authz/AccessControlModule : misk/scope/ActionSc
969975

970976
public final class misk/security/authz/AccessInterceptor : misk/ApplicationInterceptor {
971977
public static final field Companion Lmisk/security/authz/AccessInterceptor$Companion;
972-
public synthetic fun <init> (Ljava/util/Set;Ljava/util/Set;Lmisk/scope/ActionScoped;ZLjava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
978+
public synthetic fun <init> (Ljava/util/Set;Ljava/util/Set;Lmisk/scope/ActionScoped;ZLjava/util/Set;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
973979
public final fun getAllowedCapabilities ()Ljava/util/Set;
974980
public final fun getAllowedServices ()Ljava/util/Set;
975981
public fun intercept (Lmisk/Chain;)Ljava/lang/Object;

misk/src/main/kotlin/misk/security/authz/AccessAnnotationEntry.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ import kotlin.reflect.KClass
4242
data class AccessAnnotationEntry @JvmOverloads constructor(
4343
val annotation: KClass<out Annotation>,
4444
val services: List<String> = listOf(),
45-
val capabilities: List<String> = listOf()
45+
val capabilities: List<String> = listOf(),
46+
val allowAnyService: Boolean = false,
47+
val allowAnyUser: Boolean = false,
4648
)
4749

4850
inline fun <reified T : Annotation> AccessAnnotationEntry(

misk/src/main/kotlin/misk/security/authz/AccessInterceptor.kt

+18-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class AccessInterceptor private constructor(
1616
val allowedCapabilities: Set<String>,
1717
private val caller: ActionScoped<MiskCaller?>,
1818
private val allowAnyService: Boolean,
19-
private val excludeFromAllowAnyService: Set<String>
19+
private val excludeFromAllowAnyService: Set<String>,
20+
private val allowAnyUser: Boolean,
2021
) : ApplicationInterceptor {
2122
override fun intercept(chain: Chain): Any {
2223
val caller = caller.get() ?: throw UnauthenticatedException()
@@ -31,12 +32,16 @@ class AccessInterceptor private constructor(
3132
/** Check whether the caller is allowed to access this endpoint */
3233
private fun isAuthorized(caller: MiskCaller): Boolean {
3334
// Allow if we don't have any requirements on service or capability
34-
if (allowedServices.isEmpty() && allowedCapabilities.isEmpty() && !allowAnyService) return true
35+
if (allowedServices.isEmpty() && allowedCapabilities.isEmpty() && !allowAnyService && !allowAnyUser) return true
3536

3637
if (allowAnyService && caller.service != null && !excludeFromAllowAnyService.contains(caller.service)) {
3738
return true
3839
}
3940

41+
if (allowAnyUser && caller.user != null) {
42+
return true
43+
}
44+
4045
// Allow if the caller has provided an allowed capability
4146
return caller.hasCapability(allowedCapabilities) || caller.isService(allowedServices)
4247
}
@@ -70,10 +75,11 @@ class AccessInterceptor private constructor(
7075
return null
7176
}
7277

73-
val allowAnyService = action.hasAnnotation<AllowAnyService>()
78+
val allowAnyService = action.hasAnnotation<AllowAnyService>() || actionEntries.any { it.allowAnyService }
79+
val allowAnyUser = actionEntries.any { it.allowAnyUser }
7480

7581
// No access annotations. Fail with a useful message.
76-
check(allowAnyService || actionEntries.isNotEmpty()) {
82+
check(allowAnyService || allowAnyUser || actionEntries.isNotEmpty()) {
7783
val requiredAnnotations = mutableListOf<KClass<out Annotation>>()
7884
requiredAnnotations += Authenticated::class
7985
requiredAnnotations += Unauthenticated::class
@@ -114,7 +120,7 @@ class AccessInterceptor private constructor(
114120
val allowedServices = actionEntries.flatMap { it.services }.toSet()
115121
val allowedCapabilities = actionEntries.flatMap { it.capabilities }.toSet()
116122

117-
if (!allowAnyService && allowedServices.isEmpty() && allowedCapabilities.isEmpty()) {
123+
if (!allowAnyService && allowedServices.isEmpty() && !allowAnyUser && allowedCapabilities.isEmpty()) {
118124
logger.warn { "${action.name}::${action.function.name}() has an empty set of allowed services and capabilities. This method of allowing all services and users is deprecated."}
119125
}
120126

@@ -123,12 +129,17 @@ class AccessInterceptor private constructor(
123129
allowedCapabilities,
124130
caller,
125131
allowAnyService,
126-
excludeFromAllowAnyService.toSet()
132+
excludeFromAllowAnyService.toSet(),
133+
allowAnyUser,
127134
)
128135
}
129136

130137
private fun Authenticated.toAccessAnnotationEntry() = AccessAnnotationEntry(
131-
Authenticated::class, services.toList(), capabilities.toList()
138+
annotation = Authenticated::class,
139+
services = services.toList(),
140+
capabilities = capabilities.toList(),
141+
allowAnyService = allowAnyService,
142+
allowAnyUser = allowAnyUser
132143
)
133144

134145
private inline fun <reified T : Annotation> Action.hasAnnotation() =

misk/src/test/kotlin/misk/web/actions/AuthenticationTest.kt

+22
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,28 @@ class AuthenticationTest {
249249
.isEqualTo("unauthorized")
250250
}
251251

252+
@Test fun testAllowAnyUser() {
253+
val caller = MiskCaller(user = "sandy")
254+
assertThat(
255+
executeRequest(
256+
path = "/allow_any_user_access",
257+
user = caller.user
258+
)
259+
)
260+
.isEqualTo("$caller authorized as any user")
261+
}
262+
263+
@Test fun testAllowAnyUserDenyServiceOnly() {
264+
val caller = MiskCaller(service = "test")
265+
assertThat(
266+
executeRequest(
267+
path = "/allow_any_user_access",
268+
user = caller.user
269+
)
270+
)
271+
.isEqualTo("unauthenticated")
272+
}
273+
252274
private class MixesUnauthenticatedWithOtherAnnotations @Inject constructor() : WebAction {
253275
@Get("/oops")
254276
@Unauthenticated

misk/src/test/kotlin/misk/web/actions/TestWebActionModule.kt

+11
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class TestWebActionModule : KAbstractModule() {
4949
install(WebActionModule.create<EmptyAuthenticatedAccessAction>())
5050
install(WebActionModule.create<AllowAnyServiceAccessAction>())
5151
install(WebActionModule.create<AllowAnyServicePlusAuthenticatedAccessAction>())
52+
install(WebActionModule.create<AllowAnyUserAccessAction>())
5253

5354
multibind<AccessAnnotationEntry>().toInstance(
5455
AccessAnnotationEntry<CustomServiceAccess>(services = listOf("payments"))
@@ -187,3 +188,13 @@ class AllowAnyServicePlusAuthenticatedAccessAction @Inject constructor() : WebAc
187188
@Authenticated(services = ["web-proxy"], capabilities = ["admin"])
188189
fun get() = "${scopedCaller.get()} authorized as any service".toResponseBody()
189190
}
191+
192+
class AllowAnyUserAccessAction @Inject constructor() : WebAction {
193+
@Inject
194+
lateinit var scopedCaller: ActionScoped<MiskCaller?>
195+
196+
@Get("/allow_any_user_access")
197+
@ResponseContentType(MediaTypes.TEXT_PLAIN_UTF8)
198+
@Authenticated(allowAnyUser = true)
199+
fun get() = "${scopedCaller.get()} authorized as any user".toResponseBody()
200+
}

0 commit comments

Comments
 (0)