web-dev-qa-db-ja.com

ServiceStackで「ユーザーごとのオブジェクトごと」ベースでアクセス許可を制御するのに最適な設計パターンは?

ServiceStackがアクセス許可を制御するためのRequiredRole属性を提供していることを知っていますが、これは私のユースケースでは完全に機能しません。ユーザーが作成したコンテンツがたくさんあるウェブサイトがあります。ユーザーは、明示的な権限を持つドキュメントのみを編集できます。アクセス許可は、オブジェクトまたはオブジェクトのグループごとに制御されます。したがって、ユーザーがグループの管理者であれば、そのグループが管理するすべてのドキュメントを編集できます。

per object per userベースでリクエストへのアクセスを制御するのに最適な設計パターンは何ですか?すべてのAPIエンドポイントの95%に影響を与えるため、DRY方法論を使用してこれにアプローチします。

また、これをFluentValidationと統合して適切なHTTP応答を返すことはできますか?

どうもありがとう、

リチャード。

21
richardwhatever

ServiceStackアプリケーションでオブジェクトごとの権限を使用しています。事実上、これはAccess-Control-List(ACL)です。

Working Self Hosted Console Example を作成しました。これはGitHubでフォークできます。

ACLパターン:

以下の図に示すデータベース構造を使用します。これにより、データベース内のドキュメント、ファイル、連絡先などのリソース(保護したいリソース)にはすべてObjectType id。

Database

権限テーブルには、特定のユーザー、特定のグループ、特定のオブジェクト、特定のオブジェクトタイプに適用されるルールが含まれ、null値がワイルドカードのように扱われる組み合わせでそれらを柔軟に受け入れることができます。

サービスとルートの保護:

それらを処理する最も簡単な方法は、リクエストフィルター属性を使用することです。私の解決策では、リクエストルート宣言にいくつかの属性を追加するだけです:

[RequirePermission(ObjectType.Document)]
[Route("/Documents/{Id}", "GET")]
public class DocumentRequest : IReturn<string>
{
    [ObjectId]
    public int Id { get; set; }
}

[Authenticate]
public class DocumentService : Service
{
    public string Get(DocumentRequest request)
    {
        // We have permission to access this document
    }
}

フィルター属性呼び出しRequirePermissionがあります。これにより、DTOを要求している現在のユーザーDocumentRequestDocumentオブジェクトにアクセスできることを確認するチェックが実行され、ObjectIdは、プロパティIdによって指定されます。私のルートのチェックを配線するのはこれで全部です。ですから、それは非常に乾燥しています。

RequirePermission要求フィルター属性:

権限をテストするジョブは、サービスのアクションメソッドに到達する前に、filter属性で行われます。優先順位が最も低いため、検証フィルターの前に実行されます。

このメソッドは、アクティブなセッション、カスタムセッションタイプ(詳細は以下)を取得します。これにより、アクティブなユーザーのIDと、アクセスが許可されているグループIDが提供されます。また、リクエストからobjectIdがあればを決定します。

リクエストDTOのプロパティを調べて、[ObjectId]属性。

その情報を使用して、アクセス許可のソースを照会し、最も適切なアクセス許可を見つけます。

public class RequirePermissionAttribute : Attribute, IHasRequestFilter
{
    readonly int objectType;

    public RequirePermissionAttribute(int objectType)
    {
        // Set the object type
        this.objectType = objectType;
    }

    IHasRequestFilter IHasRequestFilter.Copy()
    {
        return this;
    }

    public void RequestFilter(IRequest req, IResponse res, object requestDto)
    {
        // Get the active user's session
        var session = req.GetSession() as MyServiceUserSession;
        if(session == null || session.UserAuthId == 0)
            throw HttpError.Unauthorized("You do not have a valid session");

        // Determine the Id of the requested object, if applicable
        int? objectId = null;
        var property = requestDto.GetType().GetPublicProperties().FirstOrDefault(p=>Attribute.IsDefined(p, typeof(ObjectIdAttribute)));
        if(property != null)
            objectId = property.GetValue(requestDto,null) as int?;

        // You will want to use your database here instead to the Mock database I'm using
        // So resolve it from the container
        // var db = HostContext.TryResolve<IDbConnectionFactory>().OpenDbConnection());
        // You will need to write the equivalent 'hasPermission' query with your provider

        // Get the most appropriate permission
        // The orderby clause ensures that priority is given to object specific permissions first, belonging to the user, then to groups having the permission
        // descending selects int value over null
        var hasPermission = session.IsAdministrator || 
                            (from p in Db.Permissions
                             where p.ObjectType == objectType && ((p.ObjectId == objectId || p.ObjectId == null) && (p.UserId == session.UserAuthId || p.UserId == null) && (session.Groups.Contains(p.GroupId) || p.GroupId == null))
                             orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
                             select p.Permitted).FirstOrDefault();

        if(!hasPermission)
            throw new HttpError(System.Net.HttpStatusCode.Forbidden, "Forbidden", "You do not have permission to access the requested object");
    }

