diff --git a/.changeset/nasty-dolphins-train.md b/.changeset/nasty-dolphins-train.md new file mode 100644 index 0000000..2fc6f7b --- /dev/null +++ b/.changeset/nasty-dolphins-train.md @@ -0,0 +1,5 @@ +--- +"server-sdk-kotlin": patch +--- + +Support array values in AccessToken's roomConfiguration diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4f99ec2..0c10e1d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -57,6 +57,9 @@ jobs: - name: Build with Gradle run: ./gradlew assemble + - name: Run unit tests (no livekit-server integration tests) + run: ./gradlew test --tests "io.livekit.server.AccessTokenTest" + - name: get version name if: github.event_name == 'push' run: echo "::set-output name=version_name::$(cat gradle.properties | grep VERSION_NAME | cut -d "=" -f2)" diff --git a/src/main/kotlin/io/livekit/server/AccessToken.kt b/src/main/kotlin/io/livekit/server/AccessToken.kt index ea2a781..7198c9d 100644 --- a/src/main/kotlin/io/livekit/server/AccessToken.kt +++ b/src/main/kotlin/io/livekit/server/AccessToken.kt @@ -33,10 +33,7 @@ import java.util.concurrent.TimeUnit * https://docs.livekit.io/home/get-started/authentication/ */ @Suppress("MemberVisibilityCanBePrivate", "unused") -class AccessToken( - private val apiKey: String, - private val secret: String -) { +class AccessToken(private val apiKey: String, private val secret: String) { private val videoGrants = mutableSetOf() private val sipGrants = mutableSetOf() @@ -57,33 +54,25 @@ class AccessToken( var expiration: Date? = null /** - * Date specifying the time [before which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.5). + * Date specifying the time + * [before which this token is invalid](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-4.1.5) + * . */ var notBefore: Date? = null - /** - * Display name for the participant, available as `Participant.name` - */ + /** Display name for the participant, available as `Participant.name` */ var name: String? = null - /** - * Unique identity of the user, required for room join tokens - */ + /** Unique identity of the user, required for room join tokens */ var identity: String? = null - /** - * Custom metadata to be passed to participants - */ + /** Custom metadata to be passed to participants */ var metadata: String? = null - /** - * For verifying integrity of message body - */ + /** For verifying integrity of message body */ var sha256: String? = null - /** - * Key/value attributes to attach to the participant - */ + /** Key/value attributes to attach to the participant */ val attributes = mutableMapOf() /** @@ -93,57 +82,43 @@ class AccessToken( */ var roomPreset: String? = null - /** - * Configuration for when creating a room. - */ + /** Configuration for when creating a room. */ var roomConfiguration: RoomConfiguration? = null - /** - * Add [VideoGrant] to this token. - */ + /** Add [VideoGrant] to this token. */ fun addGrants(vararg grants: VideoGrant) { for (grant in grants) { videoGrants.add(grant) } } - /** - * Add [VideoGrant] to this token. - */ + /** Add [VideoGrant] to this token. */ fun addGrants(grants: Iterable) { for (grant in grants) { videoGrants.add(grant) } } - /** - * Clear all previously added [VideoGrant]s. - */ + /** Clear all previously added [VideoGrant]s. */ fun clearGrants() { videoGrants.clear() } - /** - * Add [VideoGrant] to this token. - */ + /** Add [VideoGrant] to this token. */ fun addSIPGrants(vararg grants: SIPGrant) { for (grant in grants) { sipGrants.add(grant) } } - /** - * Add [VideoGrant] to this token. - */ + /** Add [VideoGrant] to this token. */ fun addSIPGrants(grants: Iterable) { for (grant in grants) { sipGrants.add(grant) } } - /** - * Clear all previously added [SIPGrant]s. - */ + /** Clear all previously added [SIPGrant]s. */ fun clearSIPGrants() { sipGrants.clear() } @@ -191,9 +166,7 @@ class AccessToken( claimsMap["video"] = videoGrantsMap claimsMap["sip"] = sipGrantsMap - claimsMap.forEach { (key, value) -> - withClaimAny(key, value) - } + claimsMap.forEach { (key, value) -> withClaimAny(key, value) } val alg = Algorithm.HMAC256(secret) @@ -214,22 +187,36 @@ internal fun JWTCreator.Builder.withClaimAny(name: String, value: Any) { is Instant -> withClaim(name, value) is List<*> -> withClaim(name, value) is Map<*, *> -> { - @Suppress("UNCHECKED_CAST") - withClaim(name, value as Map) + @Suppress("UNCHECKED_CAST") withClaim(name, value as Map) } } } -internal fun MessageOrBuilder.toMap(): Map { - val map = mutableMapOf() - +internal fun MessageOrBuilder.toMap(): Map = buildMap { for ((field, value) in allFields) { - if (value is MessageOrBuilder) { - map[field.name] = value.toMap() - } else { - map[field.name] = value - } + put( + field.name, + when (value) { + is MessageOrBuilder -> value.toMap() + is List<*> -> + value.map { item -> + when (item) { + is MessageOrBuilder -> item.toMap() + else -> if (isSupportedType(item)) item else item.toString() + } + } + else -> if (isSupportedType(value)) value else value.toString() + } + ) } - - return map } + +private fun isSupportedType(value: Any?) = + value == null || + value is Boolean || + value is Int || + value is Long || + value is Double || + value is String || + value is Map<*, *> || + value is List<*> diff --git a/src/test/kotlin/io/livekit/server/AccessTokenTest.kt b/src/test/kotlin/io/livekit/server/AccessTokenTest.kt index a98232d..bbb895b 100644 --- a/src/test/kotlin/io/livekit/server/AccessTokenTest.kt +++ b/src/test/kotlin/io/livekit/server/AccessTokenTest.kt @@ -18,6 +18,7 @@ package io.livekit.server import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm +import livekit.LivekitAgentDispatch import livekit.LivekitRoom.RoomConfiguration import org.junit.jupiter.api.Test import java.util.Date @@ -121,4 +122,45 @@ class AccessTokenTest { assertEquals(roomConfig.emptyTimeout, map["empty_timeout"]) assertEquals(roomConfig.egress.room.roomName, ((map["egress"] as Map<*, *>)["room"] as Map<*, *>)["room_name"]) } + + @Test + fun testArraysInRoomConfiguration() { + val roomConfig = with(RoomConfiguration.newBuilder()) { + name = "test_room" + addAgents( + LivekitAgentDispatch.RoomAgentDispatch.newBuilder() + .setAgentName("agent_name") + .setMetadata("metadata") + .build() + ) + build() + } + + val token = AccessToken(KEY, SECRET) + token.roomConfiguration = roomConfig + + // This should not throw an exception + val jwt = token.toJwt() + + // Verify the JWT can be decoded + val alg = Algorithm.HMAC256(SECRET) + val decodedJWT = JWT.require(alg) + .withIssuer(KEY) + .build() + .verify(jwt) + + // Verify the room configuration was properly encoded + val claims = decodedJWT.claims + val roomConfigMap = claims["roomConfig"]?.asMap() + assertNotNull(roomConfigMap) + + val agentsMap = roomConfigMap.get("agents") as? List<*> + assertNotNull(agentsMap) + assertEquals(1, agentsMap.size) + + val agentMap = agentsMap.first() as? Map<*, *> + assertNotNull(agentMap) + assertEquals("agent_name", agentMap.get("agent_name")) + assertEquals("metadata", agentMap.get("metadata")) + } }