I am wondering which is the correct place to secure operations on entities based on caller's identity and privilages (roles) in an ORM based application exposed through REST API.
The example is written in simplified Spring/JPA style and in Kotlin. However the code serves here as an example to illustrate the questsion which I believe to be framework-agnostic in essence.
@Entity
class User {
@PrimaryKey
private var id: String = ""
@OneToMany
private val initiatedRelationships: MutableList<Relationship> = mutableListOf()
@OneToMany
private val receivedRelationships: MutableList<Relationship> = mutableListOf()
// other members...
}
where Relationship represents a simple join-table between users and holds additional attribute - relationship status, eg. FRIEND, INVITATION, BLOCKED
@Entity
internal class UserRelationship {
@EmbeddedId
var UserRelationship? = null // composite key
@ManyToOne
val initiator: User? = null
@ManyToOne
val receiver: User? = null
val status: String = "" // FRIEND, INVITATION
}
I decided to contain some business logic in my Entity classes, so that Services do not need to handle with all the implemetnation details.
That way, in Service layer I don't have to deal with UserRelationship table. I just ask User entity for receivedInvitations, sentInvitations, friends
@Entity
class User {
// members described before ...
val friends: List<User>
get() {
return initiatedRelationsips
.filter { it.status == "FRIEND" }
.map { it.receiver }
.plus(
receivedRelationships
.filter { it.status == "FRIEND" }
.map { it.initiator }
)
}
}
My question is what is the correct place to put the authorization logic - eg. the list of friends of given user can only by accessed by his friend or him directly.
Would it by in Service layer (direct caller of
Userentity methods?)class UserService { // ... fun getFriendsOf(userId: UUID): List<User> { val user = userRepository.findById(userId) val principal = getPrincipal() // assume it gives you caller's identity if (user.id == principal.id) { return friends // Authorization OK, caller has rights to user's view firends } if (user.friends.any { friend -> friend.id == principal.id }) { return friends // Authorization OK, caller has rights to user's view firends } throw UnauthorizedException() // Neither user himself nor his friend } }Or rather inside
Userentity, so that when an unauthorized caller (principal) tries to view friends (callsuser.friends) an Business-Exception is thrown.@Entity class User { val friends: List<User> get() { if (/** Caller has no rights to read this field **/) { throw UnauthorizedException() } // here happy path, the caller is authorized } }
For now I found some resources on the topic: