Какова лучшая практика для реализации авторизации на основе ресурсов. В 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);
}
);
}
Есть ли что-то, что я пропустил, что могло бы помочь мне в таких сценариях?