65.9K
CodeProject 正在变化。 阅读更多。
Home

一个功能相当强大的授权子系统, 具有行级安全性功能 (AFCAS)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (22投票s)

2008年10月23日

LGPL3

25分钟阅读

viewsIcon

107937

downloadIcon

1128

在数据库服务器级别实现基于角色的授权控制 (RBAC) 模型, 具有行级访问控制功能。

在我之前的文章在 SQL 数据库上表示有向无环图 (DAG) 的模型中,我描述了一种在 SQL 数据库中表示层次结构和一般 DAG 的方法,以及一种高效的访问方法。

在本文中,我将进一步利用该技术来解决授权和基于行的访问控制问题。这是一个重要的目标,因为一个好的解决方案将来可能会在许多项目中重复使用,因为大多数重要的企业应用程序都需要以某种方式实现这样的子系统。

以下是我最初为这样一个子系统(我称之为 AFCAS)设定的设计目标:

  1. 子系统应该是一个通用组件,以便无需重大修改即可重复使用。这也意味着它应该能够独立运行,不依赖于它应该安全访问的对象。
  2. 子系统应该支持类似 MS Windows 的访问控制列表 (ACL),这将允许为数据库表中的行等每个资源定义添加、删除、更新、查看等操作。这实际上与目标 #1 密切相关,因为这样的系统将广泛适用。
  3. 子系统对系统其余部分的设计应该影响微乎其微。
  4. 子系统必须支持对数据库表的行级安全访问,并且性能损失合理,即受保护表中的记录数量不应对性能产生重大影响,即使是拥有数百万行的表。
  5. 子系统应该为报告和分析工具提供透明的解决方案。更具体地说,子系统必须能够使用数据库视图进行自动授权过滤,以便我们的报告工具能够透明地启用行级安全。

    除了这些更高级别的目标之外,我还设定了以下更具体的要求:

  6. 子系统应该允许使用现有的 Active Directory 组作为安全主体。也就是说,它应该能够使用 MS Windows 的用户身份,并且还必须支持包含其他组作为成员的组。此功能对于支持企业级角色和包含企业角色作为成员的应用程序角色至关重要;以及全面支持嵌套的 Active Directory 组,总的来说。
  7. 子系统应该支持操作和资源的层次结构。这将使该组件在更多场景中可用。
  8. 子系统应该为离线场景提供良好的支持。

    以下被认为是不必要或实施成本过高的:

  9. 表达任意授权规则的能力:这意味着允许脚本语言或某种授权规则插件架构。两者都会增加主要复杂性而收益不大。此外,将此类规则归类为授权方面通常相当困难;它们通常是核心业务方面,最好作为业务层的一部分实现。[1] 在基于行的访问情况下可能导致的可观性能损失是省略此能力的另一个原因。
  10. 保护给定行的单个属性的能力:理由是这不是一个非常普遍的要求,并且会使设计复杂得多。此外,将其此类列分解为它们自己的专用表可能是一种更好的数据库设计。
  11. 补充授权的身份验证组件:因为身份验证被认为最好在操作系统和/或开发平台级别处理,并且所有主要平台都内置了支持。
  12. 一种标签方案,允许将多个授权标记与资源关联:因为这样的系统需要在 select 语句的 where 子句上进行标签评估过程,从而对大型数据集造成重大性能损失(由于表扫描而不是索引查找)。此外,资源层次结构可以用于消除对这种方案的需求。有关示例,请参阅“模拟多个标签”部分。

如果没有在 SQL Server 中编写扩展存储过程来返回给定用户的组,反之亦然,就无法轻易实现目标 #6。即使我们编写了这样的扩展存储过程,它也会将组件锁定在 SQL Server 中,更重要的是,会造成显著的性能损失(因为如果此类存储过程用于 WHERE 子句中,SQL 引擎可能无法使用索引并退回到表扫描)。因此,我决定使用一个代理定期将 Active Directory 用户和组复制到 SQL 数据库。这还有一个额外的好处,就是允许 Active Directory 用户和组与应用程序定义的用户和组无缝混合,这对于面向公众的 Web 应用程序来说是一种常见场景:在内网使用 Active Directory 和集成 Windows 安全,为外部方使用应用程序级别定义的用户。

