diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt index 8c4aacf9..ee7db7f9 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnection.kt @@ -25,6 +25,7 @@ import io.grpc.ManagedChannel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.kuksa.authentication.JsonWebToken import org.eclipse.kuksa.extension.TAG import org.eclipse.kuksa.extension.copy import org.eclipse.kuksa.extension.datapoint @@ -40,6 +41,7 @@ import org.eclipse.kuksa.vsscore.model.VssProperty import org.eclipse.kuksa.vsscore.model.VssSpecification import org.eclipse.kuksa.vsscore.model.heritage import org.eclipse.kuksa.vsscore.model.vssProperties +import kotlin.properties.Delegates /** * The DataBrokerConnection holds an active connection to the DataBroker. The Connection can be use to interact with the @@ -54,8 +56,18 @@ class DataBrokerConnection internal constructor( ), private val dataBrokerSubscriber: DataBrokerSubscriber = DataBrokerSubscriber(dataBrokerTransporter), ) { + /** + * Used to register and unregister multiple [DisconnectListener]. + */ val disconnectListeners = MultiListener() + /** + * A JsonWebToken can be provided to authenticate against the DataBroker. + */ + var jsonWebToken: JsonWebToken? by Delegates.observable(null) { _, _, newValue -> + dataBrokerTransporter.jsonWebToken = newValue + } + init { val state = managedChannel.getState(false) managedChannel.notifyWhenStateChanged(state) { diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnector.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnector.kt index 565f84ac..9d885395 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnector.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerConnector.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.eclipse.kuksa.authentication.JsonWebToken import org.eclipse.kuksa.extension.TAG /** @@ -34,6 +35,7 @@ import org.eclipse.kuksa.extension.TAG */ class DataBrokerConnector @JvmOverloads constructor( private val managedChannel: ManagedChannel, + private val jsonWebToken: JsonWebToken? = null, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, ) { @@ -73,6 +75,9 @@ class DataBrokerConnector @JvmOverloads constructor( if (state == ConnectivityState.READY) { return@withContext DataBrokerConnection(managedChannel, defaultDispatcher) + .apply { + jsonWebToken = this@DataBrokerConnector.jsonWebToken + } } else { managedChannel.shutdownNow() throw DataBrokerException("timeout") diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt index 554ca496..f6d3482b 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/DataBrokerTransporter.kt @@ -28,6 +28,8 @@ import io.grpc.stub.StreamObserver import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.kuksa.authentication.JsonWebToken +import org.eclipse.kuksa.authentication.withAuthenticationInterceptor import org.eclipse.kuksa.extension.TAG import org.eclipse.kuksa.extension.applyDatapoint import org.eclipse.kuksa.proto.v1.KuksaValV1 @@ -49,6 +51,7 @@ internal class DataBrokerTransporter( private val managedChannel: ManagedChannel, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, ) { + init { val state = managedChannel.getState(false) check(state == ConnectivityState.READY) { @@ -56,6 +59,11 @@ internal class DataBrokerTransporter( } } + /** + * A JsonWebToken can be provided to authenticate against the DataBroker. + */ + var jsonWebToken: JsonWebToken? = null + /** * Sends a request to the DataBroker to respond with the specified [vssPath] and [fields] values. * @@ -76,7 +84,9 @@ internal class DataBrokerTransporter( .build() return@withContext try { - blockingStub.get(request) + blockingStub + .withAuthenticationInterceptor(jsonWebToken) + .get(request) } catch (e: StatusRuntimeException) { throw DataBrokerException(e.message, e) } @@ -114,7 +124,9 @@ internal class DataBrokerTransporter( .build() return@withContext try { - blockingStub.set(request) + blockingStub + .withAuthenticationInterceptor(jsonWebToken) + .set(request) } catch (e: StatusRuntimeException) { throw DataBrokerException(e.message, e) } @@ -171,7 +183,9 @@ internal class DataBrokerTransporter( cancellableContext.run { try { - asyncStub.subscribe(request, streamObserver) + asyncStub + .withAuthenticationInterceptor(jsonWebToken) + .subscribe(request, streamObserver) } catch (e: StatusRuntimeException) { throw DataBrokerException(e.message, e) } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/JsonWebToken.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/JsonWebToken.kt new file mode 100644 index 00000000..bed3feba --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/JsonWebToken.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa.authentication + +/** + * A JsonWebToken can be used to authenticate against the DataBroker. For authentication to work the DataBroker must be + * started with authentication enabled first. + * The JsonWebToken is defined by an [authScheme] and [token]. The [authScheme] is set to "Bearer". The [token] should + * contain a valid JsonWebToken. + * + * It will be send to the DataBroker as part of the Header Metadata in the following format: + * + * Headers + * Authorization: [authScheme] [token] + * + */ +data class JsonWebToken( + val token: String, +) { + val authScheme: String + get() = DEFAULT_AUTH_SCHEME + + private companion object { + private const val DEFAULT_AUTH_SCHEME = "Bearer" + } +} diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/VALStubExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/VALStubExtension.kt new file mode 100644 index 00000000..7150f76b --- /dev/null +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/authentication/VALStubExtension.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.eclipse.kuksa.authentication + +import com.google.common.net.HttpHeaders +import io.grpc.ClientInterceptor +import io.grpc.Metadata +import io.grpc.stub.MetadataUtils +import org.eclipse.kuksa.proto.v1.VALGrpc.VALBlockingStub +import org.eclipse.kuksa.proto.v1.VALGrpc.VALStub + +internal fun VALBlockingStub.withAuthenticationInterceptor(jsonWebToken: JsonWebToken?): VALBlockingStub { + if (jsonWebToken == null) return this + + val authenticationInterceptor = clientInterceptor(jsonWebToken) + return withInterceptors(authenticationInterceptor) +} + +internal fun VALStub.withAuthenticationInterceptor(jsonWebToken: JsonWebToken?): VALStub { + if (jsonWebToken == null) return this + + val authenticationInterceptor = clientInterceptor(jsonWebToken) + return withInterceptors(authenticationInterceptor) +} + +private fun clientInterceptor(jsonWebToken: JsonWebToken): ClientInterceptor? { + val authorizationHeader = Metadata.Key.of(HttpHeaders.AUTHORIZATION, Metadata.ASCII_STRING_MARSHALLER) + + val metadata = Metadata() + metadata.put(authorizationHeader, "${jsonWebToken.authScheme} ${jsonWebToken.token}") + + return MetadataUtils.newAttachHeadersInterceptor(metadata) +}