How to prevent other Users from seeing logged in Users email

I have asked a similar question in here, but have started a new app and want to see what I would need to do to accomplish this, since the initial question is not exactly what I am trying to achieve now.

I have a User collection here is my schema

type User {
  email: String!
  profile: Profile!
}

type Profile {
  user: User!
  bio: String
  image: String
}

type Query {
  allUsers: [User!]
}

I have a session token from fauna from creating a User after logging in with my own auth. I have a role called user with a Membership to User collection. My understanding is this associates the User that was created and the token was given to that user in the DB.

Now I have a allUsers query I don’t think it is entirely necessary so I can change it to allProfiles and query the profiles.

Now if I give access in the role to the User Collection to Read and Delete, does that mean any User who has a session can read and delete any record in the User collection, or only the session they are in.

I do want them to read any profiles so that I don’t think should be a problem.

The second part would also be writing, creating and deleting for the profile are going to need a predicate. For example the recommended code is

Lambda("values", Equals(Identity(), Select(["data", "owner"], Var("values"))))

is the owner a value I need to modify the Profile type in schema like

type Profile {
  owner: User!
  bio: String
  image: String
}

Thanks for any help ahead of time

Update:

There is another thing I am struggling with. I do want to query a Users Profile using their ID. This works but I am concerned about introducing these predicates that are looking for the owner field which I am using user so could easily change that, but the predicate to me is looking for the owner or user field on the Profile document but even when I use the @relation in my schema like so

type User {
  email: String!
  profile: Profile! @relation(name: "user_profile")
}

type Profile {
  user: User! @relation(name: "user_profile")
  bio: String
  image: String
}

type Query {
  allProfile: [Profile!]
}

Looking in the DB the profile attribute is added to the User document so I don’t know how the predicate will work see below

You can see in the next screenshot there is no reference back to the User document so how does the predicate know that it is that User that is trying to say delete the profile

For example when I query the allProfile changed from allUsers A allUsers still exists as a possible query which is fine but I don’t want it accessible to the user role or when a User logs in or a User is created. But you can also see here that the user’s email is available in the allProfile query, now I don’t want that type of query to be possible.

query {
  allProfile{
    data{
      image
      bio
      user{
        email
     }
    }
  }
}

However I still want a relation between profile and user where I can run findUserById query and show their profile, so I need the relation where a user has a profile.

query ($id: ID!) {
    findUserByID(id: $id) {
      email
      profile {
        image
        bio
      }
    }
  }

I had thought if I removed the User declaration from the Profile this would then prevent the allProfile query from have access to the user which is what I want. Still not solving the availability of the allUsers query but we will leave that for now.

So the issue here is now when I go to do a mutation that creates a profile it no longer has the option to connect it to a user.

type Profile {
  #user: User! @relation(name: "user_profile")
  bio: String
  image: String
}

See how when I have a user_profile relation I can create a profile and connect to a user but without I cannot. This does make sense, but this leaves me at a loss as to how I prevent the Profile query being able to see the users email


Screen Shot 2022-05-07 at 11.29.43 AM

OK so it seems to be a rather simple solution. If I make a lambda with the predicate of

Lambda("ref", Equals(Identity(), Var("ref")))

In the users collection privileges, I can only find the user data if it is for the logged in user session, so when I run allProfile checking for users it returns cannot return non null. I would have expected a permission denied, but when I don’t check for the user in the allProfile it works. So I think this is a solution.

Error: Cannot return null for non-nullable type (line 7, column 7): user { ^

Does anyone know why it does not return Permission denied for this query

const QUERY = gql`
  query {
    allProfile {
      data {
        bio
        _id
        image
       # This does not work returns a cannot return null error, which is good with the privilege 
       # above, but wonder why it does not return permission denied
        user{
          email
        }
      }
    }
  }
`;

Finally I am still trying to figure how to solve Users only being able to create, write and delete their own profiles, ones that are connected to the User collection. I can do this if I give full permission. But there is no way to do the following because the data doesn’t actually exist on the Profile to check for the user.

This is the last thing I need to solve. Hopefully someone can help thanks

Lambda("values", Equals(Identity(), Select(["data", "user"], Var("values"))))

I’ve tried the following and I don’t think it works because I think identity() returns a reference and select just returns the string value I think. But I don’t know what else to do for this.

Lambda(
  "values",
  Equals(Identity(), Select(["data", "user", "connect"], Var("values")))
)
{"input": {"user": {"connect": "331021661688037449"},"bio": "123","image": "123"}}

I have also tried the following

Lambda(
  "values",
  Equals(Identity(), Index("user_profile_by_profile"), Select(["values", "user", "connect"], Var("values"))))
)

I get permission denied, I wish there was a way to test these predicates equality better than just permission denied, like seeing what data is being passed.

For your permission denied question, indexes represent sets, which can have zero or more items. For items that you do not have permission to access, the index behaves as if those items are not included in the set. That lets you discover items for which you do have access. If you don’t have access to any item, the result set contains no items.

The GraphQL API uses indexes wherever possible.

When there is no document, the identity check is not possible, so your role should grant that privilege. Otherwise, users cannot create profiles when one does not already exist. Or, when a user identity is created (using a key with the admin or server roles), be sure to create the profile document at that time, so that the identity check can always be performed.

For this expression:

Lambda(
  "values",
  Equals(Identity(), Index("user_profile_by_profile"), Select(["values", "user", "connect"], Var("values"))))
)

I believe that it written incorrectly. As written, it would turn true if the Identity() reference matches the Index("user_profile_by_profile") reference and matches the result of the Select. That is unlikely to ever return true for a couple of reasons:

  1. The first comparison, between the identity and the index, is unlikely to match unless the index itself is the identity.

  2. No Fauna document has a top-level values field, so the Select(["values", "user", "connect"] is an invalid path. Since there is no default value specified, the Select call can only fail, so the permissions check fails.

Debugging roles is quite challenging because there are only three possible results: access granted, access denied, or error. The intermediate steps to arrive at a result are not reported, as that information might be used to circumvent the restrictions in place.

It would be helpful if you could share your entire role definition. Then we could assess whether the role logic looks sound, and then setup scenarios to test the validity of the implementation.

So you are saying here to create the profile document immediately after the creation of the user document?

Here is my entire role definition. Now if the profile is created right away as I think you are suggesting. I still need to lock down the User attached to that profile to have write privileges only for themselves and not others as well as delete.

Thanks for taking the time to read through my questions.

{
  ref: Role("user"),
  ts: 1651952221670000,
  name: "user",
  privileges: [
    {
      resource: Collection("User"),
      actions: {
        read: Query(Lambda("ref", Equals(Identity(), Var("ref")))),
        write: false,
        create: false,
        delete: Query(Lambda("ref", Equals(Identity(), Var("ref")))),
        history_read: false,
        history_write: false,
        unrestricted_read: false
      }
    },
    {
      resource: Collection("Profile"),
      actions: {
        read: true,
        write: true,
        create: true,
        delete: true,
        history_read: false,
        history_write: false,
        unrestricted_read: false
      }
    },
    {
      resource: Index("allUsers"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    },
    {
      resource: Index("allProfile"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    },
    {
      resource: Index("user_profile_by_profile"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    }
  ],
  membership: [
    {
      resource: Collection("User")
    }
  ]
}