基本原理

授权子系统必须回答的通用问题是:用户 P 是否被授权对资源 R 执行操作 O

这个问题可以建模为检查存储库中是否存在元组 (P, O, R),在我们的例子中是一个 SQL 数据库,它以这种元组的形式存储授权信息。显然,检查这种元组是否存在所需的 SQL 非常容易构建。如果我们假设我们用 P 表示用户的身份(主键),用 O 表示操作,用 R 表示资源,并将这些谓词存储在一个名为 AccessPredicate 的表中,那么清单 1 中的 SQL 查询可用于获取所需的答案:如果它返回一个非空集,则授予授权,否则不授予。

       SELECT PrincipalId, OperationId, ResourceId
          FROM AccessPredicate
          WHERE PrincipalId = 'P'
            AND OperationId = 'O'
            AND ResourceId = 'R' )
清单1:返回授权元组的简单 SQL

不幸的是,这个问题的解决方案并不那么简单。你可能已经看到,清单 1 中的查询存在一个根本缺陷:如果用户本身没有被授予访问权限,但用户所属的某个组被授予了访问权限怎么办?例如,用户是技术人员组的成员,并且访问权限被授予了该组[2]

当然,在这种情况下,查询将返回不完整的列表,因此授权不正确。

可以推断用户确实具有访问权限,方法是:首先查询用户所属的所有组,然后对用户所属的所有组运行清单 1 中的查询,然后返回所有查询结果的并集,同时将所有组替换为 P。如果结果集包含元组 (P, O, R),则授予访问权限,否则不授予。不幸的是,这种方法仍然存在一个问题:如果用户所属的组本身也是其他组的成员,并且访问权限实际上是授予了这些祖父组之一怎么办?简而言之,所有这些尝试都是徒劳的,因为 SQL 不支持查询这种具有递归关系的数据。

为了解决这个递归问题并适应 SQL 无法表达递归关系的限制,AFCAS 依赖于存储 DAG 关系信息的完整闭包。这里的设计使用了我之前文章中描述的 DAG 闭包存储技术的一个略微修改版本(使用 Guozhu Dong 等人描述的删除方法,以最大限度地减少对数据库大小的影响),用于所有主体、操作和资源 DAG 结构。该技术允许我们使用纯 SQL 来回答关于组、操作或资源递归关系的任何问题。(请参阅该文章中的“如何使用 Edge 表”部分,以及那里的图 5清单 4,以查看示例。)例如,以下 SQL 可用于获取该文章图 5 中用户 Jale 的所有成员资格。

    SELECT EndVertex
        FROM Edge
        WHERE StartVertex = 'Jale'
清单2:获取用户“Jale”组会员资格的 SQL;更多详情请参阅文章

这将返回

EndVertex       
--------------- 
ABCTechnicians  
Technicians     
Users

(3 row(s) affected)

因此,授权问题现在可以简化为创建一个以某种方式结合清单 1 和清单 2 中的查询的查询。

请注意,清单 1 中的查询隐式假定操作和资源是简单对象,没有任何用户和组所具有的递归关系。这无疑是一个很大的限制,特别是对于操作而言,因为允许操作层次结构肯定会极大地减少用于授权的管理和编程工作。因此,让我们尝试为更一般的情况制定一个公式,即操作和资源也都允许具有递归关系,就像图 1 中所示的简单层次结构一样。

figure-1.gif

1:用户、操作和资源结构示例

假设操作和资源的层次结构也具有与组层次结构相同的语义,即操作 O 包含 O1;资源 R 包含 R1 等。还假设授予组 G 访问资源 R 以执行操作 O。我们将其表示为元组 (G, O, R)。那么元组 (G, O, R) 的存在意味着由图 1 中所有可能的实体组合构成的元组的存在,例如 (G1, O, R)、(G2, O, R)、(U, O, R) 等。因此,对 G、O 或 R 的任何子项的授权检查都必须确保检查由图 1 中所有顶点组合构成的元组是否存在。反之,如果我们只想检查隐含或直接授权元组的存在,例如 (U, O1, R2),我们就必须形成这些实体的所有直接和隐含父顶点组合。

