Listing Subjects
ListSubjects returns all subjects of a given type that have a specific relation on an object.
Basic Usage
// Find all users who can read a repository (with pagination)
userIDs, cursor, err := checker.ListSubjects(ctx,
authz.Repository("456"),
authz.RelCanRead,
"user",
melange.PageOptions{Limit: 100},
)
if err != nil {
return err
}
// userIDs = ["alice", "bob", "carol"]
// cursor = nil when no more pages, or a string to fetch the next pageParameters
| Parameter | Type | Description |
|---|---|---|
object_type | text | Type of the object (e.g., 'repository') |
object_id | text | ID of the object |
relation | text | The relation to check |
subject_type | text | Type of subjects to return |
p_limit | int | Maximum number of results per page (NULL = no limit) |
p_after | text | Cursor from previous page (NULL = start from beginning) |
Return Value
Returns a table with subject_id and next_cursor columns. The next_cursor value is repeated on every row for convenience - use the last row’s cursor to fetch the next page. Returns an empty result set if no subjects found (not an error).
'*') first, then alphabetically by subject_id. This ensures stable pagination while keeping wildcard entries grouped at the top.Pagination
Cursor-Based Pagination
Use cursor-based pagination to iterate through large result sets efficiently:
// Paginate through all users with access
var cursor *string
for {
ids, next, err := checker.ListSubjects(ctx,
authz.Repository("456"),
authz.RelCanRead,
"user",
melange.PageOptions{Limit: 100, After: cursor},
)
if err != nil {
return err
}
for _, id := range ids {
// Process each user ID
fmt.Println("Has access:", id)
}
if next == nil {
break // No more pages
}
cursor = next
}Fetching All Results
For convenience, use the ListSubjectsAll helper which automatically paginates through all results:
// Get all users who can read (auto-paginates internally)
allUserIDs, err := checker.ListSubjectsAll(ctx,
authz.Repository("456"),
authz.RelCanRead,
"user",
)
if err != nil {
return err
}
// allUserIDs contains all user IDs with accessListSubjectsAll loads all IDs into memory. For large datasets, prefer paginated queries with ListSubjects to control memory usage.Examples
Show Access List
Display who has access to a resource:
func GetAccessList(ctx context.Context, repo Repository) ([]AccessEntry, error) {
var entries []AccessEntry
// Get users with each permission level
permissions := []string{"owner", "admin", "can_write", "can_read"}
for _, perm := range permissions {
userIDs, err := checker.ListSubjectsAll(ctx, repo, perm, "user")
if err != nil {
return nil, err
}
for _, id := range userIDs {
entries = append(entries, AccessEntry{
UserID: id,
Permission: perm,
})
}
}
return entries, nil
}Check for Any Access
Verify at least one user has access:
func HasAnyViewers(ctx context.Context, doc Document) (bool, error) {
// Use Limit: 1 to efficiently check for any results
viewers, _, err := checker.ListSubjects(ctx, doc, "can_read", "user",
melange.PageOptions{Limit: 1})
if err != nil {
return false, err
}
return len(viewers) > 0, nil
}Notify All Users with Access
func NotifyCollaborators(ctx context.Context, repo Repository, message string) error {
// Get all users who can read (using ListSubjectsAll)
userIDs, err := checker.ListSubjectsAll(ctx, repo, "can_read", "user")
if err != nil {
return err
}
for _, userID := range userIDs {
if err := notificationService.Send(ctx, userID, message); err != nil {
log.Printf("Failed to notify user %s: %v", userID, err)
}
}
return nil
}Audit Access
type AccessAuditEntry struct {
ObjectType string
ObjectID string
UserID string
Permission string
Timestamp time.Time
}
func AuditRepositoryAccess(ctx context.Context, repo Repository) ([]AccessAuditEntry, error) {
var audit []AccessAuditEntry
permissions := []string{"owner", "admin", "can_write", "can_read"}
for _, perm := range permissions {
userIDs, err := checker.ListSubjectsAll(ctx, repo, perm, "user")
if err != nil {
return nil, err
}
for _, userID := range userIDs {
audit = append(audit, AccessAuditEntry{
ObjectType: "repository",
ObjectID: fmt.Sprint(repo.ID),
UserID: userID,
Permission: perm,
Timestamp: time.Now(),
})
}
}
return audit, nil
}Performance Characteristics
ListSubjects uses a recursive CTE similar to ListObjects:
| Tuple Count | Latency |
|---|---|
| 1K tuples | ~708us |
| 10K tuples | ~6.3ms |
| 100K tuples | ~42ms |
| 1M tuples | ~864ms |
ListSubjects is typically faster than ListObjects because it starts from a specific object rather than searching across all objects.
Decision Override Behavior
| Decision | Behavior |
|---|---|
DecisionUnset | Normal database query |
DecisionDeny | Returns empty slice (no subjects have access) |
DecisionAllow | Falls through to database query |
Note: DecisionAllow cannot enumerate “all” subjects, so it performs the normal query.
Caching
Like ListObjects, ListSubjects does not use the permission cache. Implement application-level caching if needed:
func (c *CachedChecker) ListSubjectsAll(ctx context.Context, object ObjectLike, relation RelationLike, subjectType ObjectType) ([]string, error) {
key := fmt.Sprintf("subjects:%s:%s:%s:%s",
object.FGAObject().Type,
object.FGAObject().ID,
relation.FGARelation(),
subjectType,
)
if cached, ok := c.cache.Get(key); ok {
return cached.([]string), nil
}
ids, err := c.Checker.ListSubjectsAll(ctx, object, relation, subjectType)
if err != nil {
return nil, err
}
c.cache.Add(key, ids)
return ids, nil
}Common Patterns
Permission Comparison
Compare who has what level of access:
type PermissionBreakdown struct {
Owners []string
Admins []string
Writers []string
Readers []string
}
func GetPermissionBreakdown(ctx context.Context, repo Repository) (*PermissionBreakdown, error) {
var (
owners, admins, writers, readers []string
ownerErr, adminErr, writerErr, readerErr error
)
var wg sync.WaitGroup
wg.Add(4)
go func() {
defer wg.Done()
owners, ownerErr = checker.ListSubjectsAll(ctx, repo, "owner", "user")
}()
go func() {
defer wg.Done()
admins, adminErr = checker.ListSubjectsAll(ctx, repo, "admin", "user")
}()
go func() {
defer wg.Done()
writers, writerErr = checker.ListSubjectsAll(ctx, repo, "can_write", "user")
}()
go func() {
defer wg.Done()
readers, readerErr = checker.ListSubjectsAll(ctx, repo, "can_read", "user")
}()
wg.Wait()
// Check errors...
return &PermissionBreakdown{
Owners: owners,
Admins: admins,
Writers: writers,
Readers: readers,
}, nil
}Team Members via Object
When teams are modeled as objects:
// Schema: type team
// relations
// define member: [user]
func GetTeamMembers(ctx context.Context, teamID string) ([]User, error) {
team := melange.Object{Type: "team", ID: teamID}
memberIDs, err := checker.ListSubjectsAll(ctx, team, "member", "user")
if err != nil {
return nil, err
}
return db.GetUsersByIDs(ctx, memberIDs)
}Transaction Consistency
Paginated queries across multiple calls can observe changes between pages. For consistency-critical flows, run paging inside a transaction with repeatable-read or snapshot semantics:
// For consistent pagination across pages, use a transaction
tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
if err != nil {
return err
}
defer tx.Rollback()
txChecker := melange.NewChecker(tx)
var allIDs []string
var cursor *string
for {
ids, next, err := txChecker.ListSubjects(ctx, repo, "can_read", "user",
melange.PageOptions{Limit: 100, After: cursor})
if err != nil {
return err
}
allIDs = append(allIDs, ids...)
if next == nil {
break
}
cursor = next
}
tx.Commit()See Also
- Listing Objects - Find what objects a subject can access
- Checking Permissions - Single permission checks