    public int Priority { get { return int.MinValue; } }
}

権限の優先度:

アクセス許可がアクセス許可テーブルから読み取られると、最も優先度の高いアクセス許可が使用されて、アクセス権があるかどうかが判断されます。アクセス許可エントリがより具体的であるほど、結果の順序付けの際の優先順位が高くなります。

  • 現在のユーザーと一致するアクセス許可は、すべてのユーザーの一般的なアクセス許可よりも優先されます。つまり、UserId == null。同様に、特別にリクエストされたオブジェクトの権限は、そのオブジェクトタイプの一般的な権限よりも優先されます。

  • ユーザー固有の権限は、グループ権限よりも優先されます。つまり、ユーザーはグループのアクセス許可によってアクセスを許可されているが、ユーザーレベルではアクセスを拒否でき、その逆も可能です。

  • ユーザーがリソースへのアクセスを許可するグループと、アクセスを拒否する別のグループに属している場合、ユーザーはアクセスできます。

  • デフォルトのルールは、アクセスを拒否することです。

実装:

上記のサンプルコードでは、このlinqクエリを使用して、ユーザーに権限があるかどうかを確認しています。この例ではモックデータベースを使用しており、独自のプロバイダーに置き換える必要があります。

session.IsAdministrator || 
(from p in Db.Permissions
 where p.ObjectType == objectType && 
     ((p.ObjectId == objectId || p.ObjectId == null) && 
     (p.UserId == session.UserAuthId || p.UserId == null) &&
     (session.Groups.Contains(p.GroupId) || p.GroupId == null))
 orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
 select p.Permitted).FirstOrDefault();

カスタムセッション:

カスタムセッションオブジェクトを使用してグループメンバーシップを格納しました。これらは検索され、ユーザーが認証されたときにセッションに追加されます。

// Custom session handles adding group membership information to our session
public class MyServiceUserSession : AuthUserSession
{
    public int?[] Groups { get; set; }
    public bool IsAdministrator { get; set; }

    // The int value of our UserId is converted to a string!?! :( by ServiceStack, we want an int
    public new int UserAuthId { 
        get { return base.UserAuthId == null ? 0 : int.Parse(base.UserAuthId); }
        set { base.UserAuthId = value.ToString(); }
    }


    // Helper method to convert the int[] to int?[]
    // Groups needs to allow for null in Contains method check in permissions
    // Never set a member of Groups to null
    static T?[] ConvertArray<T>(T[] array) where T : struct
    {
        T?[] nullableArray = new T?[array.Length];
        for(int i = 0; i < array.Length; i++)
            nullableArray[i] = array[i];
        return nullableArray;
    }

    public override void OnAuthenticated(IServiceBase authService, ServiceStack.Auth.IAuthSession session, ServiceStack.Auth.IAuthTokens tokens, System.Collections.Generic.Dictionary<string, string> authInfo)
    {
        // Determine UserId from the Username that is in the session
        var userId = Db.Users.Where(u => u.Username == session.UserName).Select(u => u.Id).First();

        // Determine the Group Memberships of the User using the UserId
        var groups = Db.GroupMembers.Where(g => g.UserId == userId).Select(g => g.GroupId).ToArray();

        IsAdministrator = groups.Contains(1); // Set IsAdministrator (where 1 is the Id of the Administrator Group)

        Groups = ConvertArray<int>(groups);
        base.OnAuthenticated(authService, this, tokens, authInfo);
    }
}

この例がお役に立てば幸いです。不明な点がある場合はお知らせください。

流暢な検証:

また、これをFluentValidationと統合して適切なHTTP応答を返すことはできますか?

検証ではないため、検証ハンドラでこれを実行してはなりません。権限があるかどうかの確認は確認プロセスです。データソースの特定の値に対して何かをチェックする必要がある場合は、検証を実行していません。 この私の別の回答を参照してください これもカバーしています。

27
Scott