0

Consider a User type, with an email field that, for certain "anonymous" users, should only be accessible if the request was properly authorized. Other users are fine with having their email publicly displayed.

type User {
  name: String!
  "Anonymous users have the email only accessible to admins"
  email: String!
}

What are the pros and cons of these two approaches for handling that field in the email resolver for anonymous users?

  1. throw if the request is unauthorized. In that case,
    • email needs to be declared as nullable String instead of String!, or the entire query will error.
    • The client may want to match the errors to the data. Because non-anonymous users will have their email accessible without authorization, errors and data may have different numbers of elements, so this matching seems impossible, at least with apollo-server, which doesn't return anything in each errors element that would indicate to which user it belongs.
    • email will be misleadingly null for anonymous users, confusing the situation with that of the email never having been added in the first place. Remember that email needs to be String, not String!, so a null email is reasonable.
    • There is a clear errors array in the response, so this feels like the "proper" approach?
  2. return the email in a redacted form, e.g [NOT AUTHORIZED TO ACCESS THIS USER'S EMAIL].
    • This keeps the objects intact with clear errors for sensitive emails, instead of misleading "null" emails.
    • email can stay non-nullable
    • No need to try to match errors with data.
    • There is no explicit errors array in the response, so this feels like a hack.

Note that for arrays, returning a REDACTED form is not an option, because the query may ask for a field within the array (e.g. { anonUsers { email } }). The only option there is to return [] (directly or by throwing).

Am I missing anything? Is there prior work on this topic? How can I make a decision?

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
  • Would union types be a good use case? You can have a normal `User` type and a `AnonymousUser` type, and maybe have some field that separates them like `is_anonymous`. So maybe `AnonymousUser` does not have an email field, while `User` has it. – A. L May 03 '20 at 11:16
  • @A.Lau: that skirts the problem. Let's say all users are anonymous, and only authorized requests can see the `email`; then the point about matching `errors` to `data` is moot, but the others remain. – Dan Dascalescu May 03 '20 at 14:05
  • maybe for `AnonymousUser` `email` can be an object so then you can show whatever message/error codes you want. – A. L May 03 '20 at 23:29

1 Answers1

1

I've done something similar recently, what seemed to work best was to create an interface for the base user type.

interface UserInformation {
  id: ID!
  userName: String
  firstName: String!
  lastName: String
  avatarImage: String
  city: String
  ...
}

Then you'd have two separate implementations of it:

type UserPublic implements UserInformation {
  id: ID!
  userName: String
  firstName: String!
  lastName: String
  avatarImage: String
  ...
}
type UserPrivate implements UserInformation {
  id: ID!
  userName: String
  firstName: String!
  lastName: String
  avatarImage: String
  friends: [UserInformation!]
  pendingFriends: [UserInformation!]
  phone: String
  email: String
  birthday: DateTime
  ...
}

All your other queries and types use the UserInformation base interface type when exposing Users. Then your graphql server simply returns either UserPublic or UserPrivate depending on what type of access the user has.

On the client side you query it like this:

query SomeQuery {
  getSomeData {
    user {
      id
      userName
      firstName
      lastName
      avatarImage
      ...on UserPrivate {
        email
        phone
      }
      ...

You can then check client-side whether the returned type of a field (__typename) was UserPublic or UserPrivate and act accordingly.

andreialecu
  • 3,639
  • 3
  • 28
  • 36