Guest roles in login

I have the following function:

Update(Function("login"), {
  role: "admin",
  body: Query(
    Lambda(
      ["data"],
      Let(
        {
          login: Login(
            Match(
              Index("unique_User_email"),
              Select(["email"], Var("data"))
            ),
            {
              password: Select(["password"], Var("data"))
            }
          ),
          userRef: Select(
            ['instance'],
            Var('login')
          ),
          deviceToken: Select(["deviceToken"], Var("data"))
        },
        Do(
          If(
            Not(
              Exists(
                Match(
                  Index('deviceTokensByToken'),
                  Var('deviceToken')
                )
              )
            ),
            Create(
              'deviceTokens',
              {
                data: {
                  token: Var('deviceToken'),
                  user: Var('userRef')
                }
              }
            ),
            null
          ),
          {
            secret: Select(
              ['secret'],
              Var('login')
            ),
            user: Var('userRef')
          }
        )
      )
    )
  )
})

and the following role:

CreateRole({
  name: "guest",
  membership: {
    resource: Collection("users")
  },
  privileges: [
    {
      resource: Function("login"),
      actions: {
        call: true
      }
    }
  ]
})

As you can see, what I’m doing is when the user logs in, I return the secret and the user data, however, this will not work because during login, the user is using guest role and as you can see above, the said role doesn’t have access to user collection. I’m trying to avoid having to call 2 different mutations/queries for the login – which I currently do (call login mutation then call getUserData query which will return the data of the currently loggedin user).

Any advice to achieve this? Thanks.

Hi @aprilmintacpineda,

I’m not sure you’ll be able to get away from running two queries to do this. As you observed, when the user first accesses the app, they’re not logged in and hence are using a guest role. So first they have to be authenticated before getting access to their data in the user collection. Once they have their token they can take actions with their user privileges.

Unless I’m misunderstanding your goal here, I don’t think this can be collapsed much more than you’ve already done.

Cory

Isn’t there anyway that I can “UPGRADE” the user’s current identity AFTER the login was successful? Essentially telling fauna “Okay, this user has successfully logged in, from this point on, use this new logged in identity rather than the previous identity which is the guest identity”.

Also, I’m wondering why the Create on deviceTokens succeeded rather than failed, because my assumption is that by default all collections are closed unless you explicitly open them up by defining them on the CreateRole (I learned from docs What is attribute-based access control (ABAC)? | Fauna Documentation). Notice that when the Create on deviceTokens collection happened, the user is actually still using the guest identity which doesn’t have any access rights to deviceTokens collection.

Hi @aprilmintacpineda,

One thing that was overlooked before is you’re setting the UDF to have the admin role explicitly in the body of the UDF:

    role: "admin",

As such, anything calling that UDF is going to do so with admin rights. So the guest role gives access to the UDF, then everything that happens within the UDF itself runs as though the user has admin access.

You’d be better off splitting the function into two (or more) UDFs to make sure you have the granular control you’re looking for.

Cory

Hi @aprilmintacpineda,

Just to close the loop on this, we corresponded in DMs to come to the solution. It involved changing the UDF to only return the secret but not the userRef:

Update(Function("login"), {
  role: "admin",
  body: Query(
    Lambda(
      ["data"],
      Let(
        {
          login: Login(
            Match(
              Index("unique_User_email"),
              Select(["email"], Var("data"))
            ),
            {
              password: Select(["password"], Var("data"))
            }
          ),
          deviceToken: Select(["deviceToken"], Var("data"))
        },
        Do(
          If(
            Not(
              Exists(
                Match(
                  Index('deviceTokensByToken'),
                  Var('deviceToken')
                )
              )
            ),
            Create(
              'deviceTokens',
              {
                data: {
                  token: Var('deviceToken'),
                  user: Select(
                    ['instance'],
                    Var('login')
                  )
                }
              }
            ),
            null
          ),
          {
            secret: Select(
              ['secret'],
              Var('login')
            )
          }
        )
      )
    )
  )
})

And also modify your GraphQL schema so that the login response type only expected the secret, and not the user object. The final step would be modifying your app to use the secret that’s being returned as the user’s authorization header.

Please confirm that this is now working for you or if you run into any other issues.

Thanks,
Cory

That was always working for me if you read above, what I wanted to achieve here is to return both the secret and the user data in one mutation.

Right, but that’s not possible. I’ll paste the response I sent in DM in case you missed it but it all comes down to how the authorization layer is interacting with the GraphQL API.

So, what happens is:

  1. The login happens using the custom resolver “login”.
  2. The login() function is called using a set role, “admin”, and it returns a secret and an instance (in this case an instance of the User ref that matched in the login) to the GraphQL layer. Because it has that explicit “admin” role, everything works as expected.
  3. The GraphQL layer takes these two items and applies them to the LoginResponse type.
  4. The LoginResponse type is expecting two items: a string with the secret, and a User type.
  5. Here’s where it breaks down: LoginResponse tries to look up the User document based on the ref that was sent; it does so by issuing a Get() against that User ref. But because this is outside of the scope of the login UDF, it reverts to the permissions of the “guest” role, which is what was originally used when this whole thing started. The “guest” role lacks permission to look up users, so this Get() fails.

You could also modify the guest role to allow it read authorization on the User type/collection, but I’m not sure if that would violate any security policies you have in place, so may not be an effective option. But short of that you’ll have to use two queries to get all of the data you need.

1 Like