@@ -31,7 +31,9 @@ import com.google.api.client.json.jackson2.JacksonFactory
3131import com.google.api.services.chat.v1.HangoutsChat
3232import com.google.auth.http.HttpCredentialsAdapter
3333import com.google.auth.oauth2.GoogleCredentials
34+ import com.google.auth.oauth2.ImpersonatedCredentials
3435import com.google.auth.oauth2.ServiceAccountCredentials
36+ import mu.KotlinLogging
3537import java.io.ByteArrayInputStream
3638import java.io.InputStream
3739import kotlin.reflect.KClass
@@ -41,23 +43,36 @@ private const val SERVICE_CREDENTIAL_PATH_PARAMETER = "serviceCredentialPath"
4143private const val SERVICE_CREDENTIAL_CONTENT_PARAMETER = " serviceCredentialContent"
4244private const val BOT_PROJECT_NUMBER_PARAMETER = " botProjectNumber"
4345private const val CONDENSED_FOOTNOTES_PARAMETER = " useCondensedFootnotes"
46+ private const val GSA_TO_IMPERSONATE_PARAMETER = " gsaToImpersonate"
4447
4548internal object GoogleChatConnectorProvider : ConnectorProvider {
4649
50+ private val logger = KotlinLogging .logger {}
51+
4752 override val connectorType: ConnectorType get() = googleChatConnectorType
4853
4954 override fun connector (connectorConfiguration : ConnectorConfiguration ): Connector {
5055 with (connectorConfiguration) {
5156
52- val credentialInputStream =
53- connectorConfiguration.parameters[SERVICE_CREDENTIAL_PATH_PARAMETER ]
54- ?.let { resourceAsStream(it) }
55- ? : connectorConfiguration.parameters[SERVICE_CREDENTIAL_CONTENT_PARAMETER ]
56- ?.let { ByteArrayInputStream (it.toByteArray()) }
57- ? : error(" Service credential missing : either $SERVICE_CREDENTIAL_PATH_PARAMETER or $SERVICE_CREDENTIAL_CONTENT_PARAMETER must be provided" )
57+ val gsaToImpersonate = connectorConfiguration.parameters[GSA_TO_IMPERSONATE_PARAMETER ]
58+
59+ val credentials: GoogleCredentials = if (gsaToImpersonate.isNullOrBlank()) {
60+ logger.info { " Using classic authentication mode with JSON credentials" }
61+ val credentialInputStream = getCredentialInputStream(connectorConfiguration)
62+ loadCredentials(credentialInputStream)
63+ } else {
64+ logger.info { " Using impersonation mode with GSA: $gsaToImpersonate " }
65+ createImpersonatedCredentials(connectorConfiguration, gsaToImpersonate)
66+ }
67+
68+ try {
69+ val token = credentials.refreshAccessToken()
70+ logger.info { " Google credentials OK, access token valid until ${token.expirationTime} " }
71+ } catch (e: Exception ) {
72+ logger.error(e) { " Unable to obtain access token for Google Chat API" }
73+ }
5874
59- val requestInitializer: HttpRequestInitializer =
60- HttpCredentialsAdapter (loadCredentials(credentialInputStream))
75+ val requestInitializer: HttpRequestInitializer = HttpCredentialsAdapter (credentials)
6176
6277 val useCondensedFootnotes =
6378 connectorConfiguration.parameters[CONDENSED_FOOTNOTES_PARAMETER ] == " 1"
@@ -85,6 +100,54 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
85100 }
86101 }
87102
103+ private fun createImpersonatedCredentials (
104+ connectorConfiguration : ConnectorConfiguration ,
105+ targetServiceAccount : String
106+ ): GoogleCredentials {
107+
108+ val sourceCredentials = getSourceCredentials(connectorConfiguration)
109+
110+ logger.info { " Source credentials: ${(sourceCredentials as ? ServiceAccountCredentials )?.clientEmail} " }
111+ logger.info { " Impersonating target GSA = $targetServiceAccount with scopes = $CHAT_SCOPE " }
112+
113+ return ImpersonatedCredentials .create(
114+ sourceCredentials,
115+ targetServiceAccount,
116+ null ,
117+ listOf (CHAT_SCOPE ),
118+ 3600 ,
119+ null
120+ )
121+ }
122+
123+ private fun getSourceCredentials (connectorConfiguration : ConnectorConfiguration ): GoogleCredentials {
124+ return try {
125+ val credentialInputStream = getCredentialInputStream(connectorConfiguration)
126+ val creds = ServiceAccountCredentials .fromStream(credentialInputStream)
127+ .createScoped(" https://www.googleapis.com/auth/cloud-platform" )
128+
129+ logger.info { " Loaded explicit service account: ${(creds as ServiceAccountCredentials ).clientEmail} " }
130+
131+ creds
132+ } catch (e: Exception ) {
133+ logger.info { " No explicit credentials found, using Application Default Credentials" }
134+ GoogleCredentials .getApplicationDefault()
135+ .createScoped(" https://www.googleapis.com/auth/cloud-platform" )
136+ }
137+ }
138+
139+ private fun getCredentialInputStream (connectorConfiguration : ConnectorConfiguration ): InputStream {
140+ return connectorConfiguration.parameters[SERVICE_CREDENTIAL_PATH_PARAMETER ]
141+ ?.let { resourceAsStream(it) }
142+ ? : connectorConfiguration.parameters[SERVICE_CREDENTIAL_CONTENT_PARAMETER ]
143+ ?.let { ByteArrayInputStream (it.toByteArray()) }
144+ ? : error(
145+ " Service credential missing: either " +
146+ " $SERVICE_CREDENTIAL_PATH_PARAMETER or " +
147+ " $SERVICE_CREDENTIAL_CONTENT_PARAMETER must be provided"
148+ )
149+ }
150+
88151 private fun loadCredentials (inputStream : InputStream ): GoogleCredentials =
89152 ServiceAccountCredentials
90153 .fromStream(inputStream)
@@ -99,6 +162,11 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
99162 BOT_PROJECT_NUMBER_PARAMETER ,
100163 true
101164 ),
165+ ConnectorTypeConfigurationField (
166+ " Service account email to impersonate (if provided, priority over JSON credentials)" ,
167+ GSA_TO_IMPERSONATE_PARAMETER ,
168+ false
169+ ),
102170 ConnectorTypeConfigurationField (
103171 " Service account credential file path (default : /service-account-{connectorId}.json)" ,
104172 SERVICE_CREDENTIAL_PATH_PARAMETER ,
@@ -122,4 +190,4 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
122190 setOf (GoogleChatConnectorTextMessageOut ::class )
123191}
124192
125- internal class GoogleChatConnectorProviderService : ConnectorProvider by GoogleChatConnectorProvider
193+ internal class GoogleChatConnectorProviderService : ConnectorProvider by GoogleChatConnectorProvider
0 commit comments