diff --git a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala index 1444da078..79d774a93 100644 --- a/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala +++ b/auth-backends/src/main/scala/bootstrap/rudder/plugin/AuthBackendsConf.scala @@ -582,6 +582,7 @@ class AuthBackendsSpringConfiguration extends ApplicationContextAware { new RudderOpaqueTokenAuthenticationProvider( introspector, new RudderOpaqueTokenAuthenticationConverter(RudderConfig.roApiAccountRepository, config), + RudderConfig.roApiAccountRepository, config.cacheRequestDuration, () => Instant.now() ) @@ -1375,10 +1376,11 @@ object RudderOpaqueTokenAuthenticationProvider { // this class is only here to allow to use our convert in place of default spring configuration class RudderOpaqueTokenAuthenticationProvider( - introspector: OpaqueTokenIntrospector, - converter: OpaqueTokenAuthenticationConverter, - validationCache: Option[Duration], - now: () => Instant + introspector: OpaqueTokenIntrospector, + converter: OpaqueTokenAuthenticationConverter, + roApiAccountRepository: RoApiAccountRepository, + validationCache: Option[Duration], + now: () => Instant ) extends AuthenticationProvider { private val opaqueTokenAuthenticationProvider = new OpaqueTokenAuthenticationProvider(introspector) opaqueTokenAuthenticationProvider.setAuthenticationConverter(converter) @@ -1433,7 +1435,18 @@ class RudderOpaqueTokenAuthenticationProvider( case Left(ex) => throw ex // we do have an existing authentication. // Expiration will be checked in convert. - case Right(auth @ RudderOAuth2OpaqueToken(ta, _)) => + case Right(auth @ RudderOAuth2OpaqueToken(ta, u)) => + // Rudder status check on API account to prevent disabled users during cache lifetime + u.account match { + case RudderAccount.Api(account) => + roApiAccountRepository.getById(account.id).map(_.map(_.isEnabled)).runNow match { + case Some(true) => () // ok, next check + case Some(false) => throw new InvalidBearerTokenException(s"Token is disabled by Rudder API account") + case None => throw new InvalidBearerTokenException(s"Token is no longer mapped to a Rudder API account") + } + case _ => + throw new InvalidBearerTokenException(s"Token is not mapped to a Rudder API account") + } ta.getCredentials match { case x: OAuth2AccessToken => if (now().isAfter(x.getExpiresAt)) { diff --git a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala index dbcd6c205..a39666ef2 100644 --- a/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala +++ b/auth-backends/src/main/scala/com/normation/plugins/authbackends/Oauth2Authentication.scala @@ -265,7 +265,7 @@ object RudderRegistrationPropertyCommon { A_TENANTS_OVERRIDE -> "(default false) keep user configured tenants in rudder-user.xml or override them with the one provided in the token", A_TENANTS_MAPPING -> s"(optional) provides a map of alias `IdP tenant name` -> `Rudder tenant name`, where each IdP tenant name is a sub-key of '${A_TENANTS_MAPPING}'", A_TENANTS_REVERSE_MAPPING -> s"(optional) provides a map of alias `Rudder tenant name` -> `IdP tenant name`, where each IdP tenant name is a sub-key of '${A_TENANTS_MAPPING}', useful when the IdP tenant name contains '='", - A_ENFORCE_TENANTS_MAPPING -> "(default true) if true, restricts roles available by the IdP to the role defined in mapping entitlement. Else the map provides alias for Rudder internal role names.", + A_ENFORCE_TENANTS_MAPPING -> "(default true) if true, restricts tenants available by the IdP to the tenants defined in mapping entitlement. Else the map provides id for Rudder tenants.", A_URI_AUTH -> "provider URL to contact for main authentication (see provider documentation)", A_URI_TOKEN -> "provider URL to contact for token verification (see provider documentation)", A_URI_USER_INFO -> "provider URL to contact to get user information (see provider documentation)", @@ -394,7 +394,7 @@ object RudderRegistrationPropertyCommon { rolesEnabled <- read(A_ROLES_ENABLED).catchAll(_ => "false".succeed) rolesAttr <- read(A_ROLES_ATTRIBUTE).catchAll(_ => "".succeed) rolesOverride <- read(A_ROLES_OVERRIDE).catchAll(_ => "false".succeed) - enforceRoleMapping <- read(A_ENFORCE_ROLES_MAPPING).catchAll(_ => "false".succeed) + enforceRoleMapping <- read(A_ENFORCE_ROLES_MAPPING).catchAll(_ => "true".succeed) mapping <- readMap(A_ROLES_MAPPING) reverseMapping <- readMap(A_ROLES_REVERSE_MAPPING) } yield { @@ -410,18 +410,18 @@ object RudderRegistrationPropertyCommon { protected[authbackends] def readTenants()(implicit base: BasePath, config: Config): IOResult[ProvidedTenants] = { for { - tenantsEnabled <- read(A_TENANTS_ENABLED).catchAll(_ => "false".succeed) - tenantsAttr <- read(A_TENANTS_ATTRIBUTE).catchAll(_ => "".succeed) - tenantsOverride <- read(A_TENANTS_OVERRIDE).catchAll(_ => "false".succeed) - enforceRoleMapping <- read(A_ENFORCE_TENANTS_MAPPING).catchAll(_ => "false".succeed) - mapping <- readMap(A_TENANTS_MAPPING) - reverseMapping <- readMap(A_TENANTS_REVERSE_MAPPING) + tenantsEnabled <- read(A_TENANTS_ENABLED).catchAll(_ => "false".succeed) + tenantsAttr <- read(A_TENANTS_ATTRIBUTE).catchAll(_ => "".succeed) + tenantsOverride <- read(A_TENANTS_OVERRIDE).catchAll(_ => "false".succeed) + enforceTenantMapping <- read(A_ENFORCE_TENANTS_MAPPING).catchAll(_ => "true".succeed) + mapping <- readMap(A_TENANTS_MAPPING) + reverseMapping <- readMap(A_TENANTS_REVERSE_MAPPING) } yield { ProvidedTenants( toBool(tenantsEnabled), tenantsAttr, toBool(tenantsOverride), - toBool(enforceRoleMapping), + toBool(enforceTenantMapping), mapping ++ reverseMapping.map { case (a, b) => (b, a) } ) } diff --git a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestCache.scala b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestCache.scala index 446d8a2de..04f9f307e 100644 --- a/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestCache.scala +++ b/auth-backends/src/test/scala/com/normation/plugins/authbackends/TestCache.scala @@ -38,15 +38,14 @@ package com.normation.plugins.authbackends import bootstrap.rudder.plugin.RudderOAuth2OpaqueToken import bootstrap.rudder.plugin.RudderOpaqueTokenAuthenticationProvider -import com.normation.rudder.api.ApiAccount -import com.normation.rudder.api.ApiAccountId -import com.normation.rudder.api.ApiAccountKind -import com.normation.rudder.api.ApiAccountName -import com.normation.rudder.api.ApiAuthorization +import com.normation.eventlog.* +import com.normation.rudder.MockApiAccountService +import com.normation.rudder.api.* import com.normation.rudder.facts.nodes.NodeSecurityContext import com.normation.rudder.users.RudderAccount import com.normation.rudder.users.RudderUserDetail import com.normation.rudder.users.UserStatus +import com.normation.zio.UnsafeRun import java.time.Instant import java.util.concurrent.TimeUnit import org.joda.time.DateTime @@ -77,20 +76,22 @@ class TestCache extends Specification { private val TOKEN_EXP2 = Instant.ofEpochSecond(10 * 60) private val TOKEN_EXP3 = Instant.ofEpochSecond(20 * 60) + private def apiAccount(s: String) = { + ApiAccount( + ApiAccountId(s), + ApiAccountKind.PublicApi(ApiAuthorization.RW, None), + ApiAccountName(s), + None, + "", + isEnabled = true, + new DateTime(0), + new DateTime(0), + NodeSecurityContext.All + ) + } + private def rudderUserDetails(s: String) = RudderUserDetail( - RudderAccount.Api( - ApiAccount( - ApiAccountId(s), - ApiAccountKind.PublicApi(ApiAuthorization.RW, None), - ApiAccountName(s), - None, - "", - isEnabled = true, - new DateTime(0), - new DateTime(0), - NodeSecurityContext.All - ) - ), + RudderAccount.Api(apiAccount(s)), UserStatus.Active, Set(), ApiAuthorization.RW, @@ -142,12 +143,20 @@ class TestCache extends Specification { } } + private val apiAccountRepository = { + val mockService = new MockApiAccountService(null) + val repository = mockService.repository + (repository.save(apiAccount(GOOD_TOKEN_EXP1), ModificationId("test"), EventActor("test")) *> + repository.save(apiAccount(GOOD_TOKEN_EXP2), ModificationId("test"), EventActor("test"))).runNow + repository + } + // private val providerCache = new RudderOpaqueTokenAuthenticationProvider(introspector, converter, Some(Duration(4, TimeUnit.MINUTES))) "when we don't have a cache" >> { val introspector = new TestOpaqueTokenIntrospector val providerNoCache = - new RudderOpaqueTokenAuthenticationProvider(introspector, converter, None, () => Instant.ofEpochSecond(4 * 60)) + new RudderOpaqueTokenAuthenticationProvider(introspector, converter, null, None, () => Instant.ofEpochSecond(4 * 60)) "two times a correct value leads to 2 requests" in { @@ -170,6 +179,7 @@ class TestCache extends Specification { new RudderOpaqueTokenAuthenticationProvider( introspector, converter, + apiAccountRepository, Some(Duration(5, TimeUnit.MINUTES)), () => Instant.ofEpochSecond(4 * 60) ) @@ -197,6 +207,7 @@ class TestCache extends Specification { new RudderOpaqueTokenAuthenticationProvider( introspector, converter, + apiAccountRepository, Some(Duration(5, TimeUnit.MINUTES)), () => Instant.ofEpochSecond(c.toLong * 60) ) @@ -215,4 +226,72 @@ class TestCache extends Specification { introspector.count.get === 1 } } + + "when we have a cache and account is disabled" >> { + val introspector = new TestOpaqueTokenIntrospector + val c = 2 + val providerCache = { + new RudderOpaqueTokenAuthenticationProvider( + introspector, + converter, + apiAccountRepository, + Some(Duration(5, TimeUnit.MINUTES)), + () => Instant.ofEpochSecond(c.toLong * 60) + ) + } + + "first request ok" in { + providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth) + introspector.count.get === 1 + } + + "second request ko when disabled" in { + apiAccountRepository + .save(apiAccount(GOOD_TOKEN_EXP1).copy(isEnabled = false), ModificationId("test"), EventActor("test")) + .runNow + val res = Try(providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth)) + + res must beAnInstanceOf[scala.util.Failure[?]] + introspector.count.get === 1 + } + + "third request ok when re-enabled" in { + apiAccountRepository.save(apiAccount(GOOD_TOKEN_EXP1), ModificationId("test"), EventActor("test")).runNow + providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth) + introspector.count.get === 1 + } + } + + "when we have a cache and account is removed" >> { + val introspector = new TestOpaqueTokenIntrospector + val c = 2 + val providerCache = { + new RudderOpaqueTokenAuthenticationProvider( + introspector, + converter, + apiAccountRepository, + Some(Duration(5, TimeUnit.MINUTES)), + () => Instant.ofEpochSecond(c.toLong * 60) + ) + } + + "first request ok" in { + providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth) + introspector.count.get === 1 + } + + "second request ko when removed" in { + apiAccountRepository.delete(ApiAccountId(GOOD_TOKEN_EXP1), ModificationId("test"), EventActor("test")).runNow + val res = Try(providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth)) + + res must beAnInstanceOf[scala.util.Failure[?]] + introspector.count.get === 1 + } + + "third request ok when added back" in { + apiAccountRepository.save(apiAccount(GOOD_TOKEN_EXP1), ModificationId("test"), EventActor("test")).runNow + providerCache.authenticate(GOOD_TOKEN_EXP1.toAuth) + introspector.count.get === 1 + } + } }