根据上一段落中阐述的原则,现在让我们构建一个查询,该查询将列出用户 U1 对资源 R2 上的操作 O1 的所有直接或隐含授权,即元组 (U1, O1, R2)。清单 3 中给出了结果查询。

    SELECT PL.PrincipalId, OL.OperationId, RL.ResourceId
     FROM ( SELECT EndVertex as PrincipalId -- groups of U1
                FROM Edge 
                WHERE Source = 'Principal'
                  AND StartVertex = 'U1'
            UNION
            SELECT 'U1' as PrincipalId  -- U1 itself
            )  PL -- Part 1
            CROSS JOIN (
                SELECT EndVertex as OperationId -- parent operations of O1
                    FROM Edge
                    WHERE Source = 'Operation'
                      AND StartVertex = 'O1'
                UNION
                SELECT 'O1' as OperationId -- O1 itself
            )  OL -- Part 2
            CROSS JOIN (
                SELECT EndVertex as ResourceId -- parent resources of R2
                    FROM Edge
                    WHERE Source = 'Resource'
                      AND StartVertex = 'R2'
                UNION
                SELECT 'R2' as ResourceId  -- R2 itself
            ) RL -- Part 3 
              INNER JOIN AccessPredicate ACL
                 ON PL.PrincipalId = ACL.PrincipalId
                AND OL.OperationId = ACL.OperationId
                AND RL.ResourceId = ACL.ResourceId
清单3:返回给定三元组(U1、O1、R2)的所有直接或隐含授权的查询

请注意,我们假设所有实体类型的层次结构都以相同的 Edge 表表示,使用 Source 列来分隔不同类型的实体。此查询对于给定的 (P, O, R) 元组运行速度很快。因此,它非常适合编写 `IsAuthorized` 存储过程或 SQL 函数。但是,它不能用于行级安全,因为它需要提前知道我们正在查找哪个元组。

行级安全实现

高效的行级安全机制只能在数据库级别实现,因为将数据移动到客户端或中间层然后过滤未经授权的数据会带来以下后果:

  1. 不可伸缩:虽然对于小数据集来说,将所有记录从数据库移动到客户端或中间层可能是可接受的,但如果需要保护的数据超过数千条记录,则根本无法工作。
  2. 将不允许报告和分析软件按预期工作:我不确定,如果我们在中间层或客户端进行授权检查,此类用于即席报告和分析的报告应用程序是否能按预期运行。
  3. 安全性较低:当我们想要保护数据时,通常出于某种充分的理由。允许未经授权的数据传输到客户端会构成安全威胁,因为客户端软件可以很容易地被操纵以泄露它从服务器接收到的所有数据,或者可以使用网络嗅探技术。

因此,对于重要应用程序而言,将授权检查的职责委托给中间层或客户端不是一个选择。授权检查必须在数据库级别完成。

然而,如前所述,我们清单 3 中的查询不适用于过滤掉未经授权的行。它只允许我们确定用户是否被授权访问特定资源。我们需要一种机制来列出给定用户的所有授权资源。

数据库视图是一个很好的选择,因为它可以用于从资源表中过滤掉未经授权的行。事实上,利用这种技术以及集成的数据库服务器身份验证方案,可以在数据库级别构建一个非常强大的安全墙。

清单 4 是泛化清单 3 中查询的查询。它返回存储在 `AccessPredicate` 表中的给定显式授权授予集的所有直接和隐含元组。

    CREATE VIEW FlatGrantList 
    AS
    SELECT DISTINCT
        PL.StartVertex AS PrincipalId
       ,OL.StartVertex AS OperationId
       ,RL.StartVertex AS ResourceId
     FROM ( SELECT E.EndVertex, E.StartVertex 
            FROM Edge E
                INNER JOIN AccessPredicate AP
                    ON E.EndVertex = AP.PrincipalId
                WHERE E.Source = 'Principal'
            UNION 
            SELECT PrincipalId, PrincipalId 
            FROM AccessPredicate) PL
            CROSS JOIN (
                SELECT E.EndVertex, E.StartVertex 
                FROM Edge E
                    INNER JOIN AccessPredicate AP
                       ON E.EndVertex = AP.OperationId
                WHERE E.Source = 'Operation'
                UNION 
                SELECT OperationId, OperationId 
                FROM AccessPredicate) OL
            CROSS JOIN (
                SELECT E.EndVertex, E.StartVertex 
                FROM Edge E
                    INNER JOIN AccessPredicate AP
                       ON E.EndVertex = AP.ResourceId
                WHERE E.Source = 'Resource'
                UNION 
                SELECT ResourceId, ResourceId 
                FROM AccessPredicate) RL
            INNER JOIN AccessPredicate ACL 
               ON PL.EndVertex = ACL.PrincipalId
              AND RL.EndVertex = ACL.ResourceId
              AND OL.EndVertex = ACL.OperationId
清单4:查询以返回所有显式和隐含授权

请注意,清单 4 中的查询也假定资源层次结构存储在同一个 Edge 表中,但显然,这可以在单独的表中完成。

您可能已经开始认为,对于大多数数据库应用程序而言,资源不是层次结构,而是表中的简单行。对于这种情况,我们可以简化清单 4 中的查询,并使其更快一些。简化的查询如清单 5 所示。

    CREATE VIEW FlatGrantListWithFlatResources
    AS
    SELECT DISTINCT
        PL.StartVertex AS PrincipalId
       ,OL.StartVertex AS OperationId
       ,ACL.ResourceId
     FROM ( SELECT E.EndVertex, E.StartVertex 
            FROM Edge E
                INNER JOIN AccessPredicate AP
                   ON E.EndVertex = AP.PrincipalId
                WHERE E.Source = 'Principal'
            UNION 
            SELECT PrincipalId, PrincipalId 
            FROM AccessPredicate) PL
            CROSS JOIN (
                SELECT E.EndVertex, E.StartVertex 
                FROM Edge E
                    INNER JOIN AccessPredicate AP
                       ON E.EndVertex = AP.OperationId
                WHERE E.Source = 'Operation'
                UNION 
                SELECT OperationId, OperationId 
                FROM AccessPredicate) OL
            INNER JOIN AccessPredicate ACL 
               ON PL.EndVertex = ACL.PrincipalId
              AND OL.EndVertex = ACL.OperationId
清单5:返回所有具有平面资源的显式和隐含授权的查询

进一步的简化将是消除操作的层次结构。如果我们已经知道所有查看数据的操作都是非层次化的,并且此视图仅用于行级安全性,那么这很有意义。生成的 SQL 代码如清单 6 所示。

    CREATE VIEW FlatGrantListWithFlatResourcesAndOperations 
    AS
    SELECT DISTINCT 
        PL.StartVertex AS PrincipalId
       ,ACL.OperationId
       ,ACL.ResourceId
     FROM ( SELECT E.EndVertex, E.StartVertex 
            FROM Edge E
                INNER JOIN AccessPredicate AP
                   ON E.EndVertex = AP.PrincipalId
                WHERE E.Source = 'Principal'
            UNION 
            SELECT PrincipalId, PrincipalId 
            FROM AccessPredicate) PL
        INNER JOIN AccessPredicate ACL 
           ON PL.EndVertex = ACL.PrincipalId
清单6:具有平面资源和操作的查询,返回所有显式和隐含授权

最后,清单 7 是具有平面操作和分层资源的查询。这将是创建分层资源的行级安全视图的完美选择。

    CREATE VIEW FlatGrantListWithFlatOperations 
    AS
    SELECT DISTINCT
        PL.StartVertex AS PrincipalId
       ,ACL.OperationId
       ,RL.StartVertex AS ResourceId
     FROM ( SELECT E.EndVertex, E.StartVertex 
            FROM Edge E
                INNER JOIN AccessPredicate AP
                   ON E.EndVertex = AP.PrincipalId
                WHERE E.Source = 'Principal'
            UNION 
            SELECT PrincipalId, PrincipalId 
            FROM AccessPredicate) PL
            CROSS JOIN (
                SELECT E.EndVertex, E.StartVertex 
                FROM Edge E
                    INNER JOIN AccessPredicate AP
                       ON E.EndVertex = AP.ResourceId
                WHERE E.Source = 'Resource'
                UNION 
                SELECT ResourceId, ResourceId 
                FROM AccessPredicate) RL
            INNER JOIN AccessPredicate ACL 
               ON PL.EndVertex = ACL.PrincipalId
              AND RL.EndVertex = ACL.ResourceId
