From 2057a85535da0cf023d142ab09095195dd3badbd Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Tue, 19 Nov 2024 22:03:02 +0100 Subject: [PATCH 1/4] Fixes #25903: Refactor API tokens after clear-text removal --- .../rudder/api/ApiAccountRepository.scala | 40 ++--- .../normation/rudder/api/DataStructures.scala | 154 ++++++++++++++---- .../normation/rudder/api/TokenGenerator.scala | 2 +- .../rudder/apidata/RestDataSerializer.scala | 97 ++++++++--- .../repository/ldap/LDAPDiffMapper.scala | 2 +- .../repository/ldap/LDAPEntityMapper.scala | 4 +- .../marshalling/XmlSerialisationImpl.scala | 2 +- .../marshalling/XmlUnserialisationImpl.scala | 2 +- .../normation/rudder/api/ApiTokenTest.scala | 84 ++++++++-- .../rest/internal/RestApiAccounts.scala | 58 ++----- .../com/normation/rudder/users/User.scala | 2 +- .../services/EventLogDetailsGenerator.scala | 2 +- .../src/main/elm/sources/Accounts.elm | 7 +- .../main/elm/sources/Accounts/DataTypes.elm | 6 +- .../main/elm/sources/Accounts/JsonDecoder.elm | 15 +- .../main/elm/sources/Accounts/JsonEncoder.elm | 1 - .../src/main/elm/sources/Accounts/View.elm | 9 +- .../main/elm/sources/Accounts/ViewUtils.elm | 36 ++-- .../bootstrap/liftweb/AppConfigAuth.scala | 9 +- .../bootstrap/liftweb/RudderConfig.scala | 8 +- .../checks/action/CreateSystemToken.scala | 8 +- .../checks/action/TestCreateSystemToken.scala | 4 +- 22 files changed, 363 insertions(+), 189 deletions(-) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala index 69f62bfff29..e35c88febb6 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala @@ -55,7 +55,6 @@ import com.normation.rudder.repository.ldap.LDAPDiffMapper import com.normation.rudder.repository.ldap.LDAPEntityMapper import com.normation.rudder.services.user.PersonIdentService import com.normation.zio.* -import java.security.MessageDigest import org.joda.time.DateTime import zio.* import zio.syntax.* @@ -71,7 +70,7 @@ trait RoApiAccountRepository { */ def getAllStandardAccounts: IOResult[Seq[ApiAccount]] - def getByToken(token: ApiToken): IOResult[Option[ApiAccount]] + def getByToken(hashedToken: ApiTokenHash): IOResult[Option[ApiAccount]] def getById(id: ApiAccountId): IOResult[Option[ApiAccount]] @@ -98,8 +97,8 @@ final class RoLDAPApiAccountRepository( val rudderDit: RudderDit, val ldapConnexion: LDAPConnectionProvider[RoLDAPConnection], val mapper: LDAPEntityMapper, - val tokenGen: TokenGenerator, - val systemAcl: List[ApiAclElement] + val systemAcl: List[ApiAclElement], + val systemToken: ApiTokenHash ) extends RoApiAccountRepository { val systemAPIAccount: ApiAccount = { @@ -107,7 +106,7 @@ final class RoLDAPApiAccountRepository( ApiAccountId("rudder-system-api-account"), ApiAccountKind.System, ApiAccountName("Rudder system account"), - ApiToken(ApiToken.generate_secret(tokenGen, "-system")), + systemToken, "For internal use", isEnabled = true, creationDate = DateTime.now, @@ -151,25 +150,18 @@ final class RoLDAPApiAccountRepository( // Warning: When matching clear-text value we MUST make sure it is not // a hash but a clear text token to avoid accepting the hash as valid token itself. // - override def getByToken(token: ApiToken): IOResult[Option[ApiAccount]] = { - if (token.isHashed) { - None.succeed - } else if (MessageDigest.isEqual(token.value.getBytes(), systemAPIAccount.token.value.getBytes())) { - // Constant-time comparison - Some(systemAPIAccount).succeed - } else { - val hash = ApiToken.hash(token.value) - for { - ldap <- ldapConnexion - // here, be careful to the semantic of get with a filter! - optEntry <- ldap.get(rudderDit.API_ACCOUNTS.dn, BuildFilter.EQ(RudderLDAPConstants.A_API_TOKEN, hash)) - optRes <- optEntry match { - case None => None.succeed - case Some(e) => mapper.entry2ApiAccount(e).map(Some(_)).toIO - } - } yield { - optRes - } + override def getByToken(hashedToken: ApiTokenHash): IOResult[Option[ApiAccount]] = { + for { + ldap <- ldapConnexion + // here, be careful to the semantic of get with a filter! + optEntry <- ldap.get(rudderDit.API_ACCOUNTS.dn, BuildFilter.EQ(RudderLDAPConstants.A_API_TOKEN, hashedToken.exposeHash())) + optRes <- optEntry match { + case None => None.succeed + case Some(e) => mapper.entry2ApiAccount(e).map(Some(_)).toIO + } + } yield { + optRes + } } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala index ed8d3260dc2..369993b4fda 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala @@ -38,7 +38,6 @@ package com.normation.rudder.api import cats.data.* import cats.implicits.* -import com.normation.rudder.api.ApiToken.prefixV2 import com.normation.rudder.facts.nodes.NodeSecurityContext import enumeratum.* import java.nio.charset.StandardCharsets @@ -58,53 +57,102 @@ final case class ApiAccountId(value: String) extends AnyVal final case class ApiAccountName(value: String) extends AnyVal /** - * The actual authentication token. + * The actual authentication token, in clear text. * - * TODO: Once support for plain text tokens is dropped, make separate types for plain and hashed tokens. - Current situation is confusing, and hence a bit risky. + * All tokens are 32 alphanumeric characters, optionally + * followed by a "-system" suffix, indicating a system token. * - * There are two versions of tokens: + */ +final case class ApiTokenSecret(private val secret: String) { + // Avoid printing the value in logs, regardless of token type + override def toString: String = "[REDACTED ApiTokenSecret]" + + // Prevent comparison + override def equals(obj: Any) = false + + // For cases when we need to print a part of the plain token for debugging. + // Show the first 4 chars: enough to disambiguate, and preserves 166 bits of randomness. + def exposeSecretBeginning: String = { + secret.take(4) + "[SHORTENED ApiTokenSecret]" + } + + def exposeSecret(): String = { + secret + } + + def hash(): ApiTokenHash = { + ApiTokenHash.hash(this) + } +} + +object ApiTokenSecret { + private val tokenSize = 32 + + def generate(tokenGenerator: TokenGenerator, suffix: String = ""): ApiTokenSecret = { + val completeSuffix = if (suffix.isEmpty) "" else "-" + suffix + ApiTokenSecret(tokenGenerator.newToken(tokenSize) + completeSuffix) + } +} + +/* + * There are two versions of token hashes: * - * * v1: 32 alphanumeric characters stored as clear text - * they are also displayed in clear text in the interface. + * * v1: 32 alphanumeric characters stored as clear text. + * They were also displayed in clear text in the interface. + * They are not supported anymore since 8.3, we just ignore them. * * v2: starting from Rudder 8.1, tokens are still 32 alphanumeric characters, * but are now stored hashed in sha512 (128 characters), prefixed with "v2:". - * The tokens are only displayed once at creation. - * - * Both can have a `-system` suffix to mark the system token. + * The secret are only displayed once at creation. * - * To make the difference, we use a prefix to the hash value in v2 + * Hashes are stored with a prefix indicating the hash algorithm: * * * If it starts with "v2:", it is a v2 SHA512 hash of the token * * If it does not start with "v2:", it is a clear-text v1 token - * Note: v2 tokens can never start with "v" as they are encoded as en hexadecimal string + * Note: stored v1 tokens can never start with "v" as they are encoded as en hexadecimal string. + * + * We don't have generic code as V2 is (very) likely the last simple API key mechanism we'll need. + * */ -case class ApiToken(value: String) extends AnyVal { - // Avoid printing the value in logs, regardless of token type - override def toString: String = "[REDACTED ApiToken]" - // For cases we need to print a part of the plain token for debug. - // Show the first 4 chars: enough to disambiguate, and preserves 166 bits of randomness. - def exposeSecretBeginning: String = { - value.take(4) + "[SHORTENED ApiToken]" +final case class ApiTokenHash(private val value: String) { + override def toString: String = "[REDACTED ApiTokenHash]" + + // Constant time comparison + override def equals(obj: Any): Boolean = { + obj match { + case ApiTokenHash(other) => MessageDigest.isEqual(value.getBytes(), other.getBytes()) + case _ => false + } } - def isHashed: Boolean = { - value.startsWith(prefixV2) + def exposeHash(): String = { + value + } + + def version(): Int = { + if (value.startsWith(ApiTokenHash.prefix)) { + 2 + } else { + 1 + } } } -object ApiToken { - private val tokenSize = 32 - private val prefixV2 = "v2:" +object ApiTokenHash { + val prefix = "v2" + // Guaranteed to never match + val neverMatch = ApiTokenHash.build("not-matching") - def hash(clearText: String): String = { - val digest = MessageDigest.getInstance("SHA-512") - prefixV2 + new String(Hex.encode(digest.digest(clearText.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8) + // Build from hashed value + private def build(value: String): ApiTokenHash = { + ApiTokenHash(prefix + ":" + value) } - def generate_secret(tokenGenerator: TokenGenerator, suffix: String = ""): String = { - tokenGenerator.newToken(tokenSize) + suffix + def hash(token: ApiTokenSecret): ApiTokenHash = { + val sha512Digest = MessageDigest.getInstance("SHA-512") + val hash = sha512Digest.digest(token.exposeSecret().getBytes(StandardCharsets.UTF_8)) + val hexHash = Hex.encode(hash) + ApiTokenHash.build(new String(hexHash, StandardCharsets.UTF_8)) } } @@ -354,10 +402,54 @@ final case class ApiAccount( name: ApiAccountName, // used in event log to know who did actions. - token: ApiToken, + token: ApiTokenHash, description: String, isEnabled: Boolean, creationDate: DateTime, tokenGenerationDate: DateTime, tenants: NodeSecurityContext -) +) { + def toNewApiAccount(secret: ApiTokenSecret): NewApiAccount = { + NewApiAccount( + id, + kind, + name, + secret, + description, + isEnabled, + creationDate, + tokenGenerationDate, + tenants + ) + } +} + +/** + * An API principal, containing the secret, to be used just after creation, and never stored. + */ +final case class NewApiAccount( + id: ApiAccountId, + kind: ApiAccountKind, + name: ApiAccountName, + // Clear text token, only used for just-created accounts, never stored + token: ApiTokenSecret, + description: String, + isEnabled: Boolean, + creationDate: DateTime, + tokenGenerationDate: DateTime, + tenants: NodeSecurityContext +) { + def toApiAccount(): ApiAccount = { + ApiAccount( + id, + kind, + name, + token.hash(), + description, + isEnabled, + creationDate, + tokenGenerationDate, + tenants + ) + } +} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/TokenGenerator.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/TokenGenerator.scala index 50ba5e268e7..ea73a14b1f1 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/TokenGenerator.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/TokenGenerator.scala @@ -40,7 +40,7 @@ import java.security.SecureRandom import scala.util.Random /** - * Generate random string usable as token + * Generate a random string usable as secret (token, password, etc.) */ trait TokenGenerator { diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/RestDataSerializer.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/RestDataSerializer.scala index b72f7625383..bf01eeaee02 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/RestDataSerializer.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/apidata/RestDataSerializer.scala @@ -41,19 +41,24 @@ import com.normation.cfclerk.domain.* import com.normation.cfclerk.services.TechniqueRepository import com.normation.inventory.domain.NodeId import com.normation.rudder.api.ApiAccount +import com.normation.rudder.api.ApiAccountId +import com.normation.rudder.api.ApiAccountKind import com.normation.rudder.api.ApiAccountKind.PublicApi as PublicApiAccount import com.normation.rudder.api.ApiAccountKind.System import com.normation.rudder.api.ApiAccountKind.User +import com.normation.rudder.api.ApiAccountName import com.normation.rudder.api.ApiAuthorization.ACL import com.normation.rudder.api.ApiAuthorization.None as NoAccess import com.normation.rudder.api.ApiAuthorization.RO import com.normation.rudder.api.ApiAuthorization.RW import com.normation.rudder.api.ApiVersion +import com.normation.rudder.api.NewApiAccount import com.normation.rudder.domain.nodes.* import com.normation.rudder.domain.policies.* import com.normation.rudder.domain.properties.* import com.normation.rudder.domain.queries.Query import com.normation.rudder.domain.workflows.* +import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.repository.FullActiveTechnique import com.normation.rudder.repository.FullNodeGroupCategory import com.normation.rudder.rule.category.RuleCategory @@ -67,6 +72,7 @@ import com.normation.utils.DateFormaterService import net.liftweb.common.* import net.liftweb.json.* import net.liftweb.json.JsonDSL.* +import org.joda.time.DateTime import zio.json.ast.Json import zio.json.ast.Json.Str @@ -604,33 +610,76 @@ object ApiAccountSerialisation { implicit val formats: Formats = DefaultFormats + def toJsonPartial( + kind: ApiAccountKind, + id: ApiAccountId, + name: ApiAccountName, + description: String, + tokenGenerationDate: DateTime, + creationDate: DateTime, + isEnabled: Boolean, + tenants: NodeSecurityContext + ): JObject = { + val (expirationDate, authzType, acl): (Option[String], Option[String], Option[List[JsonApiAcl]]) = { + kind match { + case User | System => (None, None, None) + case PublicApiAccount(authz, expirationDate) => + val acl = authz match { + case NoAccess | RO | RW => None + case ACL(acls) => Some(acls.flatMap(x => x.actions.map(a => JsonApiAcl(x.path.value, a.name)))) + } + (expirationDate.map(DateFormaterService.getDisplayDateTimePicker), Some(authz.kind.name), acl) + } + } + + ("id" -> id.value) ~ + ("name" -> name.value) ~ + ("tokenGenerationDate" -> DateFormaterService.serialize(tokenGenerationDate)) ~ + ("kind" -> kind.kind.name) ~ + ("description" -> description) ~ + ("creationDate" -> DateFormaterService.serialize(creationDate)) ~ + ("enabled" -> isEnabled) ~ + ("expirationDate" -> expirationDate) ~ + ("expirationDateDefined" -> expirationDate.isDefined) ~ + ("authorizationType" -> authzType) ~ + ("acl" -> acl.map(x => Extraction.decompose(x))) ~ + ("tenants" -> tenants.serialize) + } + implicit class Json(val account: ApiAccount) extends AnyVal { def toJson: JObject = { - val (expirationDate, authzType, acl): (Option[String], Option[String], Option[List[JsonApiAcl]]) = { - account.kind match { - case User | System => (None, None, None) - case PublicApiAccount(authz, expirationDate) => - val acl = authz match { - case NoAccess | RO | RW => None - case ACL(acls) => Some(acls.flatMap(x => x.actions.map(a => JsonApiAcl(x.path.value, a.name)))) - } - (expirationDate.map(DateFormaterService.getDisplayDateTimePicker), Some(authz.kind.name), acl) - } - } + toJsonPartial( + account.kind, + account.id, + account.name, + account.description, + account.tokenGenerationDate, + account.creationDate, + account.isEnabled, + account.tenants + ) ~ + ("token" -> account.token.version().toString) + } + } +} + +object NewApiAccountSerialisation { - ("id" -> account.id.value) ~ - ("name" -> account.name.value) ~ - ("token" -> account.token.value) ~ - ("tokenGenerationDate" -> DateFormaterService.serialize(account.tokenGenerationDate)) ~ - ("kind" -> account.kind.kind.name) ~ - ("description" -> account.description) ~ - ("creationDate" -> DateFormaterService.serialize(account.creationDate)) ~ - ("enabled" -> account.isEnabled) ~ - ("expirationDate" -> expirationDate) ~ - ("expirationDateDefined" -> expirationDate.isDefined) ~ - ("authorizationType" -> authzType) ~ - ("acl" -> acl.map(x => Extraction.decompose(x))) ~ - ("tenants" -> account.tenants.serialize) + implicit val formats: Formats = DefaultFormats + + implicit class NewJson(val account: NewApiAccount) extends AnyVal { + def toJson: JObject = { + ApiAccountSerialisation.toJsonPartial( + account.kind, + account.id, + account.name, + account.description, + account.tokenGenerationDate, + account.creationDate, + account.isEnabled, + account.tenants + ) ~ + ("token" -> account.token.exposeSecret()) } } } diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala index cc3bdde43e3..bebd2325faa 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala @@ -596,7 +596,7 @@ class LDAPDiffMapper( } case A_API_TOKEN => nonNull(diff, mod.getOptValueDefault("")) { (d, value) => - d.copy(modToken = Some(SimpleDiff(oldAccount.token.value, value))) + d.copy(modToken = Some(SimpleDiff(oldAccount.token.exposeHash(), value))) } case A_DESCRIPTION => nonNull(diff, mod.getOptValueDefault("")) { (d, value) => diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala index 26eac9d68f3..69fda6c1389 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala @@ -1046,7 +1046,7 @@ class LDAPEntityMapper( for { id <- e.required(A_API_UUID).map(ApiAccountId(_)) name <- e.required(A_NAME).map(ApiAccountName(_)) - token <- e.required(A_API_TOKEN).map(ApiToken(_)) + token <- e.required(A_API_TOKEN).map(ApiTokenHash(_)) creationDatetime <- e.requiredAs[GeneralizedTime](_.getAsGTime, A_CREATION_DATETIME) tokenCreationDatetime <- e.requiredAs[GeneralizedTime](_.getAsGTime, A_API_TOKEN_CREATION_DATETIME) isEnabled = e.getAsBoolean(A_IS_ENABLED).getOrElse(false) @@ -1142,7 +1142,7 @@ class LDAPEntityMapper( mod.resetValuesTo(A_API_UUID, principal.id.value) mod.resetValuesTo(A_NAME, principal.name.value) mod.resetValuesTo(A_CREATION_DATETIME, GeneralizedTime(principal.creationDate).toString) - mod.resetValuesTo(A_API_TOKEN, principal.token.value) + mod.resetValuesTo(A_API_TOKEN, principal.token.exposeHash()) mod.resetValuesTo(A_API_TOKEN_CREATION_DATETIME, GeneralizedTime(principal.tokenGenerationDate).toString) mod.resetValuesTo(A_DESCRIPTION, principal.description) mod.resetValuesTo(A_IS_ENABLED, principal.isEnabled.toLDAPString) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlSerialisationImpl.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlSerialisationImpl.scala index e3a58e8b383..58186aa6f43 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlSerialisationImpl.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlSerialisationImpl.scala @@ -536,7 +536,7 @@ class APIAccountSerialisationImpl(xmlVersion: String) extends APIAccountSerialis ( {account.id.value} {account.name.value} - {account.token.value} + {account.token.exposeHash()} {account.description} {account.isEnabled} {account.creationDate.toString(ISODateTimeFormat.dateTime)} diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala index 02d38330bc2..56ce40bf24f 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala @@ -875,7 +875,7 @@ class ApiAccountUnserialisationImpl extends ApiAccountUnserialisation { ApiAccountId(id), kind, ApiAccountName(name), - ApiToken(token), + ApiTokenHash(token), description, isEnabled, creationDate, diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala index 97107676341..d816901f5a8 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala @@ -41,21 +41,85 @@ import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner +final class TestTokenGenerator extends TokenGenerator { + override def newToken(size: Int): String = "a" * size +} + @RunWith(classOf[JUnitRunner]) class TestApiToken extends Specification { "API tokens" should { + "be generated from generator" in { + val token = ApiTokenSecret.generate(new TestTokenGenerator) + token.exposeSecret() must beEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + } + "be generated with suffix" in { + val token = ApiTokenSecret.generate(new TestTokenGenerator, "end") + token.exposeSecret() must beEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-end") + } + "not be comparable" in { + val token = ApiTokenSecret.generate(new TestTokenGenerator, "end") + token.equals(token) must beFalse + } + "be hidden in strings" in { + val token = ApiTokenSecret("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.toString() must beEqualTo("[REDACTED ApiTokenSecret]") + } + "be partly hidden in controlled exposure" in { + val token = ApiTokenSecret("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.exposeSecretBeginning must beEqualTo("UBeJ[SHORTENED ApiTokenSecret]") + val shortToken = ApiTokenSecret("test") + shortToken.exposeSecretBeginning must beEqualTo("test[SHORTENED ApiTokenSecret]") + val veryShortToken = ApiTokenSecret("t") + veryShortToken.exposeSecretBeginning must beEqualTo("t[SHORTENED ApiTokenSecret]") + } + "be hashed" in { + val token = ApiTokenSecret("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.hash() must beEqualTo( + ApiTokenHash( + "v2:100caab9f3996edb04119ad4b2647b45150b10f75007b86bd82cdd0b7a9b009e2d5327115b3153bc4dc31bbbc775c6257f63f64a31f3c2d3924f11e8d24855bc" + ) + ) + } + } + + "Hashed API tokens" should { "be hidden in strings" in { - val token = ApiToken("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") - token.toString() must beEqualTo("[REDACTED ApiToken]") - } - "be partly hidden in controled exposure" in { - val token = ApiToken("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") - token.exposeSecretBeginning must beEqualTo("UBeJ[SHORTENED ApiToken]") - val shortToken = ApiToken("test") - shortToken.exposeSecretBeginning must beEqualTo("test[SHORTENED ApiToken]") - val veryShortToken = ApiToken("t") - veryShortToken.exposeSecretBeginning must beEqualTo("t[SHORTENED ApiToken]") + val token = ApiTokenHash("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.toString() must beEqualTo("[REDACTED ApiTokenHash]") + } + + "be compared correctly" in { + val hash = "UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY" + val token1 = ApiTokenHash(hash) + val token2 = ApiTokenHash(hash) + val token3 = ApiTokenHash(hash + "z") + val token4 = ApiTokenSecret(hash) + val token5 = ApiTokenHash("") + token1.equals(token2) must beTrue + token1.equals(token3) must beFalse + token1.equals(token4) must beFalse + token1.equals(token5) must beFalse + } + + "have correct version 1" in { + val token = ApiTokenHash("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.version() must beEqualTo(1) + } + + "have correct version 2" in { + val token = ApiTokenHash("v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.version() must beEqualTo(2) + } + + "have correct version 2 with empty hash" in { + val token = ApiTokenHash("v2:") + token.version() must beEqualTo(2) + } + + "not recognize version 3" in { + val token = ApiTokenHash("v3:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + token.version() must beEqualTo(1) } } } diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala index 5a6c091b556..2c0615bd5e4 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala @@ -6,6 +6,7 @@ import com.normation.rudder.api.ApiAuthorization as ApiAuthz import com.normation.rudder.api.RoApiAccountRepository import com.normation.rudder.api.WoApiAccountRepository import com.normation.rudder.apidata.ApiAccountSerialisation.* +import com.normation.rudder.apidata.NewApiAccountSerialisation.* import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.rest.RestUtils.* import com.normation.rudder.tenants.TenantService @@ -49,21 +50,12 @@ class RestApiAccounts( // and we want to avoid escalation OldInternalApiAuthz.withWriteAdmin(readApi.getAllStandardAccounts.either.runNow match { case Right(accountSeq) => - val filtered = accountSeq.toList - .map((a) => { - // Don't send hashes - a.copy(token = if (a.token.isHashed) { - ApiToken("") - } else { - a.token - }) - }) val accounts = { ( ("aclPluginEnabled" -> apiAuthService.aclEnabled) ~ ("tenantsPluginEnabled" -> tenantsService.tenantsEnabled) ~ ("accounts" -> JArray( - filtered.map(_.toJson) + accountSeq.toList.map(_.toJson) )) ) } @@ -96,31 +88,26 @@ class RestApiAccounts( val expiration = restApiAccount.expiration.getOrElse(Some(now.plusMonths(1))) val acl = restApiAccount.authz.getOrElse(ApiAuthz.None) - val secret = ApiToken.generate_secret(tokenGenerator) - val hash = ApiToken.hash(secret) - val account = ApiAccount( + val secret = ApiTokenSecret.generate(tokenGenerator) + val newAccount = NewApiAccount( id, ApiAccountKind.PublicApi(acl, expiration), restApiAccount.name.get, - ApiToken(hash), + secret, restApiAccount.description.getOrElse(""), restApiAccount.enabled.getOrElse(true), now, now, restApiAccount.tenants.getOrElse(NodeSecurityContext.All) ) + val account = newAccount.toApiAccount() writeApi.save(account, ModificationId(uuidGen.newUuid), userService.getCurrentUser.actor).either.runNow match { case Right(_) => - val accounts = ("accounts" -> JArray( + val accounts = "accounts" -> JArray( List( - account - .copy( - // Send clear text secret - token = ApiToken(secret) - ) - .toJson + newAccount.toJson ) - )) + ) toJsonResponse(None, accounts) case Left(err) => @@ -195,12 +182,7 @@ class RestApiAccounts( case Right(Some(account)) => writeApi.delete(account.id, ModificationId(uuidGen.newUuid), userService.getCurrentUser.actor).either.runNow match { case Right(_) => - val filtered = account.copy(token = if (account.token.isHashed) { - ApiToken("") - } else { - account.token - }) - val accounts = ("accounts" -> JArray(List(filtered.toJson))) + val accounts = ("accounts" -> JArray(List(account.toJson))) toJsonResponse(None, accounts) case Left(err) => @@ -225,13 +207,13 @@ class RestApiAccounts( OldInternalApiAuthz.withWriteAdmin(readApi.getById(apiTokenId).either.runNow match { case Right(Some(account)) => - val newSecret = ApiToken.generate_secret(tokenGenerator) - val newHash = ApiToken.hash(newSecret) + val newSecret = ApiTokenSecret.generate(tokenGenerator) + val newHash = newSecret.hash() val generationDate = DateTime.now writeApi .save( - account.copy(token = ApiToken(newHash), tokenGenerationDate = generationDate), + account.copy(token = newHash, tokenGenerationDate = generationDate), ModificationId(uuidGen.newUuid), userService.getCurrentUser.actor ) @@ -240,12 +222,7 @@ class RestApiAccounts( case Right(account) => val accounts = ("accounts" -> JArray( List( - account - .copy( - // Send clear text secret - token = ApiToken(newSecret) - ) - .toJson + account.toNewApiAccount(newSecret).toJson ) )) toJsonResponse(None, accounts) @@ -274,12 +251,7 @@ class RestApiAccounts( def save(account: ApiAccount)(implicit action: String, prettify: Boolean): LiftResponse = { writeApi.save(account, ModificationId(uuidGen.newUuid), userService.getCurrentUser.actor).either.runNow match { case Right(res) => - val filtered = res.copy(token = if (res.token.isHashed) { - ApiToken("") - } else { - res.token - }) - val accounts = ("accounts" -> JArray(List(filtered.toJson))) + val accounts = ("accounts" -> JArray(List(res.toJson))) toJsonResponse(None, accounts) case Left(err) => diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala index 1b238fb6214..10fcf7565bd 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala @@ -123,7 +123,7 @@ case class RudderUserDetail( override val (getUsername, getPassword, getAuthorities) = account match { case RudderAccount.User(login, password) => (login, password, RudderAuthType.User.grantedAuthorities) - case RudderAccount.Api(api) => (api.name.value, api.token.value, RudderAuthType.Api.grantedAuthorities) + case RudderAccount.Api(api) => (api.name.value, api.token.exposeHash(), RudderAuthType.Api.grantedAuthorities) } override val isAccountNonExpired = true override val isAccountNonLocked = true diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsGenerator.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsGenerator.scala index 88533b3b285..25e0ed0dc87 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsGenerator.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/web/services/EventLogDetailsGenerator.scala @@ -1320,7 +1320,7 @@ class EventLogDetailsGenerator( private def apiAccountDetails(xml: NodeSeq, apiAccount: ApiAccount) = ( "#id" #> apiAccount.id.value & "#name" #> apiAccount.name.value & - "#token" #> apiAccount.token.value & + "#token" #> apiAccount.token.exposeHash() & "#description" #> apiAccount.description & "#isEnabled" #> apiAccount.isEnabled & "#creationDate" #> DateFormaterService.getDisplayDate(apiAccount.creationDate) & diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts.elm index 1fbd3046dfc..c02bf8dc99f 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts.elm @@ -2,6 +2,7 @@ module Accounts exposing (..) import Accounts.ApiCalls exposing (..) import Accounts.DataTypes as TenantMode exposing (..) +import Accounts.DataTypes as Token exposing (..) import Accounts.DatePickerUtils exposing (..) import Accounts.Init exposing (..) import Accounts.JsonDecoder exposing (decodeErrorDetails) @@ -87,7 +88,7 @@ update msg model = editAccount = case modalState of NewAccount -> - Just (Account "" "" "" "rw" "" True "" "" "" True (Just expDate) Nothing TenantMode.AllAccess Nothing) + Just (Account "" "" "" "rw" "" True "" Token.Hashed "" True (Just expDate) Nothing TenantMode.AllAccess Nothing) EditAccount a -> Just a @@ -162,7 +163,7 @@ update msg model = (modalState, action) = case ui.modalState of NewAccount -> - ( CopyToken account.token + ( CopyToken (exposeToken account.token) , "created" ) @@ -192,7 +193,7 @@ update msg model = ) Regenerate -> - ( CopyToken account.token + ( CopyToken (exposeToken account.token) , "regenerated token of" ) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/DataTypes.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/DataTypes.elm index 04d88b4ea31..0be3a01ec6b 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/DataTypes.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/DataTypes.elm @@ -38,6 +38,10 @@ type TenantMode | NoAccess -- special "-" permission giving access to no object, whatever the tenant or its absence | ByTenants --give access to object in any of the listed tenants +type Token + = New String + | Hashed + | ClearText type alias DatePickerInfo = { currentTime : Posix @@ -69,7 +73,7 @@ type alias Account = , kind : String , enabled : Bool , creationDate : String - , token : String + , token : Token , tokenGenerationDate : String , expirationDateDefined : Bool , expirationDate : Maybe Posix diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonDecoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonDecoder.elm index b8be524b4db..5ef3f2beab2 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonDecoder.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonDecoder.elm @@ -1,6 +1,7 @@ module Accounts.JsonDecoder exposing (..) import Accounts.DataTypes as TenantMode exposing (..) +import Accounts.DataTypes as Token exposing (..) import Accounts.DatePickerUtils exposing (stringToPosix) import Json.Decode exposing (..) import Json.Decode.Pipeline exposing (..) @@ -26,7 +27,7 @@ decodeAccount datePickerInfo = |> required "kind" string |> required "enabled" bool |> required "creationDate" string - |> required "token" string + |> required "token" (string |> andThen toToken) |> required "tokenGenerationDate" string |> required "expirationDateDefined" bool |> optional "expirationDate" @@ -54,9 +55,19 @@ decodeAcl = |> required "verb" string +parseToken : String -> ( Token ) +parseToken str = + case str of + "2" -> Token.Hashed + "1" -> Token.ClearText + _ -> Token.New str + +toToken : String -> Decoder Token +toToken str = + succeed (parseToken str) --- the string for a tenant mode is '*', '-', or a comma separated list of non-empty string +-- the string for a tenant mode is '*', '-', or a comma separated list of non-empty string parseTenants : String -> ( TenantMode, Maybe (List String) ) parseTenants str = diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonEncoder.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonEncoder.elm index f064fff3e3d..d10f369d8ec 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonEncoder.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/JsonEncoder.elm @@ -32,7 +32,6 @@ encodeAccount datePickerInfo account = , ( "kind", string account.kind ) , ( "enabled", bool account.enabled ) , ( "creationDate", string account.creationDate ) - , ( "token", string account.token ) , ( "tokenGenerationDate", string account.tokenGenerationDate ) , ( "expirationDateDefined", bool expirationDateDefined ) , ( "tenants", string (encodeTenants account.tenantMode account.selectedTenants) ) diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/View.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/View.elm index b9a79a42b1f..32b7d7c7980 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/View.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/View.elm @@ -2,20 +2,20 @@ module Accounts.View exposing (..) import Accounts.ApiCalls exposing (..) import Accounts.DataTypes exposing (..) +import Accounts.DataTypes as Token exposing (..) import Accounts.ViewModals exposing (..) import Accounts.ViewUtils exposing (..) import Html exposing (..) import Html.Attributes exposing (attribute, class, disabled, href, placeholder, selected, type_, value) import Html.Events exposing (onClick, onInput) import List -import String view : Model -> Html Msg view model = let hasClearTextTokens = - List.any (\a -> String.length a.token > 0) model.accounts + List.any (\a -> a.token == Token.ClearText) model.accounts in div [ class "rudder-template" ] [ div [ class "one-col" ] @@ -41,9 +41,8 @@ view model = [ if hasClearTextTokens then div [ class "alert alert-warning" ] [ i [ class "fa fa-exclamation-triangle" ] [] - , text "You have API accounts with tokens generated on a previous Rudder versions, those for which the " - , text "beginning of the token value is displayed in the table. They are now deprecated, you should " - , text "re-generate or replace them for improved security." + , text "You have API accounts with tokens generated on an pre-8.1 Rudder versions." + , text "They are now disabled, you should re-generate or replace them." ] else diff --git a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/ViewUtils.elm b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/ViewUtils.elm index 8170889d782..11b58940afc 100644 --- a/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/ViewUtils.elm +++ b/webapp/sources/rudder/rudder-web/src/main/elm/sources/Accounts/ViewUtils.elm @@ -48,7 +48,6 @@ getSortFunction model a1 a2 = searchField datePickerInfo a = List.append [ a.name , a.id - , a.token ] ( case a.expirationDate of Just d -> [posixToString datePickerInfo d] Nothing -> [] @@ -78,10 +77,8 @@ generateLoadingList = displayAccountsTable : Model -> Html Msg displayAccountsTable model = let - hasClearTextTokens = List.any (\a -> (String.length a.token) > 0) model.accounts - - trAccount : Account -> Bool -> Html Msg - trAccount a showTokens = + trAccount : Account -> Html Msg + trAccount a = let inputId = "toggle-" ++ a.id expirationDate = case a.expirationDate of @@ -98,19 +95,7 @@ displayAccountsTable model = ] , td [] [ span [class "token-txt"][ text a.id ] ] - , if showTokens then - if isEmpty a.token then - td [class "token"] [ span [class "token-txt"][ text "[hashed]" ] ] - else - td [class "token"] - [ span [class "token-txt"] - [text (slice 0 5 a.token)] - , span[class "fa hide-text"][] - , Html.a [ class "btn-goto clipboard", title "Copy to clipboard" , onClick (Copy a.token) ] - [ i [class "ion ion-clipboard"][] ] - ] - else - td [class "date"][ text (cleanDate a.creationDate) ] + , td [class "date"][ text (cleanDate a.creationDate) ] , td [class "date"][ text expirationDate ] , td [] [ button @@ -151,10 +136,7 @@ displayAccountsTable model = [ tr [class "head"] [ th [class (thClass tableFilters Name ), onClick (UpdateFilters {filters | tableFilters = (sortTable tableFilters Name )})][ text "Account name" ] , th [class (thClass tableFilters Id ), onClick (UpdateFilters {filters | tableFilters = (sortTable tableFilters Id )})][ text "Account id" ] - , if hasClearTextTokens then - th [][ text "Token" ] - else - th [class (thClass tableFilters CreDate ), onClick (UpdateFilters {filters | tableFilters = (sortTable tableFilters CreDate )})][ text "Creation date" ] + , th [class (thClass tableFilters CreDate ), onClick (UpdateFilters {filters | tableFilters = (sortTable tableFilters CreDate )})][ text "Creation date" ] , th [class (thClass tableFilters ExpDate ), onClick (UpdateFilters {filters | tableFilters = (sortTable tableFilters ExpDate )})][ text "Expiration date" ] , th [][ text "Actions" ] ] @@ -166,10 +148,10 @@ displayAccountsTable model = ] else if List.isEmpty filteredAccounts then [ tr[] - [ td[class "empty", colspan 4][i [class"fa fa-exclamation-triangle"][], text "No api accounts match your filters"] ] + [ td[class "empty", colspan 4][i [class"fa fa-exclamation-triangle"][], text "No API accounts match your filters"] ] ] else - List.map (\a -> trAccount a hasClearTextTokens) filteredAccounts + List.map (\a -> trAccount a) filteredAccounts ) ] @@ -221,3 +203,9 @@ htmlEscape s = |> String.replace "\"" """ |> String.replace "'" "'" |> String.replace "\\" "/" + +exposeToken : Token -> String +exposeToken t = + case t of + New s -> s + _ -> "" diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala index 1bb29ceee39..43ab848a4e0 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala @@ -747,7 +747,7 @@ class RestAuthenticationFilter( ), // un-authenticated APIv1 token certainly doesn't get any authz on v2 API ApiAccountName(name), - ApiToken(name), + ApiTokenHash.neverMatch, "API Account for un-authenticated API", isEnabled = true, creationDate = new DateTime(0), @@ -775,9 +775,10 @@ class RestAuthenticationFilter( case token => // try to authenticate - val apiToken = ApiToken(token) + val apiToken = ApiTokenSecret(token) + val apiTokenHash = apiToken.hash() val systemAccount = apiTokenRepository.getSystemAccount - if (systemAccount.token == apiToken) { // system token with super authz + if (systemAccount.token == apiTokenHash) { // system token with super authz authenticate( RudderUserDetail( RudderAccount.Api(systemAccount), @@ -790,7 +791,7 @@ class RestAuthenticationFilter( chain.doFilter(request, response) } else { // standard token, try to find it in DB - apiTokenRepository.getByToken(apiToken).either.runNow match { + apiTokenRepository.getByToken(apiTokenHash).either.runNow match { case Left(err) => failsAuthentication(httpRequest, httpResponse, err) diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala index 69ffef5e1b2..363ca7b01df 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala @@ -2222,12 +2222,14 @@ object RudderConfigInit { //// everything was private //////////////////////////////////////////////////////////////////// + lazy val systemTokenSecret = ApiTokenSecret.generate(tokenGenerator, suffix = "system") + lazy val roLDAPApiAccountRepository = new RoLDAPApiAccountRepository( rudderDitImpl, roLdap, ldapEntityMapper, - tokenGenerator, - ApiAuthorization.allAuthz.acl // for system token + ApiAuthorization.allAuthz.acl, // for system token + systemTokenSecret.hash() ) lazy val roApiAccountRepository: RoApiAccountRepository = roLDAPApiAccountRepository @@ -3281,7 +3283,7 @@ object RudderConfigInit { uuidGen ), new CreateSystemToken( - roLDAPApiAccountRepository.systemAPIAccount.token, + systemTokenSecret, root / "var" / "rudder" / "run", RestAuthenticationFilter.API_TOKEN_HEADER ), diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/checks/action/CreateSystemToken.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/checks/action/CreateSystemToken.scala index 7d4434621f3..42fd9e7cac8 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/checks/action/CreateSystemToken.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/checks/action/CreateSystemToken.scala @@ -41,7 +41,7 @@ import better.files.File import bootstrap.liftweb.BootstrapChecks import bootstrap.liftweb.BootstrapLogger import com.normation.errors.IOResult -import com.normation.rudder.api.ApiToken +import com.normation.rudder.api.ApiTokenSecret import com.normation.zio.UnsafeRun import java.nio.file.attribute.PosixFilePermissions import scala.jdk.CollectionConverters.* @@ -49,14 +49,14 @@ import scala.jdk.CollectionConverters.* /** * Create an API token file at webapp startup to use for internal use. */ -class CreateSystemToken(systemToken: ApiToken, runDir: File, apiTokenHeaderName: String) extends BootstrapChecks { +class CreateSystemToken(systemToken: ApiTokenSecret, runDir: File, apiTokenHeaderName: String) extends BootstrapChecks { import CreateSystemToken.* override val description = "Create system API token files" override def checks(): Unit = { (for { - token <- restrictedPermissionsWrite(runDir / tokenFile, systemToken.value) + token <- restrictedPermissionsWrite(runDir / tokenFile, systemToken.exposeSecret()) // Allows easier usage in scripts, and in particular prevents making the token value visible in // process list by using the "--header @file" syntax in curl to read the file. header <- restrictedPermissionsWrite(runDir / tokenHeaderFile, curlTokenHeader) @@ -77,7 +77,7 @@ class CreateSystemToken(systemToken: ApiToken, runDir: File, apiTokenHeaderName: } private val curlTokenHeader: String = { - s"${apiTokenHeaderName}: ${systemToken.value}" + s"${apiTokenHeaderName}: ${systemToken.exposeSecret()}" } } diff --git a/webapp/sources/rudder/rudder-web/src/test/scala/bootstrap/liftweb/checks/action/TestCreateSystemToken.scala b/webapp/sources/rudder/rudder-web/src/test/scala/bootstrap/liftweb/checks/action/TestCreateSystemToken.scala index 06a302272db..87aa806c5cf 100644 --- a/webapp/sources/rudder/rudder-web/src/test/scala/bootstrap/liftweb/checks/action/TestCreateSystemToken.scala +++ b/webapp/sources/rudder/rudder-web/src/test/scala/bootstrap/liftweb/checks/action/TestCreateSystemToken.scala @@ -38,7 +38,7 @@ package bootstrap.liftweb.checks.action import better.files.File -import com.normation.rudder.api.ApiToken +import com.normation.rudder.api.ApiTokenSecret import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -47,7 +47,7 @@ import org.specs2.runner.JUnitRunner class TestCreateSystemToken extends Specification { private val token = "ohhcRCnPiRP67CuuxKDVMuig0AQqjKVo-system" - private val apiToken = ApiToken(token) + private val apiToken = ApiTokenSecret(token) "When writing the system tokens, we" should { "generate proper header and token files" in File.temporaryDirectory("rudder-test-system-token-") { tmpDir => From 59937dc6c75c8153bc41b9887e53d04325e29aa9 Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 25 Nov 2024 15:52:46 +0100 Subject: [PATCH 2/4] fixup! Fixes #25903: Refactor API tokens after clear-text removal Fixes #25903: Refactor API tokens after clear-text removal --- .../rudder/api/ApiAccountRepository.scala | 14 +++----------- .../com/normation/rudder/api/DataStructures.scala | 13 ++++++------- .../com/normation/rudder/api/ApiTokenTest.scala | 12 +++--------- .../scala/bootstrap/liftweb/AppConfigAuth.scala | 2 +- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala index e35c88febb6..8f88c88b2a1 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala @@ -139,17 +139,9 @@ final class RoLDAPApiAccountRepository( } } - // Here the process is: - // - // * Ensure it is a clear-text token - // * Check if token matches in-memory system account - // * Then look for it in the LDAP: - // * First as a hash - // * Then, in fallback, as clear-text token - // - // Warning: When matching clear-text value we MUST make sure it is not - // a hash but a clear text token to avoid accepting the hash as valid token itself. - // + /* + Look for a given token hash in the LDAP. + */ override def getByToken(hashedToken: ApiTokenHash): IOResult[Option[ApiAccount]] = { for { ldap <- ldapConnexion diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala index 369993b4fda..def077d1864 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala @@ -67,9 +67,6 @@ final case class ApiTokenSecret(private val secret: String) { // Avoid printing the value in logs, regardless of token type override def toString: String = "[REDACTED ApiTokenSecret]" - // Prevent comparison - override def equals(obj: Any) = false - // For cases when we need to print a part of the plain token for debugging. // Show the first 4 chars: enough to disambiguate, and preserves 166 bits of randomness. def exposeSecretBeginning: String = { @@ -112,16 +109,18 @@ object ApiTokenSecret { * * We don't have generic code as V2 is (very) likely the last simple API key mechanism we'll need. * + * Token hash comparison must use the isEqual method. + * */ final case class ApiTokenHash(private val value: String) { override def toString: String = "[REDACTED ApiTokenHash]" // Constant time comparison - override def equals(obj: Any): Boolean = { - obj match { - case ApiTokenHash(other) => MessageDigest.isEqual(value.getBytes(), other.getBytes()) - case _ => false + def isEqual(other: ApiTokenHash): Boolean = { + other match { + case ApiTokenHash(hash) => MessageDigest.isEqual(value.getBytes(), hash.getBytes()) + case _ => false } } diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala index d816901f5a8..17d9aa55c63 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala @@ -57,10 +57,6 @@ class TestApiToken extends Specification { val token = ApiTokenSecret.generate(new TestTokenGenerator, "end") token.exposeSecret() must beEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-end") } - "not be comparable" in { - val token = ApiTokenSecret.generate(new TestTokenGenerator, "end") - token.equals(token) must beFalse - } "be hidden in strings" in { val token = ApiTokenSecret("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") token.toString() must beEqualTo("[REDACTED ApiTokenSecret]") @@ -94,12 +90,10 @@ class TestApiToken extends Specification { val token1 = ApiTokenHash(hash) val token2 = ApiTokenHash(hash) val token3 = ApiTokenHash(hash + "z") - val token4 = ApiTokenSecret(hash) val token5 = ApiTokenHash("") - token1.equals(token2) must beTrue - token1.equals(token3) must beFalse - token1.equals(token4) must beFalse - token1.equals(token5) must beFalse + token1.isEqual(token2) must beTrue + token1.isEqual(token3) must beFalse + token1.isEqual(token5) must beFalse } "have correct version 1" in { diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala index 43ab848a4e0..333981c9e59 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala @@ -778,7 +778,7 @@ class RestAuthenticationFilter( val apiToken = ApiTokenSecret(token) val apiTokenHash = apiToken.hash() val systemAccount = apiTokenRepository.getSystemAccount - if (systemAccount.token == apiTokenHash) { // system token with super authz + if (systemAccount.token.isEqual(apiTokenHash)) { // system token with super authz authenticate( RudderUserDetail( RudderAccount.Api(systemAccount), From 8224c52cf761a775fc559b3fb14eb80db03f4b3a Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Mon, 25 Nov 2024 19:18:46 +0100 Subject: [PATCH 3/4] fixup! fixup! Fixes #25903: Refactor API tokens after clear-text removal Fixes #25903: Refactor API tokens after clear-text removal --- .../rudder/api/ApiAccountRepository.scala | 6 +- .../normation/rudder/api/DataStructures.scala | 81 +++++++++++++------ .../repository/ldap/LDAPDiffMapper.scala | 2 +- .../repository/ldap/LDAPEntityMapper.scala | 4 +- .../marshalling/XmlUnserialisationImpl.scala | 2 +- .../normation/rudder/api/ApiTokenTest.scala | 46 +++++++---- .../bootstrap/liftweb/AppConfigAuth.scala | 6 +- .../bootstrap/liftweb/RudderConfig.scala | 2 +- 8 files changed, 97 insertions(+), 52 deletions(-) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala index 8f88c88b2a1..566e93efde1 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/ApiAccountRepository.scala @@ -143,10 +143,14 @@ final class RoLDAPApiAccountRepository( Look for a given token hash in the LDAP. */ override def getByToken(hashedToken: ApiTokenHash): IOResult[Option[ApiAccount]] = { + val hash = hashedToken.exposeHash(); for { ldap <- ldapConnexion // here, be careful to the semantic of get with a filter! - optEntry <- ldap.get(rudderDit.API_ACCOUNTS.dn, BuildFilter.EQ(RudderLDAPConstants.A_API_TOKEN, hashedToken.exposeHash())) + optEntry <- hash match { + case None => None.succeed + case Some(h) => ldap.get(rudderDit.API_ACCOUNTS.dn, BuildFilter.EQ(RudderLDAPConstants.A_API_TOKEN, h)) + } optRes <- optEntry match { case None => None.succeed case Some(e) => mapper.entry2ApiAccount(e).map(Some(_)).toIO diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala index def077d1864..ed4a74f277a 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/api/DataStructures.scala @@ -77,8 +77,8 @@ final case class ApiTokenSecret(private val secret: String) { secret } - def hash(): ApiTokenHash = { - ApiTokenHash.hash(this) + def toHash(): ApiTokenHash = { + ApiTokenHash.fromSecret(this) } } @@ -113,45 +113,74 @@ object ApiTokenSecret { * */ -final case class ApiTokenHash(private val value: String) { +trait CompareToken { + def equalsToken(other: ApiTokenHash): Boolean +} + +sealed trait ApiTokenHash extends CompareToken { override def toString: String = "[REDACTED ApiTokenHash]" - // Constant time comparison - def isEqual(other: ApiTokenHash): Boolean = { - other match { - case ApiTokenHash(hash) => MessageDigest.isEqual(value.getBytes(), hash.getBytes()) - case _ => false + override def equalsToken(token: ApiTokenHash) = { + (this, token) match { + case (ApiTokenHash.V2(a), ApiTokenHash.V2(b)) => MessageDigest.isEqual(a.getBytes(), b.getBytes()) + case _ => false } } - def exposeHash(): String = { - value + /* + Deprecated or invalid hashes are not returned. + */ + def exposeHash(): Option[String] = { + this match { + case ApiTokenHash.V2(h) => Some(h) + case _ => None + } } def version(): Int = { - if (value.startsWith(ApiTokenHash.prefix)) { - 2 - } else { - 1 + this match { + case ApiTokenHash.V2(_) => 2 + case _ => 1 } } } object ApiTokenHash { - val prefix = "v2" - // Guaranteed to never match - val neverMatch = ApiTokenHash.build("not-matching") + // A hash that will never match + private case object Disabled extends ApiTokenHash + // A pre-hash API token. The value is not stored. + private case object V1 extends ApiTokenHash + private case class V2(private val value: String) extends ApiTokenHash + + private object V2 { + val prefix = "v2:" + + def hash(token: ApiTokenSecret): ApiTokenHash = { + val sha512Digest = MessageDigest.getInstance("SHA-512") + val hash = sha512Digest.digest(token.exposeSecret().getBytes(StandardCharsets.UTF_8)) + val hexHash = Hex.encode(hash) + val hexString = new String(hexHash, StandardCharsets.UTF_8) + V2(prefix + hexString) + } + } + + def fromHashValue(hash: String): ApiTokenHash = { + if (hash.startsWith(V2.prefix)) { + V2(hash) + } else { + // Don't bother to try to recognize old patterns. + // The hash is not used anyway. + V1 + } + } - // Build from hashed value - private def build(value: String): ApiTokenHash = { - ApiTokenHash(prefix + ":" + value) + // Use latest version for new hashes + def fromSecret(token: ApiTokenSecret): ApiTokenHash = { + V2.hash(token) } - def hash(token: ApiTokenSecret): ApiTokenHash = { - val sha512Digest = MessageDigest.getInstance("SHA-512") - val hash = sha512Digest.digest(token.exposeSecret().getBytes(StandardCharsets.UTF_8)) - val hexHash = Hex.encode(hash) - ApiTokenHash.build(new String(hexHash, StandardCharsets.UTF_8)) + def disabled(): ApiTokenHash = { + Disabled } } @@ -443,7 +472,7 @@ final case class NewApiAccount( id, kind, name, - token.hash(), + token.toHash(), description, isEnabled, creationDate, diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala index bebd2325faa..a130e48b905 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPDiffMapper.scala @@ -596,7 +596,7 @@ class LDAPDiffMapper( } case A_API_TOKEN => nonNull(diff, mod.getOptValueDefault("")) { (d, value) => - d.copy(modToken = Some(SimpleDiff(oldAccount.token.exposeHash(), value))) + d.copy(modToken = Some(SimpleDiff(oldAccount.token.exposeHash().getOrElse(""), value))) } case A_DESCRIPTION => nonNull(diff, mod.getOptValueDefault("")) { (d, value) => diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala index 69fda6c1389..472a3810bd6 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/repository/ldap/LDAPEntityMapper.scala @@ -1046,7 +1046,7 @@ class LDAPEntityMapper( for { id <- e.required(A_API_UUID).map(ApiAccountId(_)) name <- e.required(A_NAME).map(ApiAccountName(_)) - token <- e.required(A_API_TOKEN).map(ApiTokenHash(_)) + token <- e.required(A_API_TOKEN).map(ApiTokenHash.fromHashValue(_)) creationDatetime <- e.requiredAs[GeneralizedTime](_.getAsGTime, A_CREATION_DATETIME) tokenCreationDatetime <- e.requiredAs[GeneralizedTime](_.getAsGTime, A_API_TOKEN_CREATION_DATETIME) isEnabled = e.getAsBoolean(A_IS_ENABLED).getOrElse(false) @@ -1142,7 +1142,7 @@ class LDAPEntityMapper( mod.resetValuesTo(A_API_UUID, principal.id.value) mod.resetValuesTo(A_NAME, principal.name.value) mod.resetValuesTo(A_CREATION_DATETIME, GeneralizedTime(principal.creationDate).toString) - mod.resetValuesTo(A_API_TOKEN, principal.token.exposeHash()) + mod.resetValuesTo(A_API_TOKEN, principal.token.exposeHash().getOrElse("")) mod.resetValuesTo(A_API_TOKEN_CREATION_DATETIME, GeneralizedTime(principal.tokenGenerationDate).toString) mod.resetValuesTo(A_DESCRIPTION, principal.description) mod.resetValuesTo(A_IS_ENABLED, principal.isEnabled.toLDAPString) diff --git a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala index 56ce40bf24f..3cf507fb57a 100644 --- a/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala +++ b/webapp/sources/rudder/rudder-core/src/main/scala/com/normation/rudder/services/marshalling/XmlUnserialisationImpl.scala @@ -875,7 +875,7 @@ class ApiAccountUnserialisationImpl extends ApiAccountUnserialisation { ApiAccountId(id), kind, ApiAccountName(name), - ApiTokenHash(token), + ApiTokenHash.fromHashValue(token), description, isEnabled, creationDate, diff --git a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala index 17d9aa55c63..4b558f9dd0a 100644 --- a/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala +++ b/webapp/sources/rudder/rudder-core/src/test/scala/com/normation/rudder/api/ApiTokenTest.scala @@ -71,8 +71,8 @@ class TestApiToken extends Specification { } "be hashed" in { val token = ApiTokenSecret("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") - token.hash() must beEqualTo( - ApiTokenHash( + token.toHash() must beEqualTo( + ApiTokenHash.fromHashValue( "v2:100caab9f3996edb04119ad4b2647b45150b10f75007b86bd82cdd0b7a9b009e2d5327115b3153bc4dc31bbbc775c6257f63f64a31f3c2d3924f11e8d24855bc" ) ) @@ -81,38 +81,50 @@ class TestApiToken extends Specification { "Hashed API tokens" should { "be hidden in strings" in { - val token = ApiTokenHash("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") - token.toString() must beEqualTo("[REDACTED ApiTokenHash]") + val token = ApiTokenSecret.generate(new TestTokenGenerator) + val hash = token.toHash() + hash.toString() must beEqualTo("[REDACTED ApiTokenHash]") + } + + "be compared correctly when identical" in { + val hash = "v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY" + val token1 = ApiTokenHash.fromHashValue(hash) + val token2 = ApiTokenHash.fromHashValue(hash) + token1.equalsToken(token1) must beTrue + token1.equalsToken(token2) must beTrue + } + + "be compared correctly when different" in { + val hash = "v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY" + val token1 = ApiTokenHash.fromHashValue(hash) + val token2 = ApiTokenHash.fromHashValue(hash + "z") + token1.equalsToken(token2) must beFalse } - "be compared correctly" in { - val hash = "UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY" - val token1 = ApiTokenHash(hash) - val token2 = ApiTokenHash(hash) - val token3 = ApiTokenHash(hash + "z") - val token5 = ApiTokenHash("") - token1.isEqual(token2) must beTrue - token1.isEqual(token3) must beFalse - token1.isEqual(token5) must beFalse + "be compared correctly when different and empty" in { + val hash = "v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY" + val token1 = ApiTokenHash.fromHashValue(hash) + val token2 = ApiTokenHash.fromHashValue("") + token1.equalsToken(token2) must beFalse } "have correct version 1" in { - val token = ApiTokenHash("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + val token = ApiTokenHash.fromHashValue("UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") token.version() must beEqualTo(1) } "have correct version 2" in { - val token = ApiTokenHash("v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + val token = ApiTokenHash.fromHashValue("v2:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") token.version() must beEqualTo(2) } "have correct version 2 with empty hash" in { - val token = ApiTokenHash("v2:") + val token = ApiTokenHash.fromHashValue("v2:") token.version() must beEqualTo(2) } "not recognize version 3" in { - val token = ApiTokenHash("v3:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") + val token = ApiTokenHash.fromHashValue("v3:UBeJJbm1tPDwILWVHXqBdgmIm3s4xjtY") token.version() must beEqualTo(1) } } diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala index 333981c9e59..ddd38bf4e5a 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/AppConfigAuth.scala @@ -747,7 +747,7 @@ class RestAuthenticationFilter( ), // un-authenticated APIv1 token certainly doesn't get any authz on v2 API ApiAccountName(name), - ApiTokenHash.neverMatch, + ApiTokenHash.disabled(), "API Account for un-authenticated API", isEnabled = true, creationDate = new DateTime(0), @@ -776,9 +776,9 @@ class RestAuthenticationFilter( case token => // try to authenticate val apiToken = ApiTokenSecret(token) - val apiTokenHash = apiToken.hash() + val apiTokenHash = apiToken.toHash() val systemAccount = apiTokenRepository.getSystemAccount - if (systemAccount.token.isEqual(apiTokenHash)) { // system token with super authz + if (systemAccount.token.equalsToken(apiTokenHash)) { // system token with super authz authenticate( RudderUserDetail( RudderAccount.Api(systemAccount), diff --git a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala index 363ca7b01df..3b01c17b5fe 100644 --- a/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala +++ b/webapp/sources/rudder/rudder-web/src/main/scala/bootstrap/liftweb/RudderConfig.scala @@ -2229,7 +2229,7 @@ object RudderConfigInit { roLdap, ldapEntityMapper, ApiAuthorization.allAuthz.acl, // for system token - systemTokenSecret.hash() + systemTokenSecret.toHash() ) lazy val roApiAccountRepository: RoApiAccountRepository = roLDAPApiAccountRepository From 1ee29e5ae21707b85c547a0f86333f140638671d Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Wed, 27 Nov 2024 01:47:33 +0100 Subject: [PATCH 4/4] fixup! fixup! fixup! Fixes #25903: Refactor API tokens after clear-text removal Fixes #25903: Refactor API tokens after clear-text removal --- .../com/normation/rudder/rest/internal/RestApiAccounts.scala | 2 +- .../src/main/scala/com/normation/rudder/users/User.scala | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala index 2c0615bd5e4..4a5345b8310 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/rest/internal/RestApiAccounts.scala @@ -208,7 +208,7 @@ class RestApiAccounts( OldInternalApiAuthz.withWriteAdmin(readApi.getById(apiTokenId).either.runNow match { case Right(Some(account)) => val newSecret = ApiTokenSecret.generate(tokenGenerator) - val newHash = newSecret.hash() + val newHash = newSecret.toHash() val generationDate = DateTime.now writeApi diff --git a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala index 10fcf7565bd..5f41fa0b8bc 100644 --- a/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala +++ b/webapp/sources/rudder/rudder-rest/src/main/scala/com/normation/rudder/users/User.scala @@ -123,7 +123,8 @@ case class RudderUserDetail( override val (getUsername, getPassword, getAuthorities) = account match { case RudderAccount.User(login, password) => (login, password, RudderAuthType.User.grantedAuthorities) - case RudderAccount.Api(api) => (api.name.value, api.token.exposeHash(), RudderAuthType.Api.grantedAuthorities) + // We can default to "" as this value is ot used for authentication. + case RudderAccount.Api(api) => (api.name.value, api.token.exposeHash().getOrElse(""), RudderAuthType.Api.grantedAuthorities) } override val isAccountNonExpired = true override val isAccountNonLocked = true