Help us shape the future of Go! Take the 2026 Go Developer Survey

permio

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

permio-go

The Go SDK for perms.io, a permissions (RBAC) service.

Package permio wraps the generated gRPC clients with API key auth, per-attempt timeouts, retries for idempotent reads, typed errors and helpers for checks, grants, groups and paginated listings. The generated clients ship in the same module and stay reachable as an escape hatch.

Install

go get github.com/PrivateJAR/permio-go@latest

Requires Go 1.24 or later.

Quick start

client, err := permio.New(permio.Options{APIKey: os.Getenv("PERMIO_API_KEY"), Project: "my-app"})
if err != nil { log.Fatal(err) }
defer client.Close()

resource, err := permio.Resource("docs", docID) // "/docs/<encoded docID>"
if err != nil { log.Fatal(err) }
ok, err := client.Check(ctx, userID, "docs.read", resource)

A denial is false with a nil error. An error means the check could not be answered (bad key, network, ...); treat it as "not allowed".

Configuration

client, err := permio.New(permio.Options{
	APIKey:   "perms_...",           // default: $PERMIO_API_KEY
	Endpoint: permio.DevEndpoint,    // default: permio.DefaultEndpoint (api.perms.io:443)
	Project:  "my-app",              // default project_name
	Timeout:  5 * time.Second,       // per attempt; default 10s, negative disables
	Retry:    permio.RetryOptions{MaxRetries: 3}, // default 2, negative disables
})
  • The API key is checked locally (format and checksum) before any request, so a mistyped key fails in New with a config error. Only the API can tell whether a key exists: use client.WhoAmI(ctx) to verify it at startup and to learn its project and access level (check or full).
  • Endpoint takes host:port or a URL (https://dev-api.perms.io). Plaintext (Insecure: true or an http:// URL) is only allowed to localhost, *.localhost and loopback addresses, since the API key would otherwise cross the network in clear; anything else fails in New.
  • client.WithProject("other") returns a client for another project that shares the connection.
  • Set an overall deadline (across retries) with the context.
  • Options.Conn takes any grpc.ClientConnInterface instead of dialling, e.g. a bufconn connection in tests; auth, timeouts, retries and errors still apply.
Dev vs prod
Constant Host
permio.DefaultEndpoint api.perms.io:443 (production)
permio.DevEndpoint dev-api.perms.io:443 (development)

Checks

client.Check(ctx, principal, "docs.read", resource)                     // one permission
client.CheckAll(ctx, principal, []string{"docs.read", "docs.write"}, r) // every permission
client.CheckAny(ctx, principal, []string{"docs.read", "docs.admin"}, r) // at least one
client.CheckDetailed(ctx, permio.CheckRequest{...})                     // several resources, with what is missing
client.Assert(ctx, principal, "docs.read", resource)                    // error of kind KindDenied when not allowed

allowed, err := client.BatchCheck(ctx, principal, []string{"docs.read"}, uris) // map[uri]bool, one call per 100

Build resource URIs with permio.Resource("projects", id, "docs", docID), which percent-encodes each segment so IDs containing / stay one segment. Use the same helper when granting and when checking. It returns an error of kind KindValidation for no segments (never "/", which would cover everything) or an empty segment; permio.MustResource panics instead, for literals.

Filtering lists
access, err := client.ListAccess(ctx, userID, "docs.read", "/docs")
// access.CoversAll       -> no filter needed
// access.Prefixes()      -> path prefixes (match p or p + "/...")
// access.RegexpSource()  -> one anchored regex for SQL "~" / MongoDB $regex
// access.Allows(uri)     -> test a single URI

who, err := client.WhoHasAccess(ctx, "/docs/42", &permio.WhoHasAccessOptions{Permission: "docs.read"})

Granting and revoking

err := client.Grant(ctx, permio.GrantRequest{
	Principal:   "user-123",
	Resource:    "/docs",               // also covers everything below it
	Permissions: []string{"docs.read"},
	Roles:       []string{roleID},      // role IDs, see ListRoles
})
err = client.Revoke(ctx, permio.GrantRequest{...}) // exactly that URI; revoking twice is harmless

Grants and revokes need a full-access API key. They are sent once and never retried automatically (see Retries).

Permission and role CRUD: CreatePermission, GetPermission, UpdatePermission, DeletePermission, CreateRole, GetRole, UpdateRole, DeleteRole, GetGrant.

Groups

g := client.Groups()
g.Create(ctx, "editors", "Document editors")
g.AddMembers(ctx, "editors", []string{"user-1", "user-2"}) // at most 100 per call
client.Grant(ctx, permio.GrantRequest{Principal: permio.GroupPrincipal("editors"), Resource: "/docs", Permissions: []string{"docs.write"}})
g.ListMembers(ctx, "editors", nil)
g.ListForPrincipal(ctx, "user-1", nil)
g.RemoveMembers(ctx, "editors", []string{"user-2"})
g.Delete(ctx, "editors") // also removes its memberships and grants

Group methods take a slug (editors) or a principal ID (group:editors). Groups cannot contain groups.

Listing

List methods return iter.Seq2[T, error] iterators that follow pagination lazily; breaking out of the loop stops fetching.

for role, err := range client.ListRoles(ctx, nil) {
	if err != nil { return err }
	fmt.Println(role.ID, role.Name)
}
perms, err := permio.Collect(client.ListPermissions(ctx, &permio.ListOptions{Search: "docs."}))

Available: ListPermissions, ListRoles, ListPrincipals, ListAssignments(principal) (grants), Groups().List, Groups().ListMembers, Groups().ListForPrincipal.

Errors

Every failure is a *permio.Error with a Kind, the gRPC Code and the message. Match kinds with errors.Is, read details with errors.As:

var perr *permio.Error
switch {
case errors.Is(err, permio.ErrAuth):        // key unknown or revoked
case errors.As(err, &perr) && perr.Kind == permio.KindRateLimited:
	time.Sleep(perr.RetryAfter)
}
Kind / sentinel When
KindConfig / ErrConfig Malformed API key, bad endpoint, no project
KindAuth / ErrAuth Unauthenticated: key missing, unknown or revoked
KindPermission / ErrPermission PermissionDenied: the key may not make this call (Missing lists what it lacks)
KindValidation / ErrValidation InvalidArgument, or the SDK's own input checks
KindNotFound / ErrNotFound NotFound
KindAlreadyExists / ErrAlreadyExists AlreadyExists
KindUnavailable / ErrUnavailable Unavailable: the API could not be reached
KindTimeout / ErrTimeout DeadlineExceeded, per attempt or the context's
KindRateLimited / ErrRateLimited ResourceExhausted: the organisation's rate limit is used up; RetryAfter is the server's delay
KindDenied / ErrDenied Returned by Assert/AssertAll when the principal lacks the permissions
KindOther Anything else, including cancellation

status.Code(err) also works on these errors.

Retries

Idempotent reads (Check, BatchCheck, WhoAmI, Get*, List*) are retried up to 2 times on Unavailable and DeadlineExceeded, with jittered exponential backoff (100ms doubling, capped at 2s). A rate-limited read (ResourceExhausted) is retried after the server's requested delay (google.rpc.RetryInfo, else a retry-after header) when that delay is within RetryOptions.MaxDelay; otherwise it fails with KindRateLimited and RetryAfter set. ResourceExhausted without a delay is not retried. A retry-after on Unavailable is used as the minimum backoff.

Writes (Create, Update, Delete, Grant, Revoke, group membership) are never retried. Grant and revoke converge on the server, so they are safe to repeat by hand when you know nobody else is changing that grant. permio.IsIdempotent reports whether a method is retried.

Raw clients

client.Permissions()   // permissionsv1.PermissionsServiceClient
client.Organisations() // organisationv1.OrganisationServiceClient
client.Conn()          // grpc.ClientConnInterface, for other generated clients

These carry the same auth, timeouts, retries and typed errors. Set ProjectName on requests yourself. Generated packages:

Import path Contents
github.com/PrivateJAR/permio-go/proto/permissions/v1 PermissionsService client, permission/role/check/group messages
github.com/PrivateJAR/permio-go/proto/organisation/v1 OrganisationService client, organisation/project/API key messages
github.com/PrivateJAR/permio-go/proto/pagination/v1 Pagination request/response messages

Development

go vet ./...
go test -race ./...

Unit tests run against an in-process gRPC server (bufconn). The integration test TestLive runs only when PERMIO_API_KEY is set:

PERMIO_API_KEY=perms_... go test -count=1 -run TestLive -v .

It talks to the dev API by default (PERMIO_BASE_URL overrides it) and refuses production, because it writes. The key needs full access to its project (PERMIO_PROJECT, default: the key's own project). It uses fixed sdk-it-go-* IDs and removes them before and after the run. In CI the integration job runs it when the PERMIO_DEV_API_KEY secret exists.

The proto/ directory is generated from PrivateJAR/proto by its release workflow, which opens a regen/<tag> pull request here that replaces only proto/. Do not edit it by hand.

Releasing

Go needs no publish step: a version exists once its v* tag is pushed, and the Go module proxy fetches it on first request. The Release workflow runs the tests on a pushed tag and asks proxy.golang.org for the version so it is cached and shows on pkg.go.dev straight away. Tags are pushed by hand, usually after merging a regen/<tag> pull request.

Licence

Apache-2.0. See LICENSE.

Documentation

Overview

Package permio is the Go SDK for perms.io, a permissions (RBAC) service.

A Client wraps the generated gRPC clients with API key auth, per-attempt timeouts, retries for idempotent reads, typed errors (Error) and helpers for checks, grants, groups and paginated listings:

client, err := permio.New(permio.Options{APIKey: os.Getenv("PERMIO_API_KEY"), Project: "my-app"})
if err != nil { ... }
defer client.Close()
resource, err := permio.Resource("docs", docID)
if err != nil { ... }
ok, err := client.Check(ctx, userID, "docs.read", resource)

The generated clients stay reachable through Client.Permissions and Client.Organisations, with the same auth, timeouts, retries and errors.

Example
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	permio "github.com/PrivateJAR/permio-go"
)

func main() {
	client, err := permio.New(permio.Options{APIKey: os.Getenv("PERMIO_API_KEY"), Project: "my-app"})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ok, err := client.Check(context.Background(), "user-123", "docs.read", permio.MustResource("docs", "42"))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("allowed:", ok)
}

Index

Examples

Constants

View Source
const (
	ScopeSubtree = "subtree"
	ScopePattern = "pattern"
	ScopeExact   = "exact"
	ScopeUnknown = "unknown"
)

Scope kinds.

View Source
const (
	// DefaultEndpoint is the production API.
	DefaultEndpoint = "api.perms.io:443"
	// DevEndpoint is the development API.
	DevEndpoint = "dev-api.perms.io:443"
	// DefaultTimeout is the default per-attempt timeout.
	DefaultTimeout = 10 * time.Second
	// EnvAPIKey is read when Options.APIKey is empty.
	EnvAPIKey = "PERMIO_API_KEY"
)
View Source
const (

	// APIKeyLength is the length of an opaque "perms_" API key.
	APIKeyLength = len(apiKeyPrefix) + apiKeyRandomLength + apiKeyChecksumLength
)

API key format, mirroring the server: "perms_" + 32 random base62 characters + 6 base62 characters encoding the CRC32 (IEEE) of those 32, 44 in total.

View Source
const GroupPrefix = "group:"

GroupPrefix starts the principal ID of every group.

View Source
const MaxBatch = 100

MaxBatch is the most resources or permissions one Check or BatchCheck call accepts, and the most principals Groups.AddMembers and Groups.RemoveMembers accept.

View Source
const MaxPageSize = 100

MaxPageSize is the largest page the API accepts (PaginationRequest.limit is 1..100).

View Source
const MaxResourceURILength = 2048

MaxResourceURILength is the longest resource URI the API accepts, in bytes.

View Source
const WildcardSegment = "_"

WildcardSegment is the "_" segment: in a stored grant URI or a pattern scope it matches exactly one segment.

Variables

View Source
var (
	ErrConfig        error = KindConfig
	ErrAuth          error = KindAuth
	ErrPermission    error = KindPermission
	ErrValidation    error = KindValidation
	ErrNotFound      error = KindNotFound
	ErrAlreadyExists error = KindAlreadyExists
	ErrUnavailable   error = KindUnavailable
	ErrTimeout       error = KindTimeout
	ErrRateLimited   error = KindRateLimited
	ErrDenied        error = KindDenied
)

Sentinels for errors.Is. Each is the matching Kind.

Functions

func Collect

func Collect[T any](seq iter.Seq2[T, error]) ([]T, error)

Collect gathers every item of a listing into a slice, stopping at the first error:

roles, err := permio.Collect(client.ListRoles(ctx, nil))

func EncodeSegment

func EncodeSegment(segment string) string

EncodeSegment percent-encodes one resource URI segment. Only A-Z, a-z, 0-9 and "-_.~" are left as they are (like JavaScript's encodeURIComponent, but also encoding "!'()*", which the API rejects in segments).

func GroupPrincipal

func GroupPrincipal(slug string) string

GroupPrincipal returns the principal ID of the group with this slug, "group:<slug>". Grant to it, or check it, like any other principal; every member of the group holds what the group is granted.

func IsIdempotent

func IsIdempotent(fullMethod string) bool

IsIdempotent reports whether the SDK retries the RPC with this full gRPC method name ("/package.Service/Method").

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err is a NotFound error.

func MustResource

func MustResource(segments ...string) string

MustResource is like Resource but panics if the URI is invalid. Use it for segments known to be non-empty, such as literals.

func Resource

func Resource(segments ...string) (string, error)

Resource builds a resource URI from raw segments, percent-encoding each one so that IDs containing "/", spaces or other reserved characters stay a single segment:

uri, err := permio.Resource("projects", projectID, "docs", "a/b") // "/projects/p1/docs/a%2Fb"

Build URIs the same way when granting and when checking, or they will not match. It fails with KindValidation when given no segments (rather than returning "/", which would cover every resource in a grant), an empty segment, or segments that make the URI too long.

func ValidateGroupName

func ValidateGroupName(slug string) error

ValidateGroupName checks a group slug: 1-64 lower-case letters, digits and hyphens. Errors are of kind KindValidation.

func ValidateResourceURI

func ValidateResourceURI(uri string) error

ValidateResourceURI checks the basic shape the API accepts: a leading "/", no empty segments (so no "//" or trailing "/"), no whitespace or control characters, no regex metacharacters in segments, at most 2048 bytes. Errors are of kind KindValidation.

Types

type APIKeyAccess

type APIKeyAccess string

APIKeyAccess is an API key's access level on its project.

const (
	// APIKeyAccessCheck: checks and reads only.
	APIKeyAccessCheck APIKeyAccess = "check"
	// APIKeyAccessFull: also writes, grant and revoke.
	APIKeyAccessFull APIKeyAccess = "full"
	// APIKeyAccessUnknown: the key's rights were changed by hand.
	APIKeyAccessUnknown APIKeyAccess = "unknown"
)

type APIKeyKind

type APIKeyKind string

APIKeyKind says which form an API key has.

const (
	// APIKeyOpaque is a "perms_" key from the dashboard.
	APIKeyOpaque APIKeyKind = "opaque"
	// APIKeyJWT is a legacy JWT key.
	APIKeyJWT APIKeyKind = "jwt"
)

func ValidateAPIKey

func ValidateAPIKey(key string) (APIKeyKind, error)

ValidateAPIKey checks that key looks like a perms.io API key and returns its kind. Opaque "perms_" keys are checked for length, alphabet and checksum, so a typo is caught before any request is made; legacy JWT keys only for their three-part shape. Only the API can say whether a key exists and is valid. Errors are of kind KindConfig.

type AccessEntry

type AccessEntry struct {
	Principal string `json:"principal"`
	IsGroup   bool   `json:"is_group"`
	// ViaGroup is the group slug through which Principal has access, for
	// entries added by expand_groups; empty for the principal's own grants.
	ViaGroup string      `json:"via_group"`
	Via      []AccessVia `json:"via"`
}

AccessEntry is one principal with access to a resource.

type AccessScope

type AccessScope struct {
	// URI is a resource URI (subtree, exact) or a URI pattern with "_" segments (pattern).
	URI string `json:"uri"`
	// Kind is "subtree" (the URI and everything below), "pattern" (URIs whose
	// leading segments match, "_" matching one segment, and everything below),
	// "exact" (reserved by the API) or "unknown".
	Kind string `json:"kind"`
}

AccessScope is one place where a principal holds a permission.

type AccessScopes

type AccessScopes struct {
	Under string `json:"under"`
	// CoversAll is true when the principal holds the permission on Under itself,
	// and so on everything below it.
	CoversAll bool          `json:"covers_all"`
	Scopes    []AccessScope `json:"scopes"`
}

AccessScopes answers "where may this principal use this permission under Under?".

func (*AccessScopes) Allows

func (a *AccessScopes) Allows(uri string) bool

Allows reports whether the scopes cover uri. A URI outside Under was not asked about, so false for it means "unknown", not "denied".

func (*AccessScopes) Exact

func (a *AccessScopes) Exact() []string

Exact returns the exact scopes' URIs. The API does not return exact scopes today.

func (*AccessScopes) HasPatterns

func (a *AccessScopes) HasPatterns() bool

HasPatterns reports whether some scope is a pattern, so Prefixes alone does not cover everything.

func (*AccessScopes) IsEmpty

func (a *AccessScopes) IsEmpty() bool

IsEmpty reports whether the principal has no access anywhere under Under.

func (*AccessScopes) Patterns

func (a *AccessScopes) Patterns() []string

Patterns returns the pattern scopes' URIs (with "_" segments).

func (*AccessScopes) Prefixes

func (a *AccessScopes) Prefixes() []string

Prefixes returns the subtree scopes' URIs, dropping any already covered by a shorter one. A prefix p stands for p and everything below it: match `path = p OR starts_with(path, p || '/')` (PostgreSQL; elsewhere use a literal prefix test such as `left(path, length(p) + 1) = p || '/'`). Avoid `LIKE p || '/%'`: "_" and "%" are LIKE wildcards and may occur in URIs.

func (*AccessScopes) Regexp

func (a *AccessScopes) Regexp() *regexp.Regexp

Regexp is AccessScopes.RegexpSource compiled.

func (*AccessScopes) RegexpSource

func (a *AccessScopes) RegexpSource() string

RegexpSource returns one anchored regular expression that matches exactly the URIs Allows accepts, for stores that filter by regex (PostgreSQL "~", MongoDB $regex). With no scopes it matches nothing ("$.^"). The syntax is common to RE2, PCRE and POSIX ERE.

type AccessVia

type AccessVia struct {
	// Resource is the URI the grant was made on.
	Resource string `json:"resource"`
	// Inherited is true unless the grant was made on exactly the resource.
	Inherited bool `json:"inherited"`
	// Match is "exact", "ancestor", "wildcard" or "unknown".
	Match string `json:"match"`
	// Source is "role" or "direct".
	Source   string `json:"source"`
	RoleID   string `json:"role_id,omitempty"`
	RoleName string `json:"role_name,omitempty"`
}

AccessVia is one grant (or one role within a grant) that gives access.

type BatchResult

type BatchResult struct {
	Resource string `json:"resource"`
	// Allowed is true when the principal holds every requested permission on Resource.
	Allowed bool `json:"allowed"`
	// Missing is the requested permissions the principal lacks on Resource.
	Missing []string `json:"missing"`
}

BatchResult is one resource's result from Client.BatchCheckDetailed.

type CheckRequest

type CheckRequest struct {
	// Principal is the principal (user, service, "group:<slug>"...) whose
	// permissions are checked.
	Principal string
	// Resources are resource URIs; every one is checked. At most 100.
	Resources []string
	// Permissions are permission names. At most 100.
	Permissions []string
}

CheckRequest is a check of several permissions on several resources.

type CheckResult

type CheckResult struct {
	// Allowed is true when the principal holds every permission on every resource.
	Allowed bool `json:"allowed"`
	// Missing lists what is missing per resource. Empty when allowed.
	Missing []MissingPermission `json:"missing"`
}

CheckResult is the result of Client.CheckDetailed.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client talks to the perms.io API. It is safe for concurrent use.

func New

func New(opts Options) (*Client, error)

New validates the options and creates a client. It does not connect until the first call. Errors are of kind KindConfig.

func (*Client) APIKeyKind

func (c *Client) APIKeyKind() APIKeyKind

APIKeyKind reports whether the client's key is an opaque "perms_" key or a legacy JWT.

func (*Client) Assert

func (c *Client) Assert(ctx context.Context, principal, permission, resource string) error

Assert returns nil when principal holds permission on resource, and an Error of kind KindDenied (with Missing filled in) when it does not.

func (*Client) AssertAll

func (c *Client) AssertAll(ctx context.Context, req CheckRequest) error

AssertAll returns nil when the principal holds every permission on every resource, and an Error of kind KindDenied when it does not.

func (*Client) BatchCheck

func (c *Client) BatchCheck(ctx context.Context, principal string, permissions []string, resources []string) (map[string]bool, error)

BatchCheck checks each resource separately and returns, per resource URI, whether principal holds every one of permissions on it. Use it to filter a list you already have in memory. More than 100 resources are split into several calls.

func (*Client) BatchCheckDetailed

func (c *Client) BatchCheckDetailed(ctx context.Context, principal string, permissions []string, resources []string) ([]BatchResult, error)

BatchCheckDetailed is BatchCheck returning one entry per distinct resource, in request order, with the missing permissions.

func (*Client) Check

func (c *Client) Check(ctx context.Context, principal, permission, resource string) (bool, error)

Check reports whether principal holds permission on resource. A denial is false with a nil error.

func (*Client) CheckAll

func (c *Client) CheckAll(ctx context.Context, principal string, permissions []string, resource string) (bool, error)

CheckAll reports whether principal holds every one of permissions on resource.

func (*Client) CheckAny

func (c *Client) CheckAny(ctx context.Context, principal string, permissions []string, resource string) (bool, error)

CheckAny reports whether principal holds at least one of permissions on resource. It makes a single Check call.

func (*Client) CheckDetailed

func (c *Client) CheckDetailed(ctx context.Context, req CheckRequest) (*CheckResult, error)

CheckDetailed checks whether the principal holds every permission on every resource and returns what is missing. A denial is a normal result, not an error.

It fails with KindPermission if the API key lacks permission.check, and KindValidation for malformed URIs or empty inputs.

func (*Client) Close

func (c *Client) Close() error

Close releases the connection the client dialled. Clients made with Client.WithProject share it; close only the original.

func (*Client) Conn

func (c *Client) Conn() grpc.ClientConnInterface

Conn returns the wrapped connection, for other generated clients.

func (*Client) CreatePermission

func (c *Client) CreatePermission(ctx context.Context, name, description string) (*Permission, error)

CreatePermission creates a permission, or returns the existing one: create is idempotent. If the permission already exists, a non-empty description that differs replaces the stored one (which needs permission.update on it); an empty description leaves it as it is.

func (*Client) CreateRole

func (c *Client) CreateRole(ctx context.Context, name, description string, permissions []string) (*Role, error)

CreateRole creates a role and returns it with its ID, or returns the existing role with this name: create is idempotent. If the role already exists, a non-empty description or permission list that differs replaces the stored one (which needs role.update on it); empty ones leave it as it is.

func (*Client) DeletePermission

func (c *Client) DeletePermission(ctx context.Context, name string) error

DeletePermission deletes a permission and every grant of it. Deleting one that does not exist succeeds.

func (*Client) DeleteRole

func (c *Client) DeleteRole(ctx context.Context, id string) error

DeleteRole deletes a role by ID.

func (*Client) GetGrant

func (c *Client) GetGrant(ctx context.Context, principal, resource string) (*Grant, error)

GetGrant returns what is granted to principal on exactly resource (not inherited grants). The API returns role IDs only, so RoleRef.Name is empty.

func (*Client) GetPermission

func (c *Client) GetPermission(ctx context.Context, name string) (*Permission, error)

GetPermission returns one permission by name.

func (*Client) GetRole

func (c *Client) GetRole(ctx context.Context, id string) (*Role, error)

GetRole returns a role by ID.

func (*Client) Grant

func (c *Client) Grant(ctx context.Context, req GrantRequest) error

Grant grants permissions and roles to a principal on a resource, and so on every resource below it. Additive: what the principal already holds there is kept. Pass GroupPrincipal(slug) to grant to every member of a group.

Like every write, a grant is sent once and never retried automatically. It fails with KindValidation if a permission or role does not exist, and KindPermission unless the API key has full access.

Example
package main

import (
	"context"
	"errors"
	"log"

	permio "github.com/PrivateJAR/permio-go"
)

func main() {
	var client *permio.Client // from permio.New
	ctx := context.Background()

	// Grant to a group: every member holds the grant.
	err := client.Grant(ctx, permio.GrantRequest{
		Principal:   permio.GroupPrincipal("editors"),
		Resource:    permio.MustResource("docs"),
		Permissions: []string{"docs.read", "docs.write"},
	})
	if errors.Is(err, permio.ErrPermission) {
		log.Fatal("this API key is check-only; grants need a full-access key")
	}
}

func (*Client) Groups

func (c *Client) Groups() *Groups

Groups returns the group operations, in the client's project.

func (*Client) ListAccess

func (c *Client) ListAccess(ctx context.Context, principal, permission, under string) (*AccessScopes, error)

ListAccess answers "where may principal use permission at or below under?", following every page. Access through roles and groups counts. Use the result to filter lists in your database: CoversAll means no filter is needed, Prefixes gives path prefixes, RegexpSource a regex, and Allows tests a single URI.

The API key needs permission.check on under and principal.assignment.get on the principal.

Example
package main

import (
	"context"
	"fmt"
	"log"

	permio "github.com/PrivateJAR/permio-go"
)

func main() {
	var client *permio.Client // from permio.New
	access, err := client.ListAccess(context.Background(), "user-123", "docs.read", "/docs")
	if err != nil {
		log.Fatal(err)
	}
	switch {
	case access.CoversAll:
		// No filter needed.
	case !access.HasPatterns():
		fmt.Println("path prefixes:", access.Prefixes())
	default:
		fmt.Println("regex:", access.RegexpSource())
	}
}

func (*Client) ListAssignments

func (c *Client) ListAssignments(ctx context.Context, principal string, opts *ListOptions) iter.Seq2[Grant, error]

ListAssignments iterates every grant made to principal, one per resource URI (GetAllAssignmentsForPrincipal). opts may be nil; Search is ignored.

func (*Client) ListPermissions

func (c *Client) ListPermissions(ctx context.Context, opts *ListOptions) iter.Seq2[Permission, error]

ListPermissions iterates every permission in the project. opts may be nil.

func (*Client) ListPrincipals

func (c *Client) ListPrincipals(ctx context.Context, opts *ListOptions) iter.Seq2[string, error]

ListPrincipals iterates the IDs of every principal with a grant in the project, groups included as "group:<slug>". opts may be nil.

func (*Client) ListRoles

func (c *Client) ListRoles(ctx context.Context, opts *ListOptions) iter.Seq2[Role, error]

ListRoles iterates every role in the project. opts may be nil.

Example
package main

import (
	"context"
	"fmt"
	"log"

	permio "github.com/PrivateJAR/permio-go"
)

func main() {
	var client *permio.Client // from permio.New
	for role, err := range client.ListRoles(context.Background(), nil) {
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println(role.ID, role.Name)
	}
}

func (*Client) Organisations

func (c *Client) Organisations() organisationv1.OrganisationServiceClient

Organisations returns the generated OrganisationService client, with auth, timeouts, retries and error mapping applied.

func (*Client) Permissions

Permissions returns the generated PermissionsService client, with auth, timeouts, retries and error mapping applied. Use it for RPCs the helpers do not wrap; set ProjectName on requests yourself.

Example
package main

import (
	"context"
	"fmt"
	"log"

	permio "github.com/PrivateJAR/permio-go"
	permissionsv1 "github.com/PrivateJAR/permio-go/proto/permissions/v1"
)

func main() {
	var client *permio.Client // from permio.New
	// The generated client, with auth, timeouts, retries and typed errors.
	res, err := client.Permissions().GetTotalRolesAndPermissionsForOrganisation(context.Background(),
		&permissionsv1.GetTotalRolesAndPermissionsForOrganisationRequest{})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(res)
}

func (*Client) Project

func (c *Client) Project() string

Project returns the client's project, or "" if none is set.

func (*Client) Revoke

func (c *Client) Revoke(ctx context.Context, req GrantRequest) error

Revoke removes permissions and roles from a principal's grant on exactly req.Resource. Names the grant does not hold are ignored, so revoking twice is harmless. Grants on ancestor URIs are not touched, so the principal may still pass a check on the resource afterwards; WhoHasAccess shows where access comes from.

Like every write, a revoke is sent once and never retried automatically.

func (*Client) UpdatePermission

func (c *Client) UpdatePermission(ctx context.Context, name, newName, description string) (*Permission, error)

UpdatePermission replaces a permission's description and, when newName is not empty, renames it. Renaming to a name already in use fails with KindAlreadyExists.

func (*Client) UpdateRole

func (c *Client) UpdateRole(ctx context.Context, id, name, description string, permissions []string) (*Role, error)

UpdateRole replaces a role's name, description and permissions. Renaming to a name another role uses fails with KindAlreadyExists.

func (*Client) WhoAmI

func (c *Client) WhoAmI(ctx context.Context) (*Identity, error)

WhoAmI describes the credential this client uses: for an API key, its ID, its organisation, the project it belongs to and its access level. Useful to verify a key at startup. It needs no project.

func (*Client) WhoHasAccess

func (c *Client) WhoHasAccess(ctx context.Context, resource string, opts *WhoHasAccessOptions) ([]AccessEntry, error)

WhoHasAccess answers "who has access to resource?", following every page (ListPrincipalsWithAccess). It lists the principals (groups included, as "group:<slug>") with a grant on resource, an ancestor of it, or a "_" wildcard URI matching it, ordered by principal ID, with the grants that give the access. opts may be nil.

The API key needs principal.list and principal.assignment.list on the project.

func (*Client) WithProject

func (c *Client) WithProject(project string) *Client

WithProject returns a client that uses project instead of the default project. It shares c's connection.

type Error

type Error struct {
	// Kind classifies the error.
	Kind Kind
	// Code is the gRPC status code. Errors raised by the SDK itself use
	// FailedPrecondition (config), PermissionDenied (denied) and
	// InvalidArgument (its own input checks). It is never codes.OK.
	Code codes.Code
	// Message is the server's (or SDK's) message.
	Message string
	// Missing is set for KindPermission (what the API key lacks, with the
	// server's fully qualified internal URIs) and KindDenied (what the
	// principal lacks).
	Missing []MissingPermission
	// Principal is the checked principal, for KindDenied.
	Principal string
	// RetryAfter is the delay the server asked for (google.rpc.RetryInfo or a
	// retry-after header), for KindRateLimited and sometimes KindUnavailable.
	// Zero when the server gave none.
	RetryAfter time.Duration
	// Metadata holds the response headers and trailers sent with the error.
	Metadata metadata.MD
	// contains filtered or unexported fields
}

Error is the error returned by every Client call that fails, including calls made through the raw clients (Client.Permissions, Client.Organisations). Use errors.As to read its fields, or errors.Is with a Kind / Err* sentinel.

func (*Error) Error

func (e *Error) Error() string

func (*Error) GRPCStatus

func (e *Error) GRPCStatus() *status.Status

GRPCStatus lets status.FromError and status.Code read the gRPC code. The code is never codes.OK: an *Error is always a failure.

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches Kind targets, so errors.Is(err, permio.ErrAuth) works.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying gRPC or context error, if any.

type Grant

type Grant struct {
	Principal   string    `json:"principal"`
	Resource    string    `json:"resource"`
	Roles       []RoleRef `json:"roles"`
	Permissions []string  `json:"permissions"`
}

Grant is the roles and permissions granted to one principal on exactly one resource URI (an "assignment").

type GrantRequest

type GrantRequest struct {
	// Principal is one of your principal IDs, or GroupPrincipal(slug) for a group.
	Principal string
	// Resource is the resource URI of the grant. A grant also covers every URI
	// below it; a revoke applies to exactly this URI.
	Resource string
	// Permissions are permission names.
	Permissions []string
	// Roles are role IDs (see ListRoles).
	Roles []string
}

GrantRequest says what to grant or revoke.

type Group

type Group struct {
	Name        string     `json:"name"`
	Description string     `json:"description"`
	PrincipalID string     `json:"principal_id"`
	CreatedAt   *time.Time `json:"created_at,omitempty"`
	UpdatedAt   *time.Time `json:"updated_at,omitempty"`
}

Group is a named set of principals. Grants to Group.PrincipalID ("group:<name>") apply to every member.

type GroupMember

type GroupMember struct {
	PrincipalID string     `json:"principal_id"`
	CreatedAt   *time.Time `json:"created_at,omitempty"`
}

GroupMember is one member of a group.

type Groups

type Groups struct {
	// contains filtered or unexported fields
}

Groups manages groups of principals; get it with Client.Groups. A group is referenced as the principal "group:<slug>" (see GroupPrincipal): grant to it and every member holds the grant. Groups cannot contain groups.

Every method that takes a group accepts its slug ("editors") or its principal ID ("group:editors").

func (*Groups) AddMembers

func (g *Groups) AddMembers(ctx context.Context, name string, principals []string) (*MembershipChange, error)

AddMembers adds up to MaxBatch (100) principals to a group in one write; more fail with KindValidation (split them yourself). Principals that were already members are reported in Unchanged. Group principals ("group:...") are rejected, because groups cannot be nested.

func (*Groups) Create

func (g *Groups) Create(ctx context.Context, name, description string) (*Group, error)

Create creates a group. It fails with KindAlreadyExists if the project already has a group with this name.

func (*Groups) Delete

func (g *Groups) Delete(ctx context.Context, name string) error

Delete deletes a group, its memberships and every grant made to it. It succeeds whether or not the group existed.

func (*Groups) Get

func (g *Groups) Get(ctx context.Context, name string) (*Group, error)

Get returns a group, or an error of kind KindNotFound.

func (*Groups) List

func (g *Groups) List(ctx context.Context, opts *ListOptions) iter.Seq2[Group, error]

List iterates every group in the project, ordered by name. opts may be nil.

func (*Groups) ListForPrincipal

func (g *Groups) ListForPrincipal(ctx context.Context, principal string, opts *ListOptions) iter.Seq2[Group, error]

ListForPrincipal iterates the groups principal is a member of, ordered by name. opts may be nil; Search is ignored.

func (*Groups) ListMembers

func (g *Groups) ListMembers(ctx context.Context, name string, opts *ListOptions) iter.Seq2[GroupMember, error]

ListMembers iterates a group's members, ordered by principal ID. opts may be nil; Search is ignored.

func (*Groups) RemoveMembers

func (g *Groups) RemoveMembers(ctx context.Context, name string, principals []string) (*MembershipChange, error)

RemoveMembers removes up to MaxBatch (100) principals from a group in one write; more fail with KindValidation. Principals that were not members are reported in Unchanged.

func (*Groups) Update

func (g *Groups) Update(ctx context.Context, name, description string) (*Group, error)

Update replaces a group's description. A group's name cannot be changed.

type Identity

type Identity struct {
	// PrincipalID is the caller's principal ID; for an API key, the key's ID.
	PrincipalID string `json:"principal_id"`
	// OrganisationID is the caller's organisation. Empty for a user who has not joined one.
	OrganisationID string `json:"organisation_id"`
	// Kind is the kind of credential.
	Kind PrincipalKind `json:"kind"`
	// Project is the project an API key belongs to; it can only be used
	// there. Empty for users.
	Project string `json:"project,omitempty"`
	// APIKeyAccess is set for API keys only.
	APIKeyAccess APIKeyAccess `json:"api_key_access,omitempty"`
	// Email is the user's email address, when the caller is a user with one.
	Email string `json:"email,omitempty"`
}

Identity describes the caller, as returned by Client.WhoAmI.

type Kind

type Kind string

Kind classifies an Error. Kinds are also errors, so callers can match them with errors.Is:

if errors.Is(err, permio.ErrNotFound) { ... }
const (
	// KindConfig: the client is configured incorrectly, e.g. a malformed API
	// key or no project.
	KindConfig Kind = "config"
	// KindAuth: the API key was missing, unknown or revoked (gRPC Unauthenticated).
	KindAuth Kind = "auth"
	// KindPermission: the API key is valid but may not make this call (gRPC
	// PermissionDenied), e.g. a check-only key trying to write. This is about
	// the caller's rights; a principal failing a check is [KindDenied].
	KindPermission Kind = "permission_denied"
	// KindValidation: the request was rejected as malformed (gRPC
	// InvalidArgument), or failed the SDK's own input checks.
	KindValidation Kind = "invalid_argument"
	// KindNotFound: the entity does not exist (gRPC NotFound).
	KindNotFound Kind = "not_found"
	// KindAlreadyExists: the entity already exists (gRPC AlreadyExists).
	KindAlreadyExists Kind = "already_exists"
	// KindUnavailable: the API could not be reached (gRPC Unavailable).
	KindUnavailable Kind = "unavailable"
	// KindTimeout: the call did not finish in time (gRPC DeadlineExceeded).
	KindTimeout Kind = "timeout"
	// KindRateLimited: the organisation's rate limit is used up (gRPC
	// ResourceExhausted). [Error.RetryAfter] is the server's requested delay.
	KindRateLimited Kind = "rate_limited"
	// KindDenied: returned by the Assert methods when the principal does not
	// hold the permissions. A normal authorization outcome, not an API failure.
	KindDenied Kind = "denied"
	// KindOther: anything else, including cancellation by the caller.
	KindOther Kind = "error"
)

func KindOf

func KindOf(err error) Kind

KindOf returns the Kind of err, or "" if it is not an *Error.

func (Kind) Error

func (k Kind) Error() string

Error makes a Kind usable as an errors.Is target.

type ListOptions

type ListOptions struct {
	// Search filters by name, where the listing supports it.
	Search string
	// PageSize is the number of items per request, 1..100. Default 100.
	PageSize int
}

ListOptions configures a paginated listing.

type MembershipChange

type MembershipChange struct {
	// Changed are the principals added (or removed).
	Changed []string `json:"changed"`
	// Unchanged are the principals that were already members (or were not members).
	Unchanged []string `json:"unchanged"`
}

MembershipChange is the result of adding or removing group members.

type MissingPermission

type MissingPermission struct {
	Resource    string   `json:"resource"`
	Permissions []string `json:"permissions"`
}

MissingPermission lists permissions missing on one resource.

func ParseMissingPermissions

func ParseMissingPermissions(msg string) []MissingPermission

ParseMissingPermissions extracts the missing-permission list from a PermissionDenied message, if it has one.

type Options

type Options struct {
	// APIKey is a "perms_" key from the dashboard, or a legacy JWT key. When
	// empty, the PERMIO_API_KEY environment variable is used.
	APIKey string
	// Endpoint is the API's "host:port" ([DefaultEndpoint] when empty), or a
	// URL such as "https://dev-api.perms.io". "http://" URLs dial without TLS,
	// which is only allowed to a local host (see Insecure).
	// Ignored when Conn is set.
	Endpoint string
	// Insecure dials without TLS. Only for local servers: the API key would be
	// sent in clear, so New fails with [KindConfig] unless the endpoint's host
	// is a loopback address, "localhost" or a name ending in ".localhost".
	Insecure bool
	// Project is the default project_name. Use [Client.WithProject] for others.
	Project string
	// Timeout is the per-attempt timeout. Zero means [DefaultTimeout]; a
	// negative value disables it. Set an overall deadline with the context.
	Timeout time.Duration
	// Retry configures retries of idempotent reads. Writes are never retried.
	Retry RetryOptions
	// UserAgent is prepended to grpc-go's user agent.
	UserAgent string
	// DialOptions are appended to the default dial options.
	DialOptions []grpc.DialOption
	// Conn, when set, is used instead of dialling Endpoint, e.g. a bufconn
	// connection in tests. Auth, timeouts, retries and error mapping still
	// apply. The caller owns it: Close does not close it.
	Conn grpc.ClientConnInterface
}

Options configures a Client. Only an API key is required.

type Permission

type Permission struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

Permission is a named action, e.g. "document.read".

type PrincipalKind

type PrincipalKind string

PrincipalKind is the kind of credential a client uses, from Client.WhoAmI.

const (
	PrincipalUser           PrincipalKind = "user"
	PrincipalAPIKey         PrincipalKind = "api_key"
	PrincipalServiceAccount PrincipalKind = "service_account"
	PrincipalUnknown        PrincipalKind = "unknown"
)

type RetryOptions

type RetryOptions struct {
	// MaxRetries is the number of retries after the first attempt. Zero means
	// the default, 2; a negative value disables retries.
	MaxRetries int
	// BaseDelay is the backoff before the first retry; it doubles each time,
	// with jitter. Default 100ms.
	BaseDelay time.Duration
	// MaxDelay caps a single backoff. Default 2s. A rate-limited read
	// (ResourceExhausted) is retried after the server's requested delay only
	// when that delay is within MaxDelay; otherwise the error is returned.
	MaxDelay time.Duration
}

RetryOptions configures retries of idempotent reads.

type Role

type Role struct {
	ID          string   `json:"id"`
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Permissions []string `json:"permissions"`
}

Role is a named set of permissions. Roles are addressed by ID.

type RoleRef

type RoleRef struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

RoleRef identifies a role in a grant.

type WhoHasAccessOptions

type WhoHasAccessOptions struct {
	// Permission, when set, counts only grants that confer it, so a principal
	// is listed exactly when a check of it passes through its own grants (or,
	// for entries with ViaGroup, through that group).
	Permission string
	// ExpandGroups also lists every member of each listed group, once per
	// group, with ViaGroup set.
	ExpandGroups bool
}

WhoHasAccessOptions refines Client.WhoHasAccess.

Directories

Path Synopsis
proto

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL