Developing Geonotes — Tightening the access policy — Ep. 9

Developing Geonotes — Tightening the access policy — Ep. 9

Access to the most common data should be fast but most importantly secure. Let's rework the security rules.

Up to this point, access to the notes around the user was performed with a simple query to the Hasura backend. The user provided their latitude and longitude, and a query radius.

☁️ Using Cloud Functions

As we discussed in previous episodes, my Hasura instance is able to perform "Actions" by calling specific cloud functions.

This can be used to our advantage to execute some custom filtering logic before returning the set of notes.

To do so, I defined a custom query action to which the user simply passes their current latitude and longitude: get_notes.

get_notes action

Unfortunately, as of writing this, only a simple object or an array of simple objects can be returned from an Action. I would have preferred to return a nested object with some more metadata about the result (e.g. the radius that was queried, a suggested minimum distance to update the current query...)

The feature request can be found here: #4796, and I really hope they'll be able to implement this soon.

This is the current implementation for the action: Source

const getNotesActionHandler: Action = async (_, params, sendOutput) => {
  const { notes } = await sdk.Notes({
    latitude: params.latitude,
    longitude: params.longitude,
    distance: LocationConstants.maximumNoteDistance,
  });

  return sendOutput(
    notes.map((note) => ({
      id: note.id,
      latitude: note.location.coordinates[1],
      longitude: note.location.coordinates[0],
      content: note.content,
    }))
  );
};

This is pretty much the same code that was being executed on the client side, but with a few benefits:

  • I can change the query radius server-side without needing a new version release for the apps
  • I'll be able to add custom filtering logic as needed in the future
  • I can hide the complexity of the PostGIS data types so that the client can directly receive the coordinates

The most significant change, however, is the one to the function that receives a single note: since the modal had to show not only the content of the note, but the username of the author and the number of views, I had to grant the user permissions to read tables that they should not have access to.

So I implemented another action: get_note. Source

const getNoteActionHandler: Action = async (userId, params, sendOutput, sendError) => {
  const { note } = await sdk.Note({ id: params.id });

  if (!note) {
    return sendError({ code: "note-not-found", message: "The requested note could not be found." });
  }

  const user = await admin.auth().getUser(userId);
  const isAnonymous = user.providerData.length === 0;

  if (!isAnonymous) {
    // Keep track of another user viewing the selected note.
    // This uses an upsert to ensure that the view is only registered once.
    await sdk.RegisterView({ note_id: params.id, user_id: userId });
  }

  return sendOutput({
    id: params.id,
    content: note.content,
    created_at: note.created_at,
    username: note.user.username,
    view_count: note.views_aggregate.aggregate?.count ?? 0,
  });
};

This way, only the backend role can read data about the user (to extract the username) and the number of views the note has (to aggregate the count).

As a result, I was able to tighten the security rules to only allow the user to directly read just their notes (even resulting in a simpler MyNotes query since it does not need a filter anymore). Data about users is now completely invisible except for the data about the current login user.

🚧 Next steps

With these new security rules in place, I feel like I can work on some more features like a little statistics section to see how many times your notes have been found around the world.

🎙 How to follow the project

I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.

If you'd like to have even more real-time updates you can