From 403e00381a7a1457f9c71a2dfa5d037814e988ee Mon Sep 17 00:00:00 2001 From: Nathan Phelps Date: Mon, 30 Sep 2024 16:41:05 -0400 Subject: [PATCH 1/2] Removed TLS v1.2 support. Removed an API key authentication pathway. Signed-off-by: Nathan Phelps --- CHANGELOG.md | 4 + docs/openapi-3-developer.json | 2 +- docs/openapi-3-user.json | 2 +- src/main/resources/version.txt | 2 +- .../exchangeapi/ExchangeApiApp.scala | 6 +- .../exchangeapi/auth/cloud/IBM.scala | 424 +++--------------- 6 files changed, 77 insertions(+), 363 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3463b880..367fe97d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [2.125.0] - 2024-09-30 +- Removed support for TLS v1.2. +- Removed an API key authentication pathway. + ## [2.124.0] - 2024-09-14 - Application configuration overhaul. - Some database configuration changes are not backwards compatible. diff --git a/docs/openapi-3-developer.json b/docs/openapi-3-developer.json index a7186da1..febda410 100644 --- a/docs/openapi-3-developer.json +++ b/docs/openapi-3-developer.json @@ -8,7 +8,7 @@ "name" : "Apache License Version 2.0", "url" : "https://www.apache.org/licenses/LICENSE-2.0" }, - "version" : "2.124.0" + "version" : "2.125.0" }, "externalDocs" : { "description" : "Open-horizon ExchangeAPI", diff --git a/docs/openapi-3-user.json b/docs/openapi-3-user.json index 900f7e37..c7d492e5 100644 --- a/docs/openapi-3-user.json +++ b/docs/openapi-3-user.json @@ -8,7 +8,7 @@ "name" : "Apache License Version 2.0", "url" : "https://www.apache.org/licenses/LICENSE-2.0" }, - "version" : "2.124.0" + "version" : "2.125.0" }, "externalDocs" : { "description" : "Open-horizon ExchangeAPI", diff --git a/src/main/resources/version.txt b/src/main/resources/version.txt index 67d5c273..27913085 100644 --- a/src/main/resources/version.txt +++ b/src/main/resources/version.txt @@ -1 +1 @@ -2.124.0 +2.125.0 diff --git a/src/main/scala/org/openhorizon/exchangeapi/ExchangeApiApp.scala b/src/main/scala/org/openhorizon/exchangeapi/ExchangeApiApp.scala index 0712355e..34c6b3a7 100644 --- a/src/main/scala/org/openhorizon/exchangeapi/ExchangeApiApp.scala +++ b/src/main/scala/org/openhorizon/exchangeapi/ExchangeApiApp.scala @@ -531,11 +531,9 @@ object ExchangeApiApp extends App .enableHttps(ConnectionContext.httpsServer(() => { // Custom TLS parameters val engine: SSLEngine = sslContext.createSSLEngine() - engine.setEnabledProtocols(Array("TLSv1.3", "TLSv1.2")) // TLSv1.2 is in support of OpenShift 4.6. HAPoxy router is built on top of RHEL7 which does not support TLSv1.3. + engine.setEnabledProtocols(Array("TLSv1.3")) engine.setEnabledCipherSuites(Array("TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384")) + "TLS_CHACHA20_POLY1305_SHA256")) engine.setUseClientMode(false) engine })) diff --git a/src/main/scala/org/openhorizon/exchangeapi/auth/cloud/IBM.scala b/src/main/scala/org/openhorizon/exchangeapi/auth/cloud/IBM.scala index 9cb17ac6..b1c5bcf2 100644 --- a/src/main/scala/org/openhorizon/exchangeapi/auth/cloud/IBM.scala +++ b/src/main/scala/org/openhorizon/exchangeapi/auth/cloud/IBM.scala @@ -45,9 +45,6 @@ final case class IamUserInfo(account: Option[IamAccount], sub: Option[String], i } final case class IamAccount(bss: String) -// Response from /api/v1/config -final case class ClusterConfigResponse(cluster_name: String, cluster_url: String, cluster_ca_domain: String, kube_url: String, helm_host: String) - /* Represents information from one IAM Account from the Multitenancy APIs - More information here: https://www.ibm.com/support/knowledgecenter/SSHKN6/iam/3.x.x/apis/mt_apis.html "id": "id-account", @@ -57,9 +54,6 @@ final case class ClusterConfigResponse(cluster_name: String, cluster_url: String */ final case class IamAccountInfo(id: String, name: String, description: String, createdOn: String) -// Response from iam-token/oidc/token API -final case class IamOIDCToken(access_token: String, refresh_token: String, token_type: String, expires_in: String, expiration: String) - //case class TokenAccountResponse(id: String, name: String, description: String) //s"ICP IAM authentication succeeded, but the org specified in the request ($requestOrg) does not match the org associated with the ICP credentials ($userCredsOrg)" @@ -72,7 +66,17 @@ class IbmCloudModule extends LoginModule with AuthorizationSupport { private var identity: Identity = _ private var succeeded = false def logger: LoggingAdapter = ExchangeApi.defaultLogger - + + override def abort() = false + + override def commit(): Boolean = { + if (succeeded) { + subject.getPrivateCredentials().add(identity) + //subject.getPrincipals().add(ExchangeRole(identity.role)) // don't think we need this + } + succeeded + } + override def initialize( subject: Subject, handler: CallbackHandler, @@ -130,17 +134,8 @@ class IbmCloudModule extends LoginModule with AuthorizationSupport { subject.getPrivateCredentials().add(identity) true } - - override def abort() = false - - override def commit(): Boolean = { - if (succeeded) { - subject.getPrivateCredentials().add(identity) - //subject.getPrincipals().add(ExchangeRole(identity.role)) // don't think we need this - } - succeeded - } - + + private def extractApiKey(reqInfo: RequestInfo, hint: Option[String]): Try[IamAuthCredentials] = { //val creds = credentials(reqInfo) val creds: Creds = reqInfo.creds @@ -162,9 +157,6 @@ object IbmCloudAuth { implicit def executionContext: ExecutionContext = ExchangeApi.defaultExecutionContext private var db: Database = _ - private var sslSocketFactory: SSLSocketFactory = _ - private var icpClusterNameTry: Try[String] = _ // Note: we don't currently use this value - private var icpClusterNameNumRetries = 0 private implicit val formats: DefaultFormats.type = DefaultFormats @@ -175,37 +167,10 @@ object IbmCloudAuth { .expireAfterWrite(Configuration.getConfig.getInt("api.cache.IAMusersTtlSeconds"), TimeUnit.SECONDS) .build[String, Entry[String]] // the cache key is /: (where keytype is iamapikey or iamtoken), and the value is / implicit val userCache: GuavaCache[String] = GuavaCache(guavaCache) // the effect of this is that these methods don't need to be qualified - - def getICPExternalMgmtIngress: String = - try - Configuration.getConfig.getString("ibm.common-services.external-management-ingress") - catch { - case _: Exception => "" - } - - def getICPManagementIngressServicePort: String = - try - Configuration.getConfig.getString("ibm.common-services.management-ingress-service-port") - catch { - case _: Exception => "8443" - } - - def getPlatformIdentityProviderServicePort: String = - try - Configuration.getConfig.getString("ibm.common-services.identity-provider-service-port") - catch { - case _: Exception => "4300" - } // Called by ExchangeApiApp after db is established and upgraded def init(db: Database): Unit = { this.db = db - logger.debug(s"IBM authentication-related env vars: PLATFORM_IDENTITY_PROVIDER_SERVICE_PORT=$getPlatformIdentityProviderServicePort, ICP_EXTERNAL_MGMT_INGRESS=$getICPExternalMgmtIngress, ICP_MANAGEMENT_INGRESS_SERVICE_PORT=$getICPManagementIngressServicePort") - if (isIcp) { - this.sslSocketFactory = buildSslSocketFactory(getIcpCertFile) - getIcpClusterName // this caches the result in member var icpClusterNameTry - logger.info(s"ICP cluster name: $icpClusterNameTry") - } } def authenticateUser(authInfo: IamAuthCredentials, hint: Option[String]): Try[String] = { @@ -218,11 +183,13 @@ object IbmCloudAuth { */ cachingF(authInfo.cacheKey)(ttl = None) { for { - token <- if (authInfo.keyType == "iamtoken") Success(IamToken(authInfo.key)) - else if (isIcp && authInfo.keyType == "iamapikey") Success(IamToken(authInfo.key, Option(authInfo.keyType))) // this is an apikey we are putting in IamToken, but it can be used like a token in the next step - else getIamToken(authInfo) + token <- + if (authInfo.keyType == "iamtoken") + Success(IamToken(authInfo.key)) + else + getIamToken(authInfo) userInfo <- getUserInfo(token, authInfo) - oidcToken <- if (isIcp && authInfo.keyType == "iamapikey") getOIDCToken(token) else Success(IamToken(authInfo.key, Option(authInfo.keyType))) + oidcToken <- Success(IamToken(authInfo.key, Option(authInfo.keyType))) user <- getOrCreateUser(authInfo, userInfo, Option(hint.getOrElse("")), Option(oidcToken)) } yield user.username // this is the composite org/username } @@ -237,93 +204,9 @@ object IbmCloudAuth { logger.debug(s"Clearing the IBM Cloud auth cache") removeAll().map(_ => ()) // i think this map() just transforms the removeAll() return of a future into Unit } - - private def isIcp: Boolean = { - // ICP kube automatically sets the 1st one, our development environment sets the 2nd one when locally testing - //logger.debug("isIcp: ICP_EXTERNAL_MGMT_INGRESS: " + sys.env.get("ICP_EXTERNAL_MGMT_INGRESS")) - (Configuration.getConfig.hasPath("ibm.cloud.platform-identity-provider-service-port") || - Configuration.getConfig.hasPath("ibm.cloud.icp-external-management-ingress")) - } - - // Common Services IAM - private def getIcpIdentityProviderUrl: String = { - // https://$ICP_EXTERNAL_MGMT_INGRESS/idprovider or https://platform-identity-provider:$PLATFORM_IDENTITY_PROVIDER_SERVICE_PORT - if (Configuration.getConfig.hasPath("ibm.cloud.icp-external-management-ingress")) { - var ICP_EXTERNAL_MGMT_INGRESS: String = getICPExternalMgmtIngress - if (!ICP_EXTERNAL_MGMT_INGRESS.startsWith("https://")) ICP_EXTERNAL_MGMT_INGRESS = s"https://$ICP_EXTERNAL_MGMT_INGRESS" - s"$ICP_EXTERNAL_MGMT_INGRESS/idprovider" - } else { - // ICP kube automatically sets this env var and hostname - val PLATFORM_IDENTITY_PROVIDER_SERVICE_PORT: String = getPlatformIdentityProviderServicePort - s"https://platform-identity-provider:$PLATFORM_IDENTITY_PROVIDER_SERVICE_PORT" - } - } - - // - private def getIcpMgmtIngressUrl: String = { - // https://$ICP_EXTERNAL_MGMT_INGRESS or https://icp-management-ingress.kube-system:$ICP_MANAGEMENT_INGRESS_SERVICE_PORT - if (Configuration.getConfig.hasPath("ibm.cloud.icp-external-management-ingress")) { - var ICP_EXTERNAL_MGMT_INGRESS: String = getICPExternalMgmtIngress - if (!ICP_EXTERNAL_MGMT_INGRESS.startsWith("https://")) ICP_EXTERNAL_MGMT_INGRESS = s"https://$ICP_EXTERNAL_MGMT_INGRESS" - ICP_EXTERNAL_MGMT_INGRESS - } else { - // ICP kube automatically sets this env var and hostname - val ICP_MANAGEMENT_INGRESS_SERVICE_PORT: String = getICPManagementIngressServicePort - s"https://icp-management-ingress.kube-system:$ICP_MANAGEMENT_INGRESS_SERVICE_PORT" - } - } - - private def getIcpCertFile = "/etc/horizon/exchange/icp/ca.crt" // our ICP provisioning creates this file + private def iamRetryNum = 5 - // Get the ICP cluster name as a Try[String] and cache it in member var icpClusterNameTry. - // Note: we currently call this, but don't use the value for anything - private def getIcpClusterName: Try[String] = { - icpClusterNameTry match { - case null => - icpClusterNameTry = _getIcpClusterName - icpClusterNameTry - case Failure(_: IamApiTimeoutException) | Failure(_: IamApiErrorException) => - // Getting the cluster name failed before with a retryable error, so retry a few times - if (icpClusterNameNumRetries < iamRetryNum) { - icpClusterNameTry = _getIcpClusterName // Note: this method has retries within it - icpClusterNameNumRetries += 1 - icpClusterNameTry - }else { - icpClusterNameTry // we have already retried the max times, so we have to live with this error from now on - } - case Failure(_) => - icpClusterNameTry // we previously got an error that is not retryable - case Success(_) => - icpClusterNameTry - } - } - - // Internal method called from getIcpClusterName - // Common Services - private def _getIcpClusterName: Try[String] = { - for (i <- 1 to iamRetryNum) { - try { - // just get cluster name - works for both ICP 3.2.0 and 3.2.1 - val iamUrl: String = getIcpMgmtIngressUrl + "/api/v1/config" - logger.debug("Attempt " + i + " retrieving ICP IAM cluster name from " + iamUrl) - val response: HttpResponse[String] = Http(iamUrl).method("get").option(HttpOptions.sslSocketFactory(this.sslSocketFactory)).asString - if (response.code == HttpCode.OK.intValue) { - val resp: ClusterConfigResponse = parse(response.body).extract[ClusterConfigResponse] - val clusterName: String = resp.cluster_name - //logger.debug("ICP cluster name: " + clusterName) - return Success(clusterName) - } else { - logger.debug("Cluster name retrieval http code: " + response.code) - return Failure(new IamApiErrorException(response.body)) - } - } catch { - case e: Exception => return Failure(new IamApiErrorException(ExchMsg.translate("error.getting.cluster.name", e.getMessage))) - } - } - Failure(new IamApiTimeoutException(ExchMsg.translate("iam.return.value.not.set", "GET config", iamRetryNum))) - } - // Use the IBM IAM API to authenticate the iamapikey and get an IAM token. See: https://cloud.ibm.com/apidocs/iam-identity-token-api private def getIamToken(authInfo: IamAuthCredentials): Try[IamToken] = { if (authInfo.keyType == "iamapikey") { @@ -357,121 +240,31 @@ object IbmCloudAuth { // Using the IAM token get the ibm cloud account id (which we'll use to verify the exchange org) and users email (which we'll use as the exchange user) // For ICP IAM see: https://github.ibm.com/IBMPrivateCloud/roadmap/blob/master/feature-specs/security/security-services-apis.md private def getUserInfo(token: IamToken, authInfo: IamAuthCredentials): Try[IamUserInfo] = { - if (isIcp && token.tokenType.getOrElse("") == "iamapikey") { // Common Services IAM - // An icp platform apikey that we can use directly to authenticate and get the username - var delayedReturn: Try[IamUserInfo] = Failure(new IamApiTimeoutException(ExchMsg.translate("iam.return.value.not.set", "GET introspect", iamRetryNum))) - for (i <- 1 to iamRetryNum) { - try { - val iamUrl: String = getIcpMgmtIngressUrl + "/iam-token/oidc/introspect" - logger.info("Attempt " + i + " retrieving ICP IAM userinfo for " + authInfo.org + "/iamapikey from " + iamUrl) - val apiKey: String = token.accessToken - // Have our http client use the ICP self-signed cert so we don't have to use the allowUnsafeSSL option - val response: HttpResponse[String] = Http(iamUrl).method("post").option(HttpOptions.sslSocketFactory(this.sslSocketFactory)) - .header("Content-Type", "application/x-www-form-urlencoded") - .postData("apikey=" + apiKey) - .asString - logger.debug(iamUrl + " http code: " + response.code + ", body: " + response.body) - if (response.code == HttpCode.OK.intValue) { - // This api returns 200 even for an invalid api key. Have to determine its validity via the 'active' field - val userInfo: IamUserInfo = parse(response.body).extract[IamUserInfo] - if (userInfo.isActive && userInfo.user != "") return Success(userInfo) - else return Failure(new InvalidCredentialsException(ExchMsg.translate("invalid.iam.api.key"))) - } else delayedReturn = Failure(new IamApiErrorException(response.body)) - } catch { - case e: Exception => delayedReturn = Failure(new IamApiErrorException(ExchMsg.translate("error.authenticating.icp.iam.key", e.getMessage))) - } - } - delayedReturn // if we tried the max times and never got a successful positive or negative, return what we last got - } else if (isIcp) { // Common Services Identity Provider - // An icp token from the UI - var delayedReturn: Try[IamUserInfo] = Failure(new IamApiTimeoutException(ExchMsg.translate("iam.return.value.not.set", "GET userinfo", iamRetryNum))) - for (i <- 1 to iamRetryNum) { - try { - val iamUrl: String = getIcpIdentityProviderUrl + "/v1/auth/userinfo" - //logger.debug("Retrieving ICP IAM userinfo from " + iamUrl + ", token: " + token.accessToken) - logger.info("Attempt " + i + " retrieving ICP IAM userinfo for " + authInfo.org + "/iamtoken from " + iamUrl) - val response: HttpResponse[String] = Http(iamUrl).method("post").option(HttpOptions.sslSocketFactory(this.sslSocketFactory)) - .header("Authorization", s"BEARER ${token.accessToken}") - .header("Content-Type", "application/json") - .asString - logger.debug(iamUrl + " http code: " + response.code + ", body: " + response.body) - if (response.code == HttpCode.OK.intValue) return Success(parse(response.body).extract[IamUserInfo]) - else if (response.code == HttpCode.BAD_INPUT.intValue || response.code == HttpCode.BADCREDS.intValue || response.code == HttpCode.ACCESS_DENIED.intValue || response.code == HttpCode.NOT_FOUND.intValue) { - // This IAM API returns BAD_INPUT (400) when the mechanics of the api call were successful, but the token was invalid - return Failure(new InvalidCredentialsException(response.body)) - } else delayedReturn = Failure(new IamApiErrorException(response.body)) - } catch { - case e: Exception => delayedReturn = Failure(new IamApiErrorException(ExchMsg.translate("error.authenticating.icp.iam.token", e.getMessage))) - } + // An ibm public cloud token, either from the UI or from the platform apikey we were given + var delayedReturn: Try[IamUserInfo] = Failure(new IamApiTimeoutException(ExchMsg.translate("iam.return.value.not.set", "GET identity/userinfo", iamRetryNum))) + for (i <- 1 to iamRetryNum) { + try { + val iamUrl = "https://iam.cloud.ibm.com/identity/userinfo" + logger.info("Attempt " + i + " retrieving IBM Cloud IAM userinfo for " + authInfo.org + "/iamtoken from " + iamUrl) + val response: HttpResponse[String] = + Http(iamUrl).header("Authorization", s"BEARER ${token.accessToken}") + .header("Content-Type", "application/json") + .asString + logger.debug(iamUrl + " http code: " + response.code + ", body: " + response.body) + if (response.code == HttpCode.OK.intValue) { + // This api returns 200 even for an invalid token. Have to determine its validity via the 'active' field + val userInfo: IamUserInfo = parse(response.body).extract[IamUserInfo] + if (userInfo.isActive && userInfo.user != "") return Success(userInfo) + else return Failure(new InvalidCredentialsException(ExchMsg.translate("invalid.iam.token"))) + } else delayedReturn = Failure(new IamApiErrorException(response.body)) } - delayedReturn // if we tried the max times and never got a successful positive or negative, return what we last got - } else { - // An ibm public cloud token, either from the UI or from the platform apikey we were given - var delayedReturn: Try[IamUserInfo] = Failure(new IamApiTimeoutException(ExchMsg.translate("iam.return.value.not.set", "GET identity/userinfo", iamRetryNum))) - for (i <- 1 to iamRetryNum) { - try { - val iamUrl = "https://iam.cloud.ibm.com/identity/userinfo" - logger.info("Attempt " + i + " retrieving IBM Cloud IAM userinfo for " + authInfo.org + "/iamtoken from " + iamUrl) - val response: HttpResponse[String] = Http(iamUrl) - .header("Authorization", s"BEARER ${token.accessToken}") - .header("Content-Type", "application/json") - .asString - logger.debug(iamUrl + " http code: " + response.code + ", body: " + response.body) - if (response.code == HttpCode.OK.intValue) { - // This api returns 200 even for an invalid token. Have to determine its validity via the 'active' field - val userInfo: IamUserInfo = parse(response.body).extract[IamUserInfo] - if (userInfo.isActive && userInfo.user != "") return Success(userInfo) - else return Failure(new InvalidCredentialsException(ExchMsg.translate("invalid.iam.token"))) - } else delayedReturn = Failure(new IamApiErrorException(response.body)) - } catch { + catch { case e: Exception => delayedReturn = Failure(new IamApiErrorException(ExchMsg.translate("error.authenticating.iam.token", e.getMessage))) - } } - delayedReturn // if we tried the max times and never got a successful positive or negative, return what we last got } + delayedReturn // if we tried the max times and never got a successful positive or negative, return what we last got } - - // Common Services - def getOIDCToken(icpapikey: IamToken) : Try[IamToken] = { - /* - curl -k -X POST -H "Content-Type: application/x-www-form-urlencoded" -H "Accept: application/json" - -d 'grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=&response_type=cloud_iam' - "$CLUSTER_ADDRESS/iam-token/oidc/token" - */ - logger.debug("getOIDCToken") - val oidcTokenURL: String = getIcpMgmtIngressUrl + "/iam-token/oidc/token" - val response: HttpResponse[String] = Http(oidcTokenURL).method("post").option(HttpOptions.sslSocketFactory(this.sslSocketFactory)) - .header("Content-Type", "application/x-www-form-urlencoded") - .header("Accept", "application/json") - .postData(s"grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey=${icpapikey.accessToken}&response_type=cloud_iam") - .asString - if (response.code == HttpCode.OK.intValue){ - val respToken: IamOIDCToken = parse(response.body).extract[IamOIDCToken] - Success(IamToken(accessToken = respToken.access_token, tokenType = Option("OIDC_access_token"))) - } else Failure(new InvalidCredentialsException(ExchMsg.translate("invalid.iam.token"))) - } - - // Common Services - def getUserAccounts(token: IamToken, userInfo: IamUserInfo) : Try[List[IamAccountInfo]] = { - if (isIcp){ - /* - curl -k -X GET \ - -H 'Accept: application/json' \ - -H "Authorization: Bearer $TOKEN" \ - -H 'Content-Type: application/json' \ - "https://$CLUSTER_ADDRESS/idmgmt/identity/api/v1/users/$USERID/accounts" - */ - logger.debug("getUserAccounts: " + userInfo) - val accountsURL: String = getIcpMgmtIngressUrl + "/idmgmt/identity/api/v1/users/" + userInfo.user + "/accounts" - val response: HttpResponse[String] = Http(accountsURL).method("get").option(HttpOptions.sslSocketFactory(this.sslSocketFactory)) - .header("Content-Type", "application/json") - .header("Accept", "application/json") - .header("Authorization", "Bearer " + token.accessToken) - .asString - if (response.code == HttpCode.OK.intValue) Success(parse(response.body).extract[List[IamAccountInfo]]) - else Failure(new InvalidCredentialsException(ExchMsg.translate("invalid.iam.token"))) - } else Failure(new InvalidCredentialsException(ExchMsg.translate("api.access.denied"))) - } + private def getOrCreateUser(authInfo: IamAuthCredentials, userInfo: IamUserInfo, hint: Option[String], oidcToken: Option[IamToken]): Try[UserRow] = { logger.debug("Getting or creating exchange user from DB using IAM userinfo: " + userInfo) @@ -531,58 +324,26 @@ object IbmCloudAuth { // Verify that the cloud acct id of the cloud api key and the exchange org entry match // authInfo is the creds they passed in, userInfo is what was returned from the IAM calls, and orgAcctId is what we got from querying the org in the db private def fetchVerifyOrg(authInfo: IamAuthCredentials, userInfo: IamUserInfo, hint: Option[String], oidcToken: Option[IamToken]) = { - if (isIcp) { - if (hint.getOrElse("") == "exchangeNoOrgForMultLogin") { - DBIO.successful(null) - } - else { - // replace this with checking against getUserAccounts that the org they're in matches the org they're trying to access - logger.debug(s"Fetching and verifying ICP/OCP org: $authInfo, $userInfo") - //if (authInfo.keyType == "iamtoken") - if (authInfo.org == "root") DBIO.successful(authInfo.org) - else { - val tokentoUse: IamToken = if (authInfo.keyType == "iamtoken") IamToken(authInfo.key) else oidcToken.getOrElse(IamToken(null)) - getUserAccounts(tokentoUse, userInfo) match { - case Success(allAccounts) => - val accountsIDs: ListBuffer[String] = ListBuffer[String]() - for (account <- allAccounts) { - accountsIDs += account.id - } // building a list of just the account Ids to search on in the query - OrgsTQ.getOrgid(authInfo.org).map(_.tags.+>>("cloud_id")).result.headOption.flatMap({ - case None => - DBIO.failed(OrgNotFound(authInfo.org)) - case Some(orgAccountID) => - logger.debug(s"fetch org acctId: $orgAccountID") - if (accountsIDs.contains(orgAccountID.getOrElse(""))) DBIO.successful(authInfo.org) - else DBIO.failed(IncorrectOrgFoundMult(authInfo.org)) - }) - case Failure(t) => - DBIO.failed(t) - } + // IBM Cloud - we already have the account id from iam from the creds, and the account id of the exchange org + logger.debug(s"Fetching and verifying IBM public cloud org: $authInfo, $userInfo") + OrgsTQ.getOrgid(authInfo.org).map(_.tags.+>>("ibmcloud_id")).result.headOption.flatMap({ + case None => + //logger.error(s"IAM authentication succeeded, but no matching exchange org with a cloud account id was found for ${authInfo.org}") + DBIO.failed(OrgNotFound(authInfo.org)) + case Some(acctIdOpt) => // acctIdOpt is type Option[String] + logger.debug(s"fetch org acctIdOpt: $acctIdOpt") + if (authInfo.keyType == "iamtoken" && userInfo.accountId == "") { + //todo: this is the case with tokens from the edge mgmt ui (because we don't get the accountId when we validate an iamtoken). The ui already verified the org and is using the right one, + // so there is no exposure there. But we still need to verify the org in case someone is spoofing being the ui. But to get here, the iamtoken has to be valid, just not for this org. + // With IECM on ICP/OCP, the only exposure is using an iamtoken from the cluster org to manage resources in the IBM org. + DBIO.successful(authInfo.org) + } else if (acctIdOpt.getOrElse("") != userInfo.accountId) { + //logger.error(s"IAM authentication succeeded, but the cloud account id of the org $orgAcctId does not match that of the cloud account ${userInfo.accountId}") + DBIO.failed(IncorrectOrgFound(authInfo.org, userInfo.accountId)) + } else { + DBIO.successful(authInfo.org) } - } - } else { - // IBM Cloud - we already have the account id from iam from the creds, and the account id of the exchange org - logger.debug(s"Fetching and verifying IBM public cloud org: $authInfo, $userInfo") - OrgsTQ.getOrgid(authInfo.org).map(_.tags.+>>("ibmcloud_id")).result.headOption.flatMap({ - case None => - //logger.error(s"IAM authentication succeeded, but no matching exchange org with a cloud account id was found for ${authInfo.org}") - DBIO.failed(OrgNotFound(authInfo.org)) - case Some(acctIdOpt) => // acctIdOpt is type Option[String] - logger.debug(s"fetch org acctIdOpt: $acctIdOpt") - if (authInfo.keyType == "iamtoken" && userInfo.accountId == "") { - //todo: this is the case with tokens from the edge mgmt ui (because we don't get the accountId when we validate an iamtoken). The ui already verified the org and is using the right one, - // so there is no exposure there. But we still need to verify the org in case someone is spoofing being the ui. But to get here, the iamtoken has to be valid, just not for this org. - // With IECM on ICP/OCP, the only exposure is using an iamtoken from the cluster org to manage resources in the IBM org. - DBIO.successful(authInfo.org) - } else if (acctIdOpt.getOrElse("") != userInfo.accountId) { - //logger.error(s"IAM authentication succeeded, but the cloud account id of the org $orgAcctId does not match that of the cloud account ${userInfo.accountId}") - DBIO.failed(IncorrectOrgFound(authInfo.org, userInfo.accountId)) - } else { - DBIO.successful(authInfo.org) - } - }) - } + }) } private def fetchUser(org: String, userInfo: IamUserInfo) = { @@ -597,15 +358,15 @@ object IbmCloudAuth { private def createUser(org: String, userInfo: IamUserInfo) = { if(org != null) { logger.debug("Creating user: org=" + org + ", " + userInfo) - val user: UserRow = UserRow( - s"$org/${userInfo.user}", - org, - "", - admin = false, - hubAdmin = false, - userInfo.user, - ApiTime.nowUTC, - s"$org/${userInfo.user}") + val user: UserRow = + UserRow(username = s"$org/${userInfo.user}", + orgid = org, + hashedPw = "", + admin = false, + hubAdmin = false, + email = userInfo.user, + lastUpdated = ApiTime.nowUTC, + updatedBy = s"$org/${userInfo.user}") (UsersTQ += user).asTry.map(count => count.map(_ => user)) } else { logger.debug("ibmcloudmodule not creating user") @@ -613,25 +374,6 @@ object IbmCloudAuth { } } - /* private def extractICPTokenOrg(id: String) = { - // ICP id field is like: id-major-peacock-icp-cluster-account - val R = """id-(.+)-account""".r - id match { - case R(clusterName) => clusterName - case _ => "" - } - } */ - - // Currently not needed for verifying Org in the ICP case - /* private def extractICPOrg(iss: String) = { - // ICP iss value looks like: https://major-peacock-icp-cluster.icp:9443/oidc/token - val R = """https://(.+)\.icp:.+""".r - iss match { - case R(clusterName) => clusterName - case _ => "" - } - } */ - // Split an id in the form org/id and return both parts. If there is no / we assume it is an id without the org. def compositeIdSplit(compositeId: String): (String, String) = { val reg: Regex = """^(\S*?)/(\S*)$""".r @@ -643,34 +385,4 @@ object IbmCloudAuth { case _ => ("", compositeId) } } - - // Create an SSLSocketFactory that verifies a server using a self-signed cert. Used as example: https://gist.github.com/erickok/7692592 - def buildSslSocketFactory(filePath: String): SSLSocketFactory = { - try { - // Load the cert from the file - val cf: CertificateFactory = CertificateFactory.getInstance("X.509") - val caInput = new BufferedInputStream(new FileInputStream(new File(filePath))) - var ca: Certificate = null - try { - ca = cf.generateCertificate(caInput) - logger.debug("Loading self-signed CA from " + filePath + ", type: " + ca.getType) - } finally { caInput.close() } - - // Create an in-memory KeyStore containing our self-signed cert - val keyStoreType: String = KeyStore.getDefaultType - val keyStore: KeyStore = KeyStore.getInstance(keyStoreType) - keyStore.load(null, null) - keyStore.setCertificateEntry("ca", ca) - - // Create a TrustManager that trusts the CAs in our KeyStore - val alg: String = TrustManagerFactory.getDefaultAlgorithm - val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(alg) - tmf.init(keyStore) - - // Create an SSLContext that uses our TrustManager - val context: SSLContext = SSLContext.getInstance("TLS") - context.init(null, tmf.getTrustManagers, null) - context.getSocketFactory - } catch { case e: Exception => throw new SelfSignedCertException(e.getMessage) } - } } From ae1fb0e6cbf956c73bea876c97b87cd39ed8fa10 Mon Sep 17 00:00:00 2001 From: Nathan Phelps Date: Mon, 30 Sep 2024 16:51:05 -0400 Subject: [PATCH 2/2] Updated Scala dependency to v2.13.15. Signed-off-by: Nathan Phelps --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 954046ca..90677af2 100644 --- a/build.sbt +++ b/build.sbt @@ -37,7 +37,7 @@ lazy val root = (project in file(".")) pekkoVersion := "[1.0.2]", release := sys.env.getOrElse("GIT_SHORT_SHA", versionFunc()), resolvers += Classpaths.typesafeReleases, - scalaVersion := "2.13.14", + scalaVersion := "2.13.15", summary := "'Open Horizon exchange-api image'", vendor := "'Open Horizon'", version := sys.env.getOrElse("IMAGE_VERSION", versionFunc()),