清单7:返回所有具有平面操作的显式和隐含授权的查询

一旦我们制定了 FlatGrantListXXX 视图,现在通过简单地为该特定资源构建一个视图,该视图与 FlatGrantListXXX 连接,并在用户身份和操作以及为每个受保护表定义的特殊视图操作上放置 WHERE 子句,就可以非常容易地过滤掉未经授权的资源行。有两种方法可以实现这一点,各有优缺点:

  1. 使用存储过程返回授权行,该过程将用户身份作为参数:此解决方案仅适用于存在作为数据库唯一通道的中间层组件的情况。所有用户可以共享相同的数据库连接字符串,因此由于连接池的可能性而成为可伸缩的解决方案。但是,如果用户需要直接访问数据库以满足即席报告和数据分析要求,则无法使用此解决方案,因为他们可以向服务器发送任何他们想要的身份。此外,即席报告工具无法在这种情况下有效地使用存储过程并进行 SQL 连接(他们必须拉取所有数据,然后在报告服务器上进行连接,这可能不可行)
  1. 使用数据库集成安全性并为每个资源构建安全视图:此解决方案要求从基础表中删除所有数据库访问权限,并应使用存储过程进行更新和删除操作。此解决方案的主要优点是其对应用程序代码以及即席报告和分析工具的透明性。主要缺点是可伸缩性。数据库服务器必须为每个客户端维护一个连接,这对于大量并发用户来说可能是一个问题。它还具有更高的管理开销,并增加了数据库的攻击面。如果应用程序设计是多层的,该技术还需要在 Windows 网络上使用 Kerberos 委托。我在此处指出这一点,因为 Kerberos 委托在 Windows 网络中似乎不能完美运行(至少对我而言,使用 Windows Server 2003 网络和 SQL Server 2005)

清单 8 中的 SQL 代码展示了如何实现这两个选项。

    -- a stored procedure that implements option 1
    CREATE PROC GetAuthorizedResourceXYZ (
        @UserId varchar(256)
    ) AS
    BEGIN
        SELECT R.*
            FROM ResourceXYZ R
                INNER JOIN FlatGrantListWithFlatOperations G
                   ON R.Id = G.ResourceId
            WHERE G.PrincipalId = @UserId
              -- the ‘view’ operation defined for ResourceXYZ          
              AND G.OperationId = 'View.ResourceXYZ' 
    END
 
    -- a view that implements option 2
    CREATE VIEW SecurityEnabledResourceXYZView
    AS
        SELECT R.*
            FROM ResourceXYZ R
                INNER JOIN FlatGrantListWithFlatOperations G
                   ON R.Id = G.ResourceId
            WHERE G.PrincipalId = SYSTEM_USER  
              -- the ‘view’ operation defined for ResourceXYZ          
              AND G.OperationId = 'View.ResourceXYZ'
清单8:在数据库级别实现的行级安全

请注意,清单 8 中的代码不仅限于查看行。可以想象,这样的代码可以用于,例如,删除屏幕,其中向用户显示他有权删除的物品列表等等。我们所需要的只是定义一个操作,并授予相关组在所需资源上执行该操作。

访问谓词和资源层次结构的维护

请注意,清单 8 中的代码只回答了从数据库获取数据的问题。行级安全还必须有一个支持数据库更新和插入的支柱。显然,必须有某种机制来维护 `AccessPredicate` 表中的数据以及资源层次结构。不幸的是,目前没有通用的解决方案来解决这个问题,因为它高度依赖于应用程序。然而,AFCAS 提供了维护资源层次结构和访问谓词的基本设施。然后,每个应用程序将决定是提供具有专用屏幕的显式授权授予/撤销用例,还是根据用户和/或资源的属性进行自动授权。可以说,自动资源层次结构维护或自动授予的更好位置是用于资源表上的插入/更新操作的特定于应用程序的存储过程。但是,所有这些都取决于具体的场景。

