/management/groups/GROUP_ID/entitlements

This commit is contained in:
simonredfern 2025-12-19 13:24:59 +01:00
parent 410cc63bc6
commit b95dae1112
5 changed files with 353 additions and 66 deletions

View File

@ -26,7 +26,7 @@ import code.api.v5_0_0.JSONFactory500
import code.api.v5_0_0.{ViewJsonV500, ViewsJsonV500}
import code.api.v5_1_0.{JSONFactory510, PostCustomerLegalNameJsonV510}
import code.api.dynamic.entity.helper.{DynamicEntityHelper, DynamicEntityInfo}
import code.api.v6_0_0.JSONFactory600.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
import code.api.v6_0_0.JSONFactory600.{AddUserToGroupResponseJsonV600, DynamicEntityDiagnosticsJsonV600, DynamicEntityIssueJsonV600, GroupEntitlementJsonV600, GroupEntitlementsJsonV600, GroupJsonV600, GroupsJsonV600, PostGroupJsonV600, PostGroupMembershipJsonV600, PostResetPasswordUrlJsonV600, PutGroupJsonV600, ReferenceTypeJsonV600, ReferenceTypesJsonV600, ResetPasswordUrlJsonV600, RoleWithEntitlementCountJsonV600, RolesWithEntitlementCountsJsonV600, ScannedApiVersionJsonV600, UpdateViewJsonV600, UserGroupMembershipJsonV600, UserGroupMembershipsJsonV600, ValidateUserEmailJsonV600, ValidateUserEmailResponseJsonV600, ViewJsonV600, ViewPermissionJsonV600, ViewPermissionsJsonV600, ViewsJsonV600, createAbacRuleJsonV600, createAbacRulesJsonV600, createActiveCallLimitsJsonV600, createCallLimitJsonV600, createCurrentUsageJson}
import code.api.v6_0_0.{AbacRuleJsonV600, AbacRuleResultJsonV600, AbacRulesJsonV600, CreateAbacRuleJsonV600, ExecuteAbacRuleJsonV600, UpdateAbacRuleJsonV600}
import code.api.v6_0_0.OBPAPI6_0_0
import code.abacrule.{AbacRuleEngine, MappedAbacRuleProvider}
@ -3002,6 +3002,84 @@ trait APIMethods600 {
}
}
staticResourceDocs += ResourceDoc(
getGroupEntitlements,
implementedInApiVersion,
nameOf(getGroupEntitlements),
"GET",
"/management/groups/GROUP_ID/entitlements",
"Get Group Entitlements",
s"""Get all entitlements that have been granted from a specific group.
|
|This returns all entitlements where the group_id matches the specified GROUP_ID.
|
|Requires:
|- CanGetEntitlementsForAnyBank
|
|${userAuthenticationMessage(true)}
|
|""".stripMargin,
EmptyBody,
GroupEntitlementsJsonV600(
entitlements = List(
GroupEntitlementJsonV600(
entitlement_id = "entitlement-id-123",
role_name = "CanGetCustomer",
bank_id = "gh.29.uk",
user_id = "user-id-123",
username = "susan.uk.29@example.com",
group_id = Some("group-id-123"),
process = Some("GROUP_MEMBERSHIP")
)
)
),
List(
UserNotLoggedIn,
UserHasMissingRoles,
UnknownError
),
List(apiTagGroup, apiTagEntitlement),
Some(List(canGetEntitlementsForAnyBank))
)
lazy val getGroupEntitlements: OBPEndpoint = {
case "management" :: "groups" :: groupId :: "entitlements" :: Nil JsonGet _ => {
cc => implicit val ec = EndpointContext(Some(cc))
for {
(Full(u), callContext) <- authenticatedAccess(cc)
// Verify the group exists
group <- Future {
code.group.GroupTrait.group.vend.getGroup(groupId)
} map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Group not found", 404)
}
// Get entitlements by group_id
groupEntitlements <- Entitlement.entitlement.vend.getEntitlementsByGroupId(groupId) map {
x => unboxFullOrFail(x, callContext, s"$UnknownError Cannot get entitlements", 400)
}
// Get usernames for each entitlement
entitlementsWithUsernames <- Future.sequence {
groupEntitlements.map { ent =>
Users.users.vend.getUserByUserIdFuture(ent.userId).map { userBox =>
val username = userBox.map(_.name).getOrElse("")
GroupEntitlementJsonV600(
entitlement_id = ent.entitlementId,
role_name = ent.roleName,
bank_id = ent.bankId,
user_id = ent.userId,
username = username,
group_id = ent.groupId,
process = ent.process
)
}
}
}
} yield {
(GroupEntitlementsJsonV600(entitlements = entitlementsWithUsernames), HttpCode.`200`(callContext))
}
}
}
staticResourceDocs += ResourceDoc(
removeUserFromGroup,
implementedInApiVersion,

View File

@ -848,6 +848,20 @@ object JSONFactory600 extends CustomJsonFormats with MdcLoggable {
group_entitlements: List[UserGroupMembershipJsonV600]
)
case class GroupEntitlementJsonV600(
entitlement_id: String,
role_name: String,
bank_id: String,
user_id: String,
username: String,
group_id: Option[String],
process: Option[String]
)
case class GroupEntitlementsJsonV600(
entitlements: List[GroupEntitlementJsonV600]
)
case class RoleWithEntitlementCountJsonV600(
role: String,
requires_bank_id: Boolean,

View File

@ -1,6 +1,5 @@
package code.entitlement
import code.api.util.APIUtil
import net.liftweb.common.Box
import net.liftweb.util.{Props, SimpleInjector}
@ -11,32 +10,52 @@ object Entitlement extends SimpleInjector {
val entitlement = new Inject(buildOne _) {}
def buildOne: EntitlementProvider = MappedEntitlementsProvider
def buildOne: EntitlementProvider = MappedEntitlementsProvider
}
trait EntitlementProvider {
def getEntitlement(bankId: String, userId: String, roleName: String) : Box[Entitlement]
def getEntitlementById(entitlementId: String) : Box[Entitlement]
def getEntitlementsByUserId(userId: String) : Box[List[Entitlement]]
def getEntitlementsByUserIdFuture(userId: String) : Future[Box[List[Entitlement]]]
def getEntitlementsByBankId(bankId: String) : Future[Box[List[Entitlement]]]
def deleteEntitlement(entitlement: Box[Entitlement]) : Box[Boolean]
def getEntitlements() : Box[List[Entitlement]]
def getEntitlement(
bankId: String,
userId: String,
roleName: String
): Box[Entitlement]
def getEntitlementById(entitlementId: String): Box[Entitlement]
def getEntitlementsByUserId(userId: String): Box[List[Entitlement]]
def getEntitlementsByUserIdFuture(
userId: String
): Future[Box[List[Entitlement]]]
def getEntitlementsByBankId(bankId: String): Future[Box[List[Entitlement]]]
def deleteEntitlement(entitlement: Box[Entitlement]): Box[Boolean]
def getEntitlements(): Box[List[Entitlement]]
def getEntitlementsByRole(roleName: String): Box[List[Entitlement]]
def getEntitlementsFuture() : Future[Box[List[Entitlement]]]
def getEntitlementsByRoleFuture(roleName: String) : Future[Box[List[Entitlement]]]
def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String="manual", grantorUserId: Option[String]=None, groupId: Option[String]=None, process: Option[String]=None) : Box[Entitlement]
def deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String]) : Box[Boolean]
def deleteEntitlements(entityNames: List[String]) : Box[Boolean]
def getEntitlementsFuture(): Future[Box[List[Entitlement]]]
def getEntitlementsByRoleFuture(
roleName: String
): Future[Box[List[Entitlement]]]
def getEntitlementsByGroupId(groupId: String): Future[Box[List[Entitlement]]]
def addEntitlement(
bankId: String,
userId: String,
roleName: String,
createdByProcess: String = "manual",
grantorUserId: Option[String] = None,
groupId: Option[String] = None,
process: Option[String] = None
): Box[Entitlement]
def deleteDynamicEntityEntitlement(
entityName: String,
bankId: Option[String]
): Box[Boolean]
def deleteEntitlements(entityNames: List[String]): Box[Boolean]
}
trait Entitlement {
def entitlementId: String
def bankId : String
def userId : String
def roleName : String
def createdByProcess : String
def bankId: String
def userId: String
def roleName: String
def createdByProcess: String
def entitlementRequestId: Option[String]
def groupId: Option[String]
def process: Option[String]

View File

@ -1,7 +1,10 @@
package code.entitlement
import code.api.dynamic.entity.helper.DynamicEntityInfo
import code.api.util.ApiRole.{CanCreateEntitlementAtAnyBank, CanCreateEntitlementAtOneBank}
import code.api.util.ApiRole.{
CanCreateEntitlementAtAnyBank,
CanCreateEntitlementAtOneBank
}
import code.api.util.{ErrorMessages, NotificationUtil}
import code.util.{MappedUUID, UUIDString}
import net.liftweb.common.{Box, Failure, Full}
@ -12,7 +15,11 @@ import com.openbankproject.commons.ExecutionContext.Implicits.global
import net.liftweb.common
object MappedEntitlementsProvider extends EntitlementProvider {
override def getEntitlement(bankId: String, userId: String, roleName: String): Box[MappedEntitlement] = {
override def getEntitlement(
bankId: String,
userId: String,
roleName: String
): Box[MappedEntitlement] = {
// Return a Box so we can handle errors later.
MappedEntitlement.find(
By(MappedEntitlement.mBankId, bankId),
@ -28,36 +35,59 @@ object MappedEntitlementsProvider extends EntitlementProvider {
)
}
override def getEntitlementsByUserId(userId: String): Box[List[Entitlement]] = {
override def getEntitlementsByUserId(
userId: String
): Box[List[Entitlement]] = {
// Return a Box so we can handle errors later.
Some(MappedEntitlement.findAll(
By(MappedEntitlement.mUserId, userId),
OrderBy(MappedEntitlement.updatedAt, Descending)))
Some(
MappedEntitlement.findAll(
By(MappedEntitlement.mUserId, userId),
OrderBy(MappedEntitlement.updatedAt, Descending)
)
)
}
override def getEntitlementsByUserIdFuture(userId: String): Future[Box[List[Entitlement]]] = {
override def getEntitlementsByUserIdFuture(
userId: String
): Future[Box[List[Entitlement]]] = {
// Return a Box so we can handle errors later.
Future {
getEntitlementsByUserId(userId)
}
}
override def getEntitlementsByBankId(bankId: String): Future[Box[List[Entitlement]]] = {
override def getEntitlementsByBankId(
bankId: String
): Future[Box[List[Entitlement]]] = {
// Return a Box so we can handle errors later.
Future {
Some(MappedEntitlement.findAll(
By(MappedEntitlement.mBankId, bankId),
OrderBy(MappedEntitlement.mUserId, Descending)))
Some(
MappedEntitlement.findAll(
By(MappedEntitlement.mBankId, bankId),
OrderBy(MappedEntitlement.mUserId, Descending)
)
)
}
}
override def getEntitlements: Box[List[MappedEntitlement]] = {
// Return a Box so we can handle errors later.
Some(MappedEntitlement.findAll(OrderBy(MappedEntitlement.updatedAt, Descending)))
Some(
MappedEntitlement.findAll(
OrderBy(MappedEntitlement.updatedAt, Descending)
)
)
}
override def getEntitlementsByRole(roleName: String): Box[List[MappedEntitlement]] = {
override def getEntitlementsByRole(
roleName: String
): Box[List[MappedEntitlement]] = {
// Return a Box so we can handle errors later.
Some(MappedEntitlement.findAll(By(MappedEntitlement.mRoleName, roleName),OrderBy(MappedEntitlement.updatedAt, Descending)))
Some(
MappedEntitlement.findAll(
By(MappedEntitlement.mRoleName, roleName),
OrderBy(MappedEntitlement.updatedAt, Descending)
)
)
}
override def getEntitlementsFuture(): Future[Box[List[Entitlement]]] = {
@ -66,9 +96,11 @@ object MappedEntitlementsProvider extends EntitlementProvider {
}
}
override def getEntitlementsByRoleFuture(roleName: String): Future[Box[List[Entitlement]]] = {
override def getEntitlementsByRoleFuture(
roleName: String
): Future[Box[List[Entitlement]]] = {
Future {
if(roleName == null || roleName.isEmpty){
if (roleName == null || roleName.isEmpty) {
getEntitlements()
} else {
getEntitlementsByRole(roleName)
@ -76,51 +108,91 @@ object MappedEntitlementsProvider extends EntitlementProvider {
}
}
override def deleteEntitlement(entitlement: Box[Entitlement]): Box[Boolean] = {
override def getEntitlementsByGroupId(
groupId: String
): Future[Box[List[Entitlement]]] = {
Future {
Some(
MappedEntitlement.findAll(
By(MappedEntitlement.mGroupId, groupId),
OrderBy(MappedEntitlement.updatedAt, Descending)
)
)
}
}
override def deleteEntitlement(
entitlement: Box[Entitlement]
): Box[Boolean] = {
// Return a Box so we can handle errors later.
for {
findEntitlement <- entitlement
bankId <- Some(findEntitlement.bankId)
userId <- Some(findEntitlement.userId)
roleName <- Some(findEntitlement.roleName)
foundEntitlement <- MappedEntitlement.find(
foundEntitlement <- MappedEntitlement.find(
By(MappedEntitlement.mBankId, bankId),
By(MappedEntitlement.mUserId, userId),
By(MappedEntitlement.mRoleName, roleName)
)
} yield {
MappedEntitlement.delete_!(foundEntitlement)
}
yield {
MappedEntitlement.delete_!(foundEntitlement)
}
}
override def deleteDynamicEntityEntitlement(entityName: String, bankId:Option[String]): Box[Boolean] = {
val roleNames = DynamicEntityInfo.roleNames(entityName,bankId)
override def deleteDynamicEntityEntitlement(
entityName: String,
bankId: Option[String]
): Box[Boolean] = {
val roleNames = DynamicEntityInfo.roleNames(entityName, bankId)
deleteEntitlements(roleNames)
}
override def deleteEntitlements(entityNames: List[String]) : Box[Boolean] = {
Box.tryo{
MappedEntitlement.bulkDelete_!!(ByList(MappedEntitlement.mRoleName, entityNames))
override def deleteEntitlements(entityNames: List[String]): Box[Boolean] = {
Box.tryo {
MappedEntitlement.bulkDelete_!!(
ByList(MappedEntitlement.mRoleName, entityNames)
)
}
}
override def addEntitlement(bankId: String, userId: String, roleName: String, createdByProcess: String ="manual", grantorUserId: Option[String]=None, groupId: Option[String]=None, process: Option[String]=None): Box[Entitlement] = {
override def addEntitlement(
bankId: String,
userId: String,
roleName: String,
createdByProcess: String = "manual",
grantorUserId: Option[String] = None,
groupId: Option[String] = None,
process: Option[String] = None
): Box[Entitlement] = {
def addEntitlementToUser(): Full[MappedEntitlement] = {
val entitlement = MappedEntitlement.create.mBankId(bankId).mUserId(userId).mRoleName(roleName).mCreatedByProcess(createdByProcess)
val entitlement = MappedEntitlement.create
.mBankId(bankId)
.mUserId(userId)
.mRoleName(roleName)
.mCreatedByProcess(createdByProcess)
groupId.foreach(gid => entitlement.mGroupId(gid))
process.foreach(p => entitlement.mProcess(p))
val addEntitlement = entitlement.saveMe()
// When a role is Granted, we should send an email to the Recipient telling them they have been granted the role.
NotificationUtil.sendEmailRegardingAssignedRole(userId: String, addEntitlement: Entitlement)
NotificationUtil.sendEmailRegardingAssignedRole(
userId: String,
addEntitlement: Entitlement
)
Full(addEntitlement)
}
// Return a Box so we can handle errors later.
grantorUserId match {
case Some(userId) =>
val canCreateEntitlementAtAnyBank = MappedEntitlement.findAll(By(MappedEntitlement.mUserId, userId)).exists(e => e.roleName == CanCreateEntitlementAtAnyBank)
val canCreateEntitlementAtOneBank = MappedEntitlement.findAll(By(MappedEntitlement.mUserId, userId)).exists(e => e.roleName == CanCreateEntitlementAtOneBank && e.bankId == bankId)
if(canCreateEntitlementAtAnyBank || canCreateEntitlementAtOneBank) {
val canCreateEntitlementAtAnyBank = MappedEntitlement
.findAll(By(MappedEntitlement.mUserId, userId))
.exists(e => e.roleName == CanCreateEntitlementAtAnyBank)
val canCreateEntitlementAtOneBank = MappedEntitlement
.findAll(By(MappedEntitlement.mUserId, userId))
.exists(e =>
e.roleName == CanCreateEntitlementAtOneBank && e.bankId == bankId
)
if (canCreateEntitlementAtAnyBank || canCreateEntitlementAtOneBank) {
addEntitlementToUser()
} else {
Failure(ErrorMessages.EntitlementCannotBeGrantedGrantorIssue)
@ -131,8 +203,11 @@ object MappedEntitlementsProvider extends EntitlementProvider {
}
}
class MappedEntitlement extends Entitlement
with LongKeyedMapper[MappedEntitlement] with IdPK with CreatedUpdated {
class MappedEntitlement
extends Entitlement
with LongKeyedMapper[MappedEntitlement]
with IdPK
with CreatedUpdated {
def getSingleton = MappedEntitlement
@ -141,17 +216,17 @@ class MappedEntitlement extends Entitlement
object mUserId extends UUIDString(this)
object mRoleName extends MappedString(this, 255)
object mCreatedByProcess extends MappedString(this, 255)
object mGroupId extends MappedString(this, 255) {
override def dbColumnName = "group_id"
override def defaultValue = ""
}
object mProcess extends MappedString(this, 255) {
override def dbColumnName = "process"
override def defaultValue = ""
}
object entitlement_request_id extends MappedUUID(this) {
override def dbColumnName = "entitlement_request_id"
override def defaultValue = null
@ -161,27 +236,30 @@ class MappedEntitlement extends Entitlement
override def bankId: String = mBankId.get
override def userId: String = mUserId.get
override def roleName: String = mRoleName.get
override def createdByProcess: String =
if(mCreatedByProcess.get == null || mCreatedByProcess.get.isEmpty) "manual" else mCreatedByProcess.get
override def createdByProcess: String =
if (mCreatedByProcess.get == null || mCreatedByProcess.get.isEmpty) "manual"
else mCreatedByProcess.get
override def groupId: Option[String] = {
val gid = mGroupId.get
if(gid == null || gid.isEmpty) None else Some(gid)
if (gid == null || gid.isEmpty) None else Some(gid)
}
override def process: Option[String] = {
val p = mProcess.get
if(p == null || p.isEmpty) None else Some(p)
if (p == null || p.isEmpty) None else Some(p)
}
override def entitlementRequestId: Option[String] = {
entitlement_request_id.get match {
case uuid if uuid.toString.nonEmpty && uuid.toString != "00000000-0000-0000-0000-000000000000" =>
case uuid
if uuid.toString.nonEmpty && uuid.toString != "00000000-0000-0000-0000-000000000000" =>
Some(uuid.toString)
case _ =>
case _ =>
None
}
}
}
object MappedEntitlement extends MappedEntitlement with LongKeyedMetaMapper[MappedEntitlement] {
object MappedEntitlement
extends MappedEntitlement
with LongKeyedMetaMapper[MappedEntitlement] {
override def dbIndexes = UniqueIndex(mEntitlementId) :: super.dbIndexes
}
}

View File

@ -0,0 +1,98 @@
package code.api.v6_0_0
import code.api.util.APIUtil.OAuth._
import code.api.util.ApiRole.{
CanCreateGroupAtAllBanks,
CanGetEntitlementsForAnyBank
}
import code.api.util.ErrorMessages
import code.api.util.ErrorMessages.UserHasMissingRoles
import code.api.v6_0_0.APIMethods600.Implementations6_0_0
import code.entitlement.Entitlement
import code.setup.DefaultUsers
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 GroupEntitlementsTest extends V600ServerSetup with DefaultUsers {
override def beforeAll(): Unit = {
super.beforeAll()
}
override def afterAll(): Unit = {
super.afterAll()
}
/** Test tags Example: To run tests with tag "getGroupEntitlements": mvn test
* -D tagsToInclude
*
* This is made possible by the scalatest maven plugin
*/
object VersionOfApi extends Tag(ApiVersion.v6_0_0.toString)
object ApiEndpoint1
extends Tag(nameOf(Implementations6_0_0.getGroupEntitlements))
feature(
s"Assuring that endpoint getGroupEntitlements works as expected - $VersionOfApi"
) {
scenario(
"We try to consume endpoint getGroupEntitlements - Anonymous access",
ApiEndpoint1,
VersionOfApi
) {
When("We make the request")
val request =
(v6_0_0_Request / "management" / "groups" / "test-group-id" / "entitlements").GET
val response = makeGetRequest(request)
Then("We should get a 401")
And("We should get a message: " + ErrorMessages.UserNotLoggedIn)
response.code should equal(401)
response.body.extract[ErrorMessage].message should equal(
ErrorMessages.UserNotLoggedIn
)
}
scenario(
"We try to consume endpoint getGroupEntitlements without proper role - Authorized access",
ApiEndpoint1,
VersionOfApi
) {
When("We make the request")
val request =
(v6_0_0_Request / "management" / "groups" / "test-group-id" / "entitlements").GET <@ (user1)
val response = makeGetRequest(request)
Then("We should get a 403")
And(
"We should get a message: " + s"$CanGetEntitlementsForAnyBank entitlement required"
)
response.code should equal(403)
response.body.extract[ErrorMessage].message should equal(
UserHasMissingRoles + CanGetEntitlementsForAnyBank
)
}
scenario(
"We try to consume endpoint getGroupEntitlements with proper role - Authorized access",
ApiEndpoint1,
VersionOfApi
) {
When("We add the required entitlement")
Entitlement.entitlement.vend.addEntitlement(
"",
resourceUser1.userId,
CanGetEntitlementsForAnyBank.toString
)
And("We make the request")
val request =
(v6_0_0_Request / "management" / "groups" / "test-group-id" / "entitlements").GET <@ (user1)
val response = makeGetRequest(request)
Then("We should get a 404 because the group doesn't exist")
response.code should equal(404)
}
}
}