diff --git a/obp-api/src/main/scala/code/api/util/ApiRole.scala b/obp-api/src/main/scala/code/api/util/ApiRole.scala index 95be8e467..f8d21f953 100644 --- a/obp-api/src/main/scala/code/api/util/ApiRole.scala +++ b/obp-api/src/main/scala/code/api/util/ApiRole.scala @@ -490,6 +490,9 @@ object ApiRole extends MdcLoggable{ case class CanRefreshUser(requiresBankId: Boolean = false) extends ApiRole lazy val canRefreshUser = CanRefreshUser() + case class CanSyncUser(requiresBankId: Boolean = false) extends ApiRole + lazy val canSyncUser = CanSyncUser() + case class CanGetAccountApplications(requiresBankId: Boolean = false) extends ApiRole lazy val canGetAccountApplications = CanGetAccountApplications() diff --git a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala index f33b971fe..1aa13af55 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/APIMethods510.scala @@ -21,7 +21,7 @@ import code.api.v2_1_0.ConsumerRedirectUrlJSON import code.api.v3_0_0.JSONFactory300 import code.api.v3_0_0.JSONFactory300.createAggregateMetricJson import code.api.v3_1_0.ConsentJsonV310 -import code.api.v3_1_0.JSONFactory310.createBadLoginStatusJson +import code.api.v3_1_0.JSONFactory310.{createBadLoginStatusJson, createRefreshUserJson} import code.api.v4_0_0.JSONFactory400.{createAccountBalancesJson, createBalancesJson, createNewCoreBankAccountJson} import code.api.v4_0_0.{JSONFactory400, PostAccountAccessJsonV400, PostApiCollectionJson400, RevokedJsonV400} import code.api.v5_0_0.JSONFactory500 @@ -32,8 +32,9 @@ import code.consent.{ConsentRequests, Consents} import code.consumer.Consumers import code.loginattempts.LoginAttempt import code.metrics.APIMetrics +import code.metrics.MappedMetric.userId import code.model.AppType -import code.model.dataAccess.MappedBankAccount +import code.model.dataAccess.{AuthUser, MappedBankAccount} import code.regulatedentities.MappedRegulatedEntityProvider import code.userlocks.UserLocksProvider import code.users.Users @@ -626,6 +627,47 @@ trait APIMethods510 { } + + staticResourceDocs += ResourceDoc( + syncExternalUser, + implementedInApiVersion, + nameOf(syncExternalUser), + "POST", + "/users/PROVIDER/PROVIDER_ID/sync", + "Sync User", + s"""The endpoint is used to create or sync an OBP User with User from an external identity provider. + |PROVIDER is the host of the provider e.g. a Keycloak Host. + |PROVIDER_ID is the unique identifier for the User at the PROVIDER. + |At the end of the process, a User will exist in OBP with the Account Access records defined by the CBS. + | + |${authenticationRequiredMessage(true)} + | + |""", + EmptyBody, + refresUserJson, + List( + $UserNotLoggedIn, + UserHasMissingRoles, + UnknownError + ), + List(apiTagUser), + Some(List(canSyncUser)) + ) + + lazy val syncExternalUser : OBPEndpoint = { + case "users" :: provider :: providerId :: "sync" :: Nil JsonPost _ => { + cc => implicit val ec = EndpointContext(Some(cc)) + for { + (user: User, callContext) <- NewStyle.function.getOrCreateResourceUser(provider, providerId, cc.callContext) + _ <- AuthUser.refreshUser(user, callContext) + } yield { + (JSONFactory510.getSyncedUser(user), HttpCode.`201`(callContext)) + } + } + } + + + staticResourceDocs += ResourceDoc( getAccountsHeldByUserAtBank, implementedInApiVersion, diff --git a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala index bb0f98848..1b9f1f6f4 100644 --- a/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala +++ b/obp-api/src/main/scala/code/api/v5_1_0/JSONFactory5.1.0.scala @@ -555,6 +555,8 @@ case class ConsumerLogoUrlJson( logo_url: String ) +case class SyncExternalUserJson(user_id: String) + object JSONFactory510 extends CustomJsonFormats { def createViewJson(view: View): CustomViewJsonV510 = { @@ -893,6 +895,10 @@ object JSONFactory510 extends CustomJsonFormats { ) } + def getSyncedUser(user : User): SyncExternalUserJson = { + SyncExternalUserJson(user.userId) + } + def createUserAttributesJson(userAttribute: List[UserAttribute]): UserAttributesResponseJsonV510 = { UserAttributesResponseJsonV510(userAttribute.map(createUserAttributeJson)) } diff --git a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala index 8aa825e47..34e0ae201 100644 --- a/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala +++ b/obp-api/src/test/scala/code/api/v5_1_0/AccountTest.scala @@ -1,12 +1,13 @@ package code.api.v5_1_0 import code.api.util.APIUtil.OAuth._ -import code.api.util.ApiRole.{CanGetAccountsHeldAtAnyBank, CanGetAccountsHeldAtOneBank} +import code.api.util.ApiRole.{CanGetAccountsHeldAtAnyBank, CanGetAccountsHeldAtOneBank, CanSyncUser} import code.api.util.ErrorMessages.{UserHasMissingRoles, UserNotLoggedIn} import code.api.v5_1_0.OBPAPI5_1_0.Implementations5_1_0 import com.github.dwickern.macros.NameOf.nameOf import com.openbankproject.commons.model.ErrorMessage import com.openbankproject.commons.util.ApiVersion +import net.liftweb.json.Serialization.write import org.scalatest.Tag class AccountTest extends V510ServerSetup { @@ -21,6 +22,7 @@ class AccountTest extends V510ServerSetup { object GetCoreAccountByIdThroughView extends Tag(nameOf(Implementations5_1_0.getCoreAccountByIdThroughView)) object getAccountsHeldByUserAtBank extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUserAtBank)) object GetAccountsHeldByUser extends Tag(nameOf(Implementations5_1_0.getAccountsHeldByUser)) + object SyncExternalUser extends Tag(nameOf(Implementations5_1_0.syncExternalUser)) lazy val bankId = randomBankId @@ -74,5 +76,24 @@ class AccountTest extends V510ServerSetup { response.body.extract[ErrorMessage].message contains errorMessage should be(true) } } + + feature(s"test ${SyncExternalUser.name}") { + scenario(s"We will test ${SyncExternalUser.name}", SyncExternalUser, VersionOfApi) { + val request = (v5_1_0_Request / "users" / resourceUser2.provider / resourceUser2.idGivenByProvider / "sync").GET + // Anonymous call fails + val response = makePostRequest(request, write("")) + response.code should equal(401) + response.body.extract[ErrorMessage].message should equal(UserNotLoggedIn) + } + scenario("We will call the endpoint with user credentials", SyncExternalUser, VersionOfApi) { + When(s"We make a request $SyncExternalUser") + val requestGet = (v5_1_0_Request / "users" / resourceUser2.provider / resourceUser2.idGivenByProvider / "sync").GET <@(user1) + val response = makePostRequest(requestGet, write("")) + Then("We should get a 403") + response.code should equal(403) + val errorMessage = UserHasMissingRoles + s"$CanSyncUser" + response.body.extract[ErrorMessage].message contains errorMessage should be(true) + } + } } \ No newline at end of file