LogoLogo
search
  • Overview
  • Installation
  • Getting Started
  • keyboard_arrow_down

    Concepts


  • CLI Reference
  • Best Practices

  • function
    Serverless

    database
    Database & Storage
  • Overview
  • SQL Tables
  • File Storage
  • Task Queues
  • Key-Value Stores

  • lock
    Auth
  • Overview
  • Providers
  • User Management
  • Introduction to Groups

  • update
    Realtime

    receipt_long
    Logging

Introduction to Groups

$PLACEHOLDER_NAME$ unifies the concepts of permissions, organizations, distribution lists, and membership into a single concept: groups.
Groups can be extremely powerful. They can be used to model permissions for your own employees, external user accounts, B2B organizations, compliance, API keys, JWT access tokens, and much more. In this guide, we'll walk through the basics of groups.
In principle, they are simple. Users can be (direct) members of groups, and groups can also be members of other groups. They work transitively; if group engineeringInterns is a member of group engineers, then all members in engineeringInterns are indirect members of group engineers.

Defining groupslink

You can define a group declaratively:
const engineeringInternsGroup = app.defaultTree.defineGroup({
id: "engineeringInterns",
description: "Contains all current engineering interns",
version: 1,
});
const engineeringInternsGroup = app.defaultTree.defineGroup({
id: "engineeringInterns",
description: "Contains all current engineering interns",
version: 1,
});
In fact, users are actually nothing else than groups themselves. They are distinguished from other groups by belonging to the users group, as described below.
There is full CLI support for groups. For example, to see which groups a user (or another group) is a direct member of, you can list them using the CLI:
$ pn groups list --has-direct-member [USER_OR_GROUP_ID]
engineeringInterns
$ pn groups list --has-direct-member [USER_OR_GROUP_ID]
engineeringInterns

Permissionslink

Let's say we want to check whether an employee is allowed to commit code. Permissions like these are not a special concept in $PLACEHOLDER_NAME$. Instead, we just test group membership on a new group allowCodeCommits, like this:
const user = ...;
if (user.isMemberOf("allowCodeCommits")) {
// ...
}
const user = ...;
if (user.isMemberOf("allowCodeCommits")) {
// ...
}
If we now make our engineers group be a member of allowCodeCommits, all our engineers (and engineering interns) can commit code.
Per convention, in documentation and informal language, we call groups that are mostly used for permission management (such as allowCodeCommits) "permissions", and groups that are mostly used for organizing members (such as engineeringInterns) "roles". In many cases, users would be a member of roles, and roles would be a member of permissions or other roles. However, there is no technical difference between the two and it is important to understand that they are all just groups.
$ pn groups explain-access --user [USER_ID] --group allowCodeCommits
User [USER_ID] has access to allowCodeCommits because:
- They are a direct member of engineeringInterns
- hence they are an indirect member of engineers
- hence they are an indirect member of allowCodeCommits
$ pn groups explain-access --user [USER_ID] --group allowCodeCommits
User [USER_ID] has access to allowCodeCommits because:
- They are a direct member of engineeringInterns
- hence they are an indirect member of engineers
- hence they are an indirect member of allowCodeCommits

Userslink

Just like the difference between roles and permissions, there is nothing inherently different about a user compared to other groups.
However, in documentation and informal language, we use the terminology of "user" to refer to any direct member of the users group. This means that indirect members (such as access tokens or API keys) are not considered users, even if they are direct members of other users.
New members of this group are automatically created when a provider returns with successful authentication. Additionally, $PLACEHOLDER_NAME$ allows any group that is a direct member of the users group to have a user profile associated with it.

Access expirationlink

...

Computed groupslink

...

Treeslink

For security or compliance reasons, you may want to separate some permissions from others. Group trees are a great way to do this. Note that more complex security & compliance requirements are better served by security invariants.
If you don't create any trees, all your data is in the default tree. To create a new tree:
const webAppTree = app.defineTree({
id: "webApp",
description: "Contains all groups and permissions for the web app",
version: 1,
});

const webAppUsersGroup = webAppTree.defineGroup({
id: "webAppUsers",
description: "All users of the web app",
version: 1,
});

// The default authentication resource is on the default tree only.
// To get user authentication on the webApp tree, we need to
// define our own.
const webAppAuth = webAppTree.defineAuth({
id: "webAppAuth",
version: 1,
usersGroup: webAppUsersGroup,
});
const webAppTree = app.defineTree({
id: "webApp",
description: "Contains all groups and permissions for the web app",
version: 1,
});

const webAppUsersGroup = webAppTree.defineGroup({
id: "webAppUsers",
description: "All users of the web app",
version: 1,
});

// The default authentication resource is on the default tree only.
// To get user authentication on the webApp tree, we need to
// define our own.
const webAppAuth = webAppTree.defineAuth({
id: "webAppAuth",
version: 1,
usersGroup: webAppUsersGroup,
});
Please note that all trees are entirely separate. This yields strong compliance guarantees, but makes interop between trees impossible. If you are not sure whether you should use trees for your application, don't; organizing groups via membership with security invariants is enough for most cases, and you will know when creating total separation is necessary.

Security invariantslink

TODO I haven't looked at this in detail yet, the approach below won't work
Even inside a tree, it is possible to create strong guarantees with security invariants. Essentially, each invariant ensures that its predicate is always true; if a modification would make it false, the modification is rejected.
For example, if you want to make sure that no one with the engineeringInterns role can ever get access to your readInternEvaluations permission:
const engineeringInternsGroup = app.defaultTree.defineGroup(...);

// TODO fix this syntax

const readInternEvaluationsGroup = app.defaultTree.defineGroup({
id: "readInternEvaluations",
description: "Allows reading intern evaluations",
version: 1,
invariants: [
PN.Predicate.not(engineeringInternsGroup),
],
});
const engineeringInternsGroup = app.defaultTree.defineGroup(...);

// TODO fix this syntax

const readInternEvaluationsGroup = app.defaultTree.defineGroup({
id: "readInternEvaluations",
description: "Allows reading intern evaluations",
version: 1,
invariants: [
PN.Predicate.not(engineeringInternsGroup),
],
});
Please be aware that $PLACEHOLDER_NAME$ rejects any computed group that would allow falsifying a security invariant, even if the predicate is never actually true.

LogoLogo