What is the proper way to define fields that may not be accessible to all users.
For example, a general user can query the users and find out another users handle, but only admin users can find out their email address. The user type defines it as a field but it may not be accessible. Should there be a separate type for what a general user can see? How would you define it?
Sorry if that isn't that clear I just don't possess the vocabulary.
Edit: Caution: Graphql documentation disagrees with this approach. Use with caution. Wherever you need a private field you must include the appropriate middlewares.
Use absinthe middleware.
Here is some code how to do it. In this example the authenticated user can see the email addresses. The anonymous user can't. You can adjust the logic to require whatever permissions you want.
defmodule MySite.Middleware.RequireAuthenticated do
@behaviour Absinthe.Middleware
@moduledoc """
Middleware to require authenticated user
"""
def call(resolution, config) do
case resolution.context do
%{current_user: _} ->
resolution
_ ->
Absinthe.Resolution.put_result(resolution, {:error, "unauthenticated"})
end
end
end
and then you define your object:
object :user do
field :id, :id
field :username, :string
field :email, :string do
middleware MySite.Middleware.RequireAuthenticated
middleware Absinthe.Middleware.MapGet, :email
end
end
So our field email is protected by the RequireAuthenticated middleware. But according to the link above
One use of middleware/3 is setting the default middleware on a field, replacing the default_resolver macro.
This happens also by using the middleware/2 macro on the field. This is why we need to also add
middleware Absinthe.Middleware.MapGet, :email
to the list of middlewares on the field.
Finally when we perform a query
query {
user(id: 1){
username
email
id
}
}
We get the response with the open fields filled and the protected fields nullified
{
"errors": [
{
"message": "In field \"email\": unauthenticated",
"locations": [
{
"line": 4,
"column": 0
}
]
}
],
"data": {
"user": {
"username": "MyAwesomeUsername",
"id": "1",
"email": null
}
}
}
You can also use the middleware/3 callback so your object don't get too verbose
def middleware(middleware, %{identifier: :email} = field, _object) do
[MySite.Middleware.RequireAuthenticated] ++
[{Absinthe.Middleware.MapGet, :email}] ++
middleware
end
With a bit of creative use of the __using__/1 callback you can get a bunch of such functions out of your main schema file.
@voger gave an awesome answer and I just wanted to post a macro sample based on the accepted question. I'm currently using it to authenticate every field in my schema.
Here's a macro definition:
defmodule MyApp.Notation do
defmacro protected_field(field, type, viewers, opts \\ []) do
{ast, other_opts} =
case Keyword.split(opts, [:do]) do
{[do: ast], other_opts} ->
{ast, other_opts}
{_, other_opts} ->
{[], other_opts}
end
auth_middleware =
if viewers !== :public do
quote do
middleware(MyApp.Middleware.ProtectedField, unquote(viewers))
end
end
quote do
field(unquote(field), unquote(type), unquote(other_opts)) do
unquote(auth_middleware)
middleware(Absinthe.Middleware.MapGet, unquote(field))
unquote(ast)
end
end
end
end
And then inside of your type definitions, you can do this.
import MyApp.Notation
# ...
object :an_object do
protected_field(:description, :string, [User, Admin]) do
middleware(OtherMiddleware)
resolve(fn _, _, _ ->
# Custom Resolve
end)
end
protected_field(:name, :stirng, :public, resolve: &custom_resolve/2)
end
Explanation:
It adds an argument that I call viewers that I just forward to my middleware to check if the user type is correct. In this scenario, I actually have different models that I call Admin and User that I can check the current user against. This is just an example of one way to do it, so your solution might be different. I have a special case for :public fields that are just a passthrough.
This is great because I can inject middleware with the extra argument and forward everything else to the original field definition.
I hope this helps add to the answer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With