应用程序接口

数据库端的工作完成后,我们现在可以转向系统的应用程序接口。AFCAS 主要面向需要受控访问保护的数据库应用程序。然而,AFCAS 并不局限于需要行级安全的数据库应用程序。它具有非常复杂的授权方案,可以满足大多数授权需求。

显然,AFCAS 不能对它要保护的资源做出太多假设。但是,必须存在一个客户端应用程序和 AFCAS 本身都能理解的资源链接。AFCAS 数据库出于效率原因,为其保护的每个资源使用一个字符串标识符。这是安全的,因为每个持久化对象行都必须具有一组唯一的属性,通常可以轻松地转换为字符串。如果需要转换以生成字符串标识符,则必须由 AFCAS 的客户端定义。因此,清单 9 是 AFCAS 实现的基本接口。

/// <summary>
/// The main interface to be used by the clients that need to make authorization decisions.
/// An instance of this interface is provided by <see cref="Afcas"/> class.
/// </summary>
public interface IAuthorizationProvider {
    // the method to justify the existence of this interface
    bool IsAuthorized( string principalId, string operationId, ResourceHandle resource );

    // these method also have uses for authorization purposes
    bool IsMemberOf( string groupId, string memberId );
    bool IsSubOperation( string opId, string subOpId );
    bool IsSubResource( ResourceHandle resource, ResourceHandle subResource );
  
    // These two methods are for offline support
    IList< ResourceAccessPredicate > GetAuthorizationDigest( string principalId );
    IList< Operation > GetAuthorizedOperations( string principalId,
        ResourceHandle resource );

    // This can be used to allow the user to browse authorized resources
    IList< ResourceHandle > GetAuthorizedResources( string principalId,
        string operationId );
}
清单 9:AFCAS 的主要接口

ResourceHandle 是抽象基类,封装了将给定资源转换为字符串标识符以及反之的逻辑。AFCAS 的用户必须为其希望保护的每个资源编写子类并向 AFCAS 注册。有关示例,请参阅随附的源代码。

`ResourceAccessPredicate` 是一个结构,它为特定的访问授权(或拒绝)保存元组 (`PrincipalId`, `OperationId`, `ResourceId`) 数据。其定义见清单 10。

    public struct ResourceAccessPredicate {
        // ...
        public string PrincipalId { get { return _PrincipalId; } }
        public string OperationId { get { return _OperationId; } }
        public ResourceHandle ResourceId { get { return _ResourceId; } }
        public ResourceAccessPredicateType AccessPredicateType { get {
            return _AccessPredicateType; } }
    }
清单10:ResourceAccessPredicate 结构(此处为部分定义)

Operation 具有类似的目的,并维护特定操作的 IdName 和 Description 等属性。

清单 11 是我用来指定访问类型的枚举 ResourceAccessPredicateType。正如您所看到的,AFCAS 支持显式拒绝,这些拒绝被认为比授权具有更高的优先级,就像在 MS Windows 的 ACL 中一样。我最初认为这将是一个有用的功能,可以简化管理。然而,事实证明它是一个不太需要的功能,因此我取消了它的数据库实现,因为它会带来性能损失[3]。但是,我仍然在 .NET 代码中保留了它,以便如果确实需要,可以以后再启用它。

    public enum ResourceAccessPredicateType {
        Grant = 0,
        Deny = 1
    }
清单11:ResourceAccessPredicateType

IAuthorizationProvider 接口简单易懂且易于使用。它允许依赖子系统使用主体(很可能是登录用户)、操作的字符串 ID 和可选的 ResourceHandle 进行查询。ResourceHandle 被认为是可选的,因为某些操作没有有意义的资源上下文。在这种情况下,约定应将 NullResource.Instance 设置为。以下是使用 AFCAS 的预期方式:

        public void SomeUsefulWork( ResourceTypeXyz someResource ) {
            IAuthorizationProvider provider = Afcas.GetAuthorizationProvider( );
 
            // A resource handle factory for this particular type must be defined
            // and registered
            // using Afcas.RegisterHandleFactory( )
            // prior to calling this method, preferrably during application startup
            ResourceHandleFactory fac = Afcas.GetHandleFactory( "ResourceTypeXyz" );

            // here the app should replace hard coded userid and operationIds.
            if( !provider.IsAuthorized( "domain\\kemal", "SomeUsefulWorkOpId",
                fac.GenerateResourceHandle( someResource ) ) ) {
                throw new SecurityException(
                    "You are not authorized to perform SomeUsefulWork" );
            }

            // authorized, do the work
            ...
        }
