Как реализовать ресурсную авторизацию с помощью GraphQL? - PullRequest
0 голосов
/ 25 июня 2019

Какова лучшая практика для реализации авторизации на основе ресурсов. В REST API довольно просто дать каждой конечной точке роль, но как это происходит с GraphQL. Существует только одна конечная точка, в которой выполняется весь запрос.

Пример: Если структура базы данных выглядит следующим образом:

Таблица: Пользователь Столбцы: идентификатор, логин, имя, описание, возраст, любимые темы и т. Д. И т. Д.

Каждый запрос направляется в одну и ту же конечную точку. Пользователь должен иметь возможность запрашивать все свои данные, а также части других пользовательских данных. Когда пользователь отправляет свой собственный идентификатор в запросе (идентификатор пользователя также в JWT, но когда он также отправляет идентификатор в запросе, тогда оба идентификатора могут быть сопоставлены), тогда он должен иметь возможность запросить все столбцы таблицы для его учетная запись. Но когда он отправляет другой идентификатор пользователя (идентификатор JTW и отправленный идентификатор совпадают), пользователь должен иметь возможность запрашивать только некоторые открытые поля, такие как favour_topic или около того.

Я нашел документацию от Microsoft, в которой авторизация на основе ролей является своего рода объяснением, но не с GraphQL

https://docs.microsoft.com/de-de/aspnet/core/security/authorization/resourcebased?view=aspnetcore-2.2

Также я нашел здесь вопрос о stackoberflow, но ни один из них не имел никакого отношения к graphql.

Поскольку документация Microsoft не помогла мне в этом, я пытался создать что-то для себя. Идея заключалась в том, чтобы создать белый список для каждой группы и группы. Пользователь группы будет иметь две дочерние группы, например: - User_with_own_id - user_with_strangers_id

Обе из подгрупп будут иметь разные белые списки. Белый список может выглядеть примерно так:

User_Whitelist_Own_ID = new List<string>();
User_Whitelist_Own_ID.Add("id");
User_Whitelist_Own_ID.Add("login_name");
User_Whitelist_Own_ID.Add("games");
User_Whitelist_Own_ID.Add("game");
User_Whitelist_Own_ID.Add("name");
User_Whitelist_Own_ID.Add("user_relations");
User_Whitelist_Own_ID.Add("messages");
User_Whitelist_Own_ID.Add("message");
User_Whitelist_Own_ID.Add("fk_user_account_id_sender");

Белый список в данном случае - это просто строка типа List со всеми полями, которые пользователь мог или не мог запросить.

И с такими списками я мог бы просто пройти через отправленный запрос пользователя с помощью рекурсивной функции и проверить, разрешены ли все поля, которые нужно запросить:

public bool CheckGraphQLQueryFieldPermission(List<KeyValuePair<string, GraphQL.Language.AST.Field>> fieldSelection, bool boolUnEvenCount, List<string> fieldWhiteList)
    {
        int intMatchedWhiteListFields = fieldSelection.Where(x => fieldWhiteList.Contains(x.Key)).ToList().Count();
        var intAllFields = fieldSelection.Count();

        foreach(KeyValuePair<string, GraphQL.Language.AST.Field> field in fieldSelection)
        {
            if (intMatchedWhiteListFields != intAllFields || boolUnEvenCount)
            {
                return boolUnEvenCount = true;
            }

            int intChildrenCount = field.Value.SelectionSet.Selections.Count();
            if (intChildrenCount > 0)
            {
                boolUnEvenCount = CheckGraphQLQueryFieldPermission(field.Value.GetSelectedFields().ToList(), boolUnEvenCount, fieldWhiteList);
            }   

        }
        return false;
    }

Поскольку я использую MediatR, существует конвейер, который выполняется до того, как будет выполнен запрос. Это место, где я строю это.

Чтобы завершить его, конвейер может выглядеть так:

public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        //get the query context
        var context = request.GetType().GetProperty("context").GetValue(request, null);
        //get all the asked fields
        IHaveSelectionSet fieldAst = (IHaveSelectionSet) context.GetType().GetProperty("FieldAst").GetValue(context, null);
        //get the endpoint name
        string strEndPointName = (string) context.GetType().GetProperty("FieldName").GetValue(context, null);

        //convert all sended arguments into json and then convert to a dictonary
        var json = JsonConvert.SerializeObject(context.GetType().GetProperty("Arguments").GetValue(context, null));
        Dictionary<string, object> dictArguments = JsonConvert.DeserializeObject<Dictionary<string, object>>(json);

        switch(_userService.GetUserRole())
        {
            case RoleEnum.User:

            Guid guidTokenUserId = _userService.GetUserId();
            if (dictArguments != null && dictArguments["user_id"] != null)
            {
                Guid guidArgumentUserID = Guid.Parse(dictArguments["user_id"].ToString());

                //if the user sends his own id then "do something....", when the user sends the id of
                //another user, then check the query with all the whitelists
                if (guidTokenUserId == guidArgumentUserID) 
                {
                    //do something...
                }
                else
                {
                    bool boolQueryValidation = CheckGraphQLQueryFieldPermission(fieldAst.GetSelectedFields().ToList(), false, WhiteList);
                } 
            }

            break;
        }

        var response = next();
        return response;
    }

Это сработало довольно хорошо, но я сомневаюсь, что это правильное решение. Проблема с этими решениями состоит в том, что каждой группе нужны отдельные группы с собственными белыми списками, и каждый белый список должен храниться где-то в месте его сохранения, потому что, когда кому-то удастся изменить список, API-интерфейс нарушается. Кроме того, куча списков типа string не кажется хорошей идеей.

Теперь пользователь может отправить запрос, подобный этому (давайте представим, что отправитель имеет ID = 1):

{ 
 "query":
  "{
    user_account{
      get_user_account(user_id: 1) {
        id,
        login_name,
            password
      }
   }
 }"
}

Этот запрос должен работать. Но когда тот же пользователь с ID = 1 отправляет запрос, где он хочет запросить данные у другого пользователя, например:

{ 
 "query":
  "{
    user_account{
      get_user_account(user_id: 15) {
        id,
        login_name,
            password
      }
   }
 }"
}

запрос не должен быть допущен к выполнению.

Конечная точка GraphQL - это стандартная точка, созданная:

public UserAccountGraphType(IQueryBuilder<UserAccount> useraccountQueryBuilder, IMediator _mediator)
        {
            Name = "user_account";

            Field<UserAccountType>(
                "get_user_account",
                arguments: new QueryArguments(
                    new QueryArgument<IdGraphType> { Name = "user_id", Description = "The ID of the Author." }),
                resolve: context =>
                {
                    //A new Object is created with the current context
                    //and is send to the meditor/handler (exmp: GetUserAccountHandler)
                    var getUserAccount = new GetUserAccountQuery() {context = context};
                    return _mediator.Send(getUserAccount);
                }
            );



        }

Есть ли что-то, что я пропустил, что могло бы помочь мне в таких сценариях?

...