diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 292a73b249..e2a7ddba7f 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1367,5 +1367,15 @@ regulated_entities = [] # "grantee_consumer_id": "fb327484-94d7-44d2-83e5-8d27301e8279" \ #}] +# Bootstrap Super User +# Given the following credentials, OBP will create a user if they do not already exist. +# This user's password will be valid for a limited time. +# This user will be granted ONLY the CanCreateEntitlementAtAnyBank permission. +# This feature can also be used in a "Break Glass" scenario. +# If you want to use this feature, please set up all three values properly at the same time. +# super_admin_username=TomWilliams +# super_admin_inital_password=681aeeb9f681aeeb9f681aeeb9 +# super_admin_email=tom@tesobe.com + # Note: For secure and http only settings for cookies see resources/web.xml which is mentioned in the README.md \ No newline at end of file diff --git a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala index 4c708fe61f..e097bd7b77 100644 --- a/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala +++ b/obp-api/src/main/scala/bootstrap/liftweb/Boot.scala @@ -43,6 +43,7 @@ import code.api.ResourceDocs1_4_0._ import code.api._ import code.api.attributedefinition.AttributeDefinition import code.api.cache.Redis +import code.api.util.ApiRole.CanCreateEntitlementAtAnyBank import code.api.util.APIUtil.{enableVersionIfAllowed, errorJsonResponse, getPropsValue, gitCommit} import code.api.util._ import code.api.util.migration.Migration @@ -76,7 +77,7 @@ import code.dynamicMessageDoc.DynamicMessageDoc import code.dynamicResourceDoc.DynamicResourceDoc import code.endpointMapping.EndpointMapping import code.endpointTag.EndpointTag -import code.entitlement.MappedEntitlement +import code.entitlement.{Entitlement, MappedEntitlement} import code.entitlementrequest.MappedEntitlementRequest import code.fx.{MappedCurrency, MappedFXRate} import code.kafka.{KafkaHelperActors, OBPKafkaConsumer} @@ -318,6 +319,8 @@ class Boot extends MdcLoggable { //see the notes for this method: createDefaultBankAndDefaultAccountsIfNotExisting() + createBootstrapSuperUser() + //launch the scheduler to clean the database from the expired tokens and nonces, 1 hour DataBaseCleanerScheduler.start(intervalInSeconds = 60*60) @@ -954,6 +957,60 @@ class Boot extends MdcLoggable { logger.debug(s"creating BankAccount(${defaultBankId}, $outgoingAccountId).") } } + + + /** + * Bootstrap Super User + * Given the following credentials, OBP will create a user *if it does not exist already*. + * This user's password will be valid for a limited amount of time. + * This user will be granted ONLY CanCreateEntitlementAtAnyBank + * This feature can also be used in a "Break Glass scenario" + */ + private def createBootstrapSuperUser() ={ + + val superAdminUsername = APIUtil.getPropsValue("super_admin_username","") + val superAdminInitalPassword = APIUtil.getPropsValue("super_admin_inital_password","") + val superAdminEmail = APIUtil.getPropsValue("super_admin_email","") + + val isPropsNotSetProperly = superAdminUsername==""||superAdminInitalPassword ==""||superAdminEmail=="" + + //This is the logic to check if an AuthUser exists for the `create sandbox` endpoint, AfterApiAuth, OpenIdConnect ,,, + val existingAuthUser = AuthUser.find(By(AuthUser.username, superAdminUsername)) + + if(isPropsNotSetProperly) { + //Nothing happens, props is not set + }else if(existingAuthUser.isDefined) { + logger.error(s"createBootstrapSuperUser- Errors: Existing AuthUser with username ${superAdminUsername} detected in data import where no ResourceUser was found") + } else { + val authUser = AuthUser.create + .email(superAdminEmail) + .firstName(superAdminUsername) + .lastName(superAdminUsername) + .username(superAdminUsername) + .password(superAdminInitalPassword) + .passwordShouldBeChanged(true) + .validated(true) + + val validationErrors = authUser.validate + + if(!validationErrors.isEmpty) + logger.error(s"createBootstrapSuperUser- Errors: ${validationErrors.map(_.msg)}") + else { + Full(authUser.save()) //this will create/update the resourceUser. + + val userBox = Users.users.vend.getUserByProviderAndUsername(authUser.getProvider(), authUser.username.get) + + val resultBox = userBox.map(user => Entitlement.entitlement.vend.addEntitlement("", user.userId, CanCreateEntitlementAtAnyBank.toString)) + + if(resultBox.isEmpty){ + logger.error(s"createBootstrapSuperUser- Errors: ${resultBox}") + } + } + + } + + } + } object ToSchemify { diff --git a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala index bd0c039966..fbef735723 100644 --- a/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala +++ b/obp-api/src/main/scala/code/model/dataAccess/AuthUser.scala @@ -96,6 +96,8 @@ class AuthUser extends MegaProtoUser[AuthUser] with CreatedUpdated with MdcLogga def getSingleton = AuthUser // what's the "meta" server object user extends MappedLongForeignKey(this, ResourceUser) + + object passwordShouldBeChanged extends MappedBoolean(this) override lazy val firstName = new MyFirstName @@ -185,7 +187,7 @@ class AuthUser extends MegaProtoUser[AuthUser] with CreatedUpdated with MdcLogga case e if usernameRegex.findFirstMatchIn(e).isDefined => Nil case _ => List(FieldError(this, Text(msg))) } - override def displayName = S.?("Username") + override def displayName = Helper.i18n("Username") @deprecated("Use UniqueIndex(username, provider)","27 December 2021") override def dbIndexed_? = false // We use more general index UniqueIndex(username, provider) :: super.dbIndexes override def validations = isEmpty(Helper.i18n("Please.enter.your.username")) _ :: @@ -398,7 +400,7 @@ class AuthUser extends MegaProtoUser[AuthUser] with CreatedUpdated with MdcLogga override def validate = i_is_! match { case null => List(FieldError(this, Text(Helper.i18n("Please.enter.your.email")))) case e if e.trim.isEmpty => List(FieldError(this, Text(Helper.i18n("Please.enter.your.email")))) - case e if (!isEmailValid(e)) => List(FieldError(this, Text(S.?("invalid.email.address")))) + case e if (!isEmailValid(e)) => List(FieldError(this, Text(Helper.i18n("invalid.email.address")))) case _ => Nil } override def _toForm: Box[Elem] = diff --git a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala index d4a2102bfd..283242ab4e 100644 --- a/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala +++ b/obp-api/src/main/scala/code/snippet/VrpConsentCreation.scala @@ -264,7 +264,7 @@ class VrpConsentCreation extends MdcLoggable with RestHelper with APIMethods510 case Full(consumerJsonV310) => //4th: get the redirect url. val redirectURL = consumerJsonV310.redirect_url.trim - S.redirectTo(s"$redirectURL?CONSENT_REQUEST_ID=${consentJsonV510.consent_request_id.getOrElse("")}") + S.redirectTo(s"$redirectURL?CONSENT_REQUEST_ID=${consentJsonV510.consent_request_id.getOrElse("")}&status=${consentJsonV510.status}") case _ => S.error(s"$InvalidJsonFormat The Json body should be the $ConsumerJsonV310. " + s"Please check `Get Consumer` !") diff --git a/obp-api/src/main/scala/code/util/Helper.scala b/obp-api/src/main/scala/code/util/Helper.scala index e8592073f5..a762910048 100644 --- a/obp-api/src/main/scala/code/util/Helper.scala +++ b/obp-api/src/main/scala/code/util/Helper.scala @@ -386,15 +386,21 @@ object Helper extends Loggable { } def i18n(message: String, default: Option[String] = None): String = { - if(S.?(message)==message) { - val words = message.split('.').toList match { - case x :: Nil => Helpers.capify(x) :: Nil - case x :: xs => Helpers.capify(x) :: xs - case _ => Nil - } - default.getOrElse(words.mkString(" ") + ".") + if (S.inStatefulScope_?) { + if (S.?(message) == message) { + val words = message.split('.').toList match { + case x :: Nil => Helpers.capify(x) :: Nil + case x :: xs => Helpers.capify(x) :: xs + case _ => Nil + } + default.getOrElse(words.mkString(" ") + ".") + } else + S.?(message) + } else { + logger.error(s"i18n(message($message), default${default}: Attempted to use resource bundles outside of an initialized S scope. " + + s"S only usable when initialized, such as during request processing. Did you call S.? from Future?") + default.getOrElse(message) } - else S.?(message) } /** diff --git a/release_notes.md b/release_notes.md index 63b8a3f1b8..c43d5d104d 100644 --- a/release_notes.md +++ b/release_notes.md @@ -3,6 +3,10 @@ ### Most recent changes at top of file ``` Date Commit Action +17/02/2025 5877d2f2 Bootstrap Super User + Added props super_admin_username=TomWilliams + Added props super_admin_inital_password=681aeeb9f681aeeb9f681aeeb9 + Added props super_admin_email=tom@tesobe.com 24/01/2025 ad68f054 Added props skip_consent_sca_for_consumer_id_pairs . 03/02/2024 7bcb6bc5 Added props oauth2.keycloak.source_of_truth, default is false. oauth2.keycloak.source_of_truth = true turns sync ON.