域驱动设计中的访问控制

Posted

技术标签:

【中文标题】域驱动设计中的访问控制【英文标题】:Access Control in Domain Driven Design 【发布时间】:2014-06-21 07:06:03 【问题描述】:

我阅读了有关 DDD 和访问控制的信息,发现以下两种观点之间存在一些矛盾:

“安全问题应在域外处理” “访问控制要求是特定领域的”

我正在寻找关于此的最佳实践。那么领域驱动设计的访问控制逻辑应该放在哪里,应该如何实现呢?

(更具体的是 DDD + CQRS + ES。)

我认为它应该靠近业务逻辑,例如用户故事可能是这样的:

用户可以通过发送用户名、爱好列表、简历等来编辑他的个人资料...

根据用户故事,我们实现领域模型和服务,例如:

UserService
    editProfile(EditUserProfileCommand command)
        User user = userRepository.getOneById(command.id)
        user.changeName(command.name)
        user.changeHobbies(command.hobbies)
        user.changeCV(command.cv)

UserRepository
    User getOneById(id)

User
    changeName(String name)
    changeHobbies(String[] hobbies)
    changeCV(String cv)

这没关系,但故事的 HIS profile 部分在哪里?

这显然是基于属性的访问控制,因为我们应该写这样的规则:

deny all, but if subject.id = resource.owner.id then grant access

但是我们应该在哪里执行这个规则,我们应该如何执行呢?

【问题讨论】:

在命令中包含用户 ID (command.id) 会引起歧义。更好地从命令中删除用户 ID,并将从授权上下文中获取的用户与命令一起传递。 @Lightman 感谢您的意见。老实说,我不知道为什么这个问题和答案会得到这么高的分数。也许有人应该写一篇文章或示例应用程序来说明应该如何正确地做到这一点。最近又开始看DDD了,好文章真的好难找。其中一些存在明显的设计问题。我想这会发生在每个人都可以写一个主题的时候,即使是像我这样不完全理解的人。 【参考方案1】:

那么我应该把访问控制逻辑放在哪里呢?

据此:https://softwareengineering.stackexchange.com/a/71883/65755 策略执行点应该在调用UserService.editProfile() 之前。

我得出了同样的结论:它不能在 UI 中,因为多个 UI 会导致代码重复。它应该在创建领域事件之前,因为它们表明我们已经在系统中做了一些事情。所以我们可以限制对域对象或使用这些域对象的服务的访问。通过 CQRS,我们不需要读取模型的域对象,只有服务,所以如果我们想要一个通用的解决方案,我们必须限制对服务的访问。我们可以将访问决策放在每个服务操作的开头,但这将是grant all, deny x 安全反模式。

我应该如何实现它?

这取决于适用于域的访问控制模型,因此取决于用户故事。通过访问决策,我们通常会发送访问请求并等待许可作为回报。访问请求通常有以下几个部分:主题、资源、操作、环境。因此,主体需要权限才能对环境中的资源执行操作。首先我们识别主题,然后我们对其进行身份验证,然后是授权,我们检查访问请求是否符合我们的访问策略。每个访问控制模型都以类似的方式工作。办公室。他们可能缺少其中一些步骤,但这并不重要......

我创建了一个访问控制模型的简短列表。我将规则、策略放入注释中,但如果我们想要一个可维护性良好的系统,通常我们应该将它们存储在可能以 XACML 格式的数据库中......

通过基于身份的访问控制 (IBAC),我们有一个身份 - 权限存储(访问控制列表、能力列表、访问控制矩阵)。因此,例如通过访问控制列表,我们存储可以拥有权限的用户或组的列表。

UserService
    @AccessControlList[inf3rno]
    editProfile(EditUserProfileCommand command)

通过基于格的访问控制 (LBAC),主题具有权限级别,资源具有所需的权限级别,我们检查哪个级别更高...

@posseses[level=5]
inf3rno

UserService
    @requires(level>=3)
    editProfile(EditUserProfileCommand command)

通过基于角色的访问控制 (RBAC),我们定义了主体角色,并将权限授予扮演实际角色的主体。

@roles[admin]
inf3rno

UserService
    @requires(role=admin)
    editProfile(EditUserProfileCommand command)

通过基于属性的访问控制 (ABAC),我们定义主题、资源和环境属性,并根据它们编写策略。

@attributes[roles=[admin]]
inf3rno

UserService
    @policy(subject.role=admin or resource.owner.id = subject.id)
    editProfile(EditUserProfileCommand command)
    @attribute(owner)
    Subject getOwner(EditUserProfileCommand command)

通过基于策略的访问控制 (PBAC),我们不会将我们的策略分配给其他任何东西,它们是独立的。

@attributes[roles=[admin]]
inf3rno

UserService
    editProfile(EditUserProfileCommand command)
    deleteProfile(DeleteUserProfileCommand command)
    @attribute(owner)
    Subject getOwner(EditUserProfileCommand command)

@permission(UserService.editProfile, UserService.deleteProfile)
@criteria(subject.role=admin or resource.owner.id = subject.id)
WriteUserServicePolicy

通过风险自适应访问控制 (RAdAC),我们根据对象的相对风险概况和操作的风险级别做出决定。这不能用我认为的规则来描述。我不确定实现,也许这就是***的点系统使用的。

通过基于授权的访问控制 (ZBAC),我们不进行识别和身份验证,而是将权限分配给识别因素。例如,如果有人发送一个令牌,那么她就可以访问一项服务。其他一切都与以前的解决方案类似。以 ABAC 为例:

@attributes[roles=[editor]]
token:2683fraicfv8a2zuisbkcaac

ArticleService
    @policy(subject.role=editor)
    editArticle(EditArticleCommand command)

所以知道2683fraicfv8a2zuisbkcaac 令牌的每个人都可以使用该服务。

等等……

还有许多其他型号,最合适的型号始终取决于客户的需求。

总结一下

- "security concerns should be handled outside the domain"
- "access control requirements are domain specific"

两者都可能是正确的,因为安全性不是域模型的一部分,但其实现取决于域模型和应用程序逻辑。

2 年后修改 2016-09-05

自从我作为 DDD 新手回答了我自己的问题以来,我已经阅读了 Vaughn Vernon 的 Implementing Domain-Driven Design。这是一本有趣的书。这是它的引述:

这构成了一个新的限界上下文——身份和访问 上下文 - 并且将通过标准被其他有界上下文使用 DDD 集成技术。对于消费上下文,身份和 访问上下文是一个通用子域。该产品将被命名 IdOvation。

因此,根据 Vernon 的说法,将访问控制移至通用子域的最佳解决方案可能是。

【讨论】:

关于编辑,很容易将基于角色、ACL 或基于权限的 BC 移至通用 BC,但对于基于规则的授权则不然。 对于这些具有@policy(subject.role=admin or resource.owner.id = subject.id) 之类的检查的 ABAC 系统,我不明白的一点是如何处理版本控制,因为策略中的代码是来自服务代码的沙盒。当实际服务方法中的代码请求资源时,所有者可能已经改变,但也许我缺少一些魔法? @Jordan 随意编辑或更改您想要的方式。据我所知,我读了一周关于 ABAC 的文章,但我从未使用过它。据我所知,它是某种基于策略或规则的访问控制,尽管它被称为基于属性。 5年前我写那个答案时,我可能完全误解了一些东西。我应该只留下“2年后的编辑部分”,但人们已经对我的答案表示赞同,我想这就是我把它留在那里的原因。这不是第一次人们在这里投票错误的答案...... @Jordan 是的,当然,但最终当你开始考虑它时,它通常并不重要。例如,管理层正在开会,他们刚刚决定解雇一名员工,并且必须撤销访问权限。决定已经做出,但系统中还没有人采取行动。已经有一个现实世界的比赛条件。大多数情况下,执行授权规则时谁先点击并不重要。访问权限通常甚至会在许多系统中缓存一段时间。 @KyleFong 听起来不错,但我不是该主题的专家。我什至不明白我是如何获得这么多赞成票的,可能其他人也不是专家。 :D【参考方案2】:

但是我们应该在哪里执行这个规则,我们应该如何执行呢?

考虑用例,用户名只能由管理员更改,因此应将授权传递给每个实体和域服务行为/方法(包括工厂方法)。

在以下示例中,应用程序服务是反映应用程序/API 用例的服务。它依赖于域的存储库和授权服务。将所有必需的依赖项注入到应用程序服务中。

根据你的例子,我们可以修改如下:

UserRepository interface
    User getOneById(id)

AuthorizationService interface
    ActiveUser getActiveUser(String token)

ActiveUser interface
    Role getRole()
    Id getId()

ApplicationService

    //dependencies
    UserRepository userRepository
    AuthorizationService authorizationService
    ...

    editProfile(RequestContext context, EditUserProfileCommand command)
        activeUser = authorizationService.getActiveUser(context.getAccessToken)
        User user = userRepository.getOneById(command.id)
        user.changeName(activeUser, command.name)
        user.changeHobbies(activeUser, command.hobbies)
        user.changeCV(activeUser, command.cv)
        userRepository.save(user)

===
User
    changeName(ActiveUser activeUser, String name)
    changeHobbies(ActiveUser activeUser, String[] hobbies)
    changeCV(ActiveUser activeUser, String cv)

//example how to handle in entity behavior
changeName(ActiveUser activeUser, String newName)
    if activeUser.getRole() != UserRole.ADMINISTRATOR
        throw UnauthorizedException()
    this.name = newName

【讨论】:

以上是关于域驱动设计中的访问控制的主要内容,如果未能解决你的问题,请参考以下文章

域驱动设计:如何访问聚合根的子节点

Linux驱动设备中的并发控制

如何从 Grails 中的控制器访问域属性?

微服务中的授权 - 如何使用 ACL 处理域对象或实体级别的访问控制?

同一域上的访问控制允许来源问题

域客户端访问总是指向其他的域控制器