UDF has Admin role, Visitor role has UDF privilege, but requires other collection privileges to work

Hello!

Pardon if there is a duplicate of this topic, I wasn’t able to find it with my terms.

I’m wondering if I’m missing something with the ABAC model.

Given a UDF with the role Admin and a Visitor role with privileges to call that UDF, I’ve found that the query does not work unless I enable reads on the collections read in the UDF.

Here are the relevant FQL snippets.

UDF

{
  name: "songs_for_tag",
  role: "admin",
  body: Query(
    Lambda(
      ["tagID", "size", "after", "before"],
      Let(
        {
          match: Match(
            Index("song_tags_by_tag"),
            Ref(Collection("Tag"), Var("tagID"))
          ),
          page: If(
            Equals(Var("before"), null),
            If(
              Equals(Var("after"), null),
              Paginate(Var("match"), { size: Var("size") }),
              Paginate(Var("match"), { after: Var("after"), size: Var("size") })
            ),
            Paginate(Var("match"), { before: Var("before"), size: Var("size") })
          )
        },
        Map(Var("page"), Lambda("ref", Get(Var("ref"))))
      )
    )
  )
}

And the Visitor role:

{
  ref: Role("Visitor"),
  ts: 1603724952120000,
  name: "Visitor",
  privileges: [
    {
      resource: Index("allTags"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    },
    {
      resource: Collection("Tag"),
      actions: {
        read: true,
        write: false,
        create: false,
        delete: false,
        history_read: false,
        history_write: false,
        unrestricted_read: false
      }
    },
    {
      resource: Ref(Ref("functions"), "songs_for_tag"),
      actions: {
        call: true
      }
    },
    {
      resource: Collection("Album"),
      actions: {
        read: true,
        write: false,
        create: false,
        delete: false,
        history_read: false,
        history_write: false,
        unrestricted_read: false
      }
    }
  ],
  membership: []
}

I expected to be able to have a Visitor that looked more like:

{
  ref: Role("Visitor"),
  ts: 1603724952120000,
  name: "Visitor",
  privileges: [
    {
      resource: Ref(Ref("functions"), "songs_for_tag"),
      actions: {
        call: true
      }
    }
  ],
  membership: []
}

Where the Admin role on the UDF would have access to Tag, Song, and Album documents.

Thanks!

Hi @tmikeschu,

As far as I can see, in the function, you are using Get() on one or more documents. In that case you need read privilege on that collection.

Luigi

@Luigi_Servini got it, thanks! Is there an alternative to Get that would allow an admin role to access documents without specific privileges?

Well, Get() reads the document by passing its Ref. If you don’t have read access, you cannot read documents…

I notice you created the UDF with role:

role: "admin",

It should be:

role: Role('admin'),

may you try that way?

Luigi

@Luigi_Servini interesting! The FQL snippet is just copied from the dashboard, I used the GUI to assign the role.

Is this a possible bug in the GUI, then?

Hi @tmikeschu,

Sorry, it is correct, for the built-in role you just pass (and get) the string.
Do you mind sharing with me the index definition and couple of documents from each collection?

Luigi

Index

{
  name: "song_tags_by_tag",
  unique: false,
  serialized: true,
  source: "song_tags",
  terms: [
    {
      field: ["data", "tagID"]
    }
  ],
  values: [
    {
      field: ["data", "songID"]
    }
  ]
}

Tag document

{
  "ref": Ref(Collection("Tag"), "277421861124440576"),
  "ts": 1600828954245000,
  "data": {
    "name": "😔"
  }
}

song_tags collection

{
  "ref": Ref(Collection("song_tags"), "279935773698949636"),
  "ts": 1603226407790000,
  "data": {
    "songID": Ref(Collection("Song"), "279935773551100420"),
    "tagID": Ref(Collection("Tag"), "277422332028387840")
  }
}

Song document

{
  "ref": Ref(Collection("Song"), "277422222462681600"),
  "ts": 1603228808490000,
  "data": {
    "title": "Sanguine",
    "album": Ref(Collection("Album"), "277421988903911954"),
    "lyrics": "Make me sanguine\nHelp me genuinely\nKill the doubt that strangles myself worth\nPaint the picture that I swore I heard\n\nSpiritless and mean\nGhost that comes between\nI will keep my wits about myself\nDisregard directions sent from hell\n\nWhen it brings the gleam\nLonger lasting than me\nOh my love but we are bound to die\nMy heart is broke but you won't hear me cry\nOh my love but we are bound to die\nMy heart is broke but you won't see me"
  }
}

It might also be worth mentioning that the song tags collection is created via graphql schema directives.

type Song {
  title: String!
  album: Album!
  lyrics: String!
  tags: [Tag!] @relation
}

type Album {
  title: String!
  songs: [Song!] @relation
}

type Tag {
  name: String! @unique
  songs: [Song!]! @relation
}

type Query {
  allTags: [Tag!]!
  allAlbums: [Album!]!
  allSongs: [Song!]!
  songsForTag(tagID: ID!): [Song!]!
    @resolver(name: "songs_for_tag", paginated: true)
}