清单 12:AFCAS 示例用法

Administration

请注意,IAuthorizationProvider 没有定义任何用于维护用户、组和操作的方法。这是 AFCAS 的管理控制台组件的工作。定义了一个名为 IAuthorizationManager 的接口,用于管理用户、组和操作及其层次结构。还提供了该接口的实现。详情请参阅随附的源代码。

唯一缺少的部分是使用 `IAuthorizationManager` 接口的管理控制台 UI,它由以下部分组成:

用户和组管理:仅依赖 AD 同步可以省去我们编写和管理用户/组管理控制台应用程序的麻烦,因为标准 Active Directory 工具可以用于此类目的。但是,对于面向外部的具有应用程序级用户的互联网应用程序,仍然需要编写此类控制台应用程序。我没有编写这样的工具,仅仅是因为目前还没有这样的需求。

操作管理:给定应用程序的操作列表通常在应用程序发布到生产环境时是固定的。操作列表仅在添加/删除功能(意味着源代码更改)时才会更改。因此,对于大多数应用程序,不需要由 IT 运营人员使用的操作管理控制台。

资源层次结构管理:资源层次结构的管理通常是特定于应用程序的任务,大多数应用程序都有自己的 UI 来管理资源层次结构。然而,IAuthorizationManager 接口提供了维护层次结构的功能。UI 应该由客户端应用程序编写。

授予/撤销访问权限:这部分也高度依赖于应用程序,大多数应用程序应该创建自己的 UI,该 UI 还应显示有关要保护资源的有意义的上下文信息。实际工作由 AFCAS 通过其 IAuthorizationManager 实现完成。

简而言之,这里展示的基本 AFCAS 组件完全避免了编写管理控制台。但是,仍然存在对用户和组管理控制台的需求。我将来有空时会解决这个问题。

实现

所有重要的计算都在数据库级别执行。其大部分授权计算都以 SQL 查询的形式委托给数据库服务器,这些查询已在前面的部分中介绍。原因是,在执行集合代数(AFCAS 所有工作的基础)方面,SQL 优于任何命令式语言。

AFCAS 数据库由四张表组成:

  1. Principal:存储用户和组的基本信息。
  2. Operation:存储要保护的操作的名称和代码,一旦设置,数据大部分是静态的。
  3. Edge:此表存储主体、操作和资源的 DAG 模型。有关其用法的完整解释,请参阅我的上一篇文章
  4. AccessPredicate:存储显式授予(或拒绝)的访问权限。这是一个关联表,用于将资源链接到访问权限。

这些表的完整结构以及存储过程的 SQL 代码和 C# 代码都可以在本文附带的 zip 文件中找到。

Active Directory (AD) 同步和管理

如前所述,AFCAS 旨在利用现有的 Active Directory 用户和组帐户。我们还注意到,AFCAS 将使用纯 SQL 查询在数据库中执行所有授权计算。为了解决将现有 AD 用户和组与 AFCAS 数据库中定义的用户和组关联起来的问题,我创建了一个同步(仅从 AD 到 SQL 数据库)模块,该模块会将 AD 中的用户和组复制到 SQL Server。它使用 AD 用户和组的 `objectID` 属性作为同步比较的基准。同步频率可以根据需要调整,最短可达几分钟。在我们的案例中,每日同步即可。

详情请参阅附带的源代码。

ASP.NET 会员资格和角色提供程序

ASP.NET 拥有一个相当实用的身份验证和授权基础设施,顺便说一下,它的使用并不局限于 Web 应用程序。有关更多信息,请参阅 MSDN 主题保护 ASP.NET 网站。但是,其授权部分的功能远不及 AFCAS,尤其是在低级安全性方面。因此,如果能利用 ASP.NET 安全的现有身份验证部分并用 AFCAS 替换授权部分,那将是非常棒的。

看来将 AFCAS 集成到 ASP.NET 会员资格和角色提供程序模型中并不困难。正如我之前所指出的,我相信使用平台设施进行身份验证是最好的选择。因此,我没有任何替换身份验证部分的计划。然而,基于 AFCAS 的 RoleProvider 可能会非常有用,我计划在有空的时候编写一个。

为常见场景模拟多个标签

许多处理敏感信息的组织都采用了分类方案,对信息的敏感程度进行评级,例如“未分类”、“敏感”、“机密”和“高度机密”。他们还将信息分为集群,以方便按需知悉原则,例如地理位置:用户 A 仅处理国家 X 的数据,因此他只能被授予国家 X 的访问权限。

AFCAS的资源嵌套能力使其很容易支持此类分类方案。图2中的示例资源层次结构展示了如何使用它来支持此类场景。

image002.gif

2:示例资源层次结构

请注意“根”节点:它使管理员能够轻松且无人工错误地授予高级管理人员“所有”数据的访问权限。请看国家数据是如何保存在其独立的独立分支中的。一旦用户准备好将“新文档”保存到系统,他们就必须确定数据属于哪个国家,然后确定安全分类级别。然后他们可以使用 AFCAS API 在分类和文档之间建立适当的关系。

将一个文档置于多个安全类别下是没有意义的,但可以想象某些文档必须归入多个国家,如 图2 中的“Doc11”:这对于 AFCAS 来说不是问题。但是,当一个项目归入多个主要类别时,用户必须充分了解其安全隐患。

图 2 还强调了自动授权机制的重要性。如果没有这种机制,如果需要保护的资源数量达到数万,任何授权方案都会失败。一个好的默认策略是分析用户当前的应用程序角色,并在提交全新数据时自动授予所有这些角色查看权限。然而,这一切都取决于应用程序,并且在许多情况下肯定不适用。尽管如此,建立访问授权策略的需求仍然是一个常见问题。

性能和可伸缩性

对于需要极高吞吐量的应用程序,AFCAS 的数据库设计存在一些潜在的瓶颈。一个明显的地方是 Edge 表,因为它用于存储所有主体、操作和资源的关系信息。在高吞吐量环境中,可能会发生竞争条件以获取 Edge 表上的数据库锁。

解决此类问题的一种方法是为每个实体维护一个单独的 Edge 表:即 Principal、Operation 和应用程序希望保护的每个资源。显然,这将需要更改视图,并且还需要复制数据库级别闭包维护操作的存储过程代码。

进一步的性能改进是维护**独立**的主体和操作实体,以及每个资源的独立 Edge 表。主体和操作必须在主副本中维护,然后定期复制到特定于资源的副本中。这将进一步减少可能的竞争条件,从而提高性能。它还避免了重复 DB 代码的需要,因为只需要维护 Principal 和 Operation 表的主副本。其余部分由复制代码处理(复制代码将执行一些智能复制,例如仅复制给定资源的适用操作子集和允许的用户组)

结束语

AFCAS 已被证明是我开发的一个有用的子系统,我希望对读者来说也一样。欢迎提交错误报告、建议和意见。

[1] 例如,不清楚规则“应用程序 xxx 仅允许在 09:00-17:00 执行”是属于授权域还是业务域。仔细审查后,许多此类复杂的授权规则最终只是另一组要实现的业务规则。

[2] 一种流行的方法是授权规则永不改变:一旦系统中的角色被定义,只有与应用程序角色最初相似的组才被授予访问权限。之后,只允许更改组成员。这将简化授权子系统的管理和审计,因为此策略只保留授权方程中的一个变量。

[3] 将显式拒绝纳入之前提出的查询中是相当容易的。我们只需要构建授权集(及其隐含元组),然后使用标准 SQL 差集操作或简单地使用带 WHERE 子句的左外连接来删除所有显式拒绝(及其隐含元组)。由于性能问题,我将其从 SQL 视图和 `IsAuthorized` 存储过程中删除。

历史

  • 2008年10月23日——文章发布
  • 2009年2月12日——更新源代码,显著提升SQL侧性能。
© . All rights reserved.