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)
}
Output:
Index ¶
- Constants
- Variables
- func Collect[T any](seq iter.Seq2[T, error]) ([]T, error)
- func EncodeSegment(segment string) string
- func GroupPrincipal(slug string) string
- func IsIdempotent(fullMethod string) bool
- func IsNotFound(err error) bool
- func MustResource(segments ...string) string
- func Resource(segments ...string) (string, error)
- func ValidateGroupName(slug string) error
- func ValidateResourceURI(uri string) error
- type APIKeyAccess
- type APIKeyKind
- type AccessEntry
- type AccessScope
- type AccessScopes
- func (a *AccessScopes) Allows(uri string) bool
- func (a *AccessScopes) Exact() []string
- func (a *AccessScopes) HasPatterns() bool
- func (a *AccessScopes) IsEmpty() bool
- func (a *AccessScopes) Patterns() []string
- func (a *AccessScopes) Prefixes() []string
- func (a *AccessScopes) Regexp() *regexp.Regexp
- func (a *AccessScopes) RegexpSource() string
- type AccessVia
- type BatchCheckOptions
- type BatchResult
- type CheckRequest
- type CheckResult
- type Client
- func (c *Client) APIKeyKind() APIKeyKind
- func (c *Client) Assert(ctx context.Context, principal, permission, resource string) error
- func (c *Client) AssertAll(ctx context.Context, req CheckRequest) error
- func (c *Client) BatchCheck(ctx context.Context, principal string, permissions []string, ...) (map[string]bool, error)
- func (c *Client) BatchCheckDetailed(ctx context.Context, principal string, permissions []string, ...) ([]BatchResult, error)
- func (c *Client) Check(ctx context.Context, principal, permission, resource string) (bool, error)
- func (c *Client) CheckAll(ctx context.Context, principal string, permissions []string, resource string) (bool, error)
- func (c *Client) CheckAny(ctx context.Context, principal string, permissions []string, resource string) (bool, error)
- func (c *Client) CheckDetailed(ctx context.Context, req CheckRequest) (*CheckResult, error)
- func (c *Client) Close() error
- func (c *Client) Conn() grpc.ClientConnInterface
- func (c *Client) CreatePermission(ctx context.Context, name, description string) (*Permission, error)
- func (c *Client) CreateRole(ctx context.Context, name, description string, permissions []string) (*Role, error)
- func (c *Client) DeletePermission(ctx context.Context, name string) error
- func (c *Client) DeleteRole(ctx context.Context, id string) error
- func (c *Client) GetGrant(ctx context.Context, principal, resource string) (*Grant, error)
- func (c *Client) GetPermission(ctx context.Context, name string) (*Permission, error)
- func (c *Client) GetRole(ctx context.Context, id string) (*Role, error)
- func (c *Client) Grant(ctx context.Context, req GrantRequest) error
- func (c *Client) Groups() *Groups
- func (c *Client) ListAccess(ctx context.Context, principal, permission, under string, ...) (*AccessScopes, error)
- func (c *Client) ListAccessSeq(ctx context.Context, principal, permission, under string) iter.Seq2[AccessScope, error]
- func (c *Client) ListAssignments(ctx context.Context, principal string, opts *ListOptions) iter.Seq2[Grant, error]
- func (c *Client) ListPermissions(ctx context.Context, opts *ListOptions) iter.Seq2[Permission, error]
- func (c *Client) ListPrincipals(ctx context.Context, opts *ListOptions) iter.Seq2[string, error]
- func (c *Client) ListRoles(ctx context.Context, opts *ListOptions) iter.Seq2[Role, error]
- func (c *Client) Organisations() organisationv1.OrganisationServiceClient
- func (c *Client) Permissions() permissionsv1.PermissionsServiceClient
- func (c *Client) Project() string
- func (c *Client) Revoke(ctx context.Context, req GrantRequest) error
- func (c *Client) UpdatePermission(ctx context.Context, name, newName, description string) (*Permission, error)
- func (c *Client) UpdateRole(ctx context.Context, id, name, description string, permissions []string) (*Role, error)
- func (c *Client) WhoAmI(ctx context.Context) (*Identity, error)
- func (c *Client) WhoHasAccess(ctx context.Context, resource string, opts *WhoHasAccessOptions) ([]AccessEntry, error)
- func (c *Client) WhoHasAccessSeq(ctx context.Context, resource string, opts *WhoHasAccessOptions) iter.Seq2[AccessEntry, error]
- func (c *Client) WithProject(project string) *Client
- type Error
- type Grant
- type GrantRequest
- type Group
- type GroupMember
- type Groups
- func (g *Groups) AddMembers(ctx context.Context, name string, principals []string) (*MembershipChange, error)
- func (g *Groups) Create(ctx context.Context, name, description string) (*Group, error)
- func (g *Groups) Delete(ctx context.Context, name string) error
- func (g *Groups) Get(ctx context.Context, name string) (*Group, error)
- func (g *Groups) List(ctx context.Context, opts *ListOptions) iter.Seq2[Group, error]
- func (g *Groups) ListForPrincipal(ctx context.Context, principal string, opts *ListOptions) iter.Seq2[Group, error]
- func (g *Groups) ListMembers(ctx context.Context, name string, opts *ListOptions) iter.Seq2[GroupMember, error]
- func (g *Groups) RemoveMembers(ctx context.Context, name string, principals []string) (*MembershipChange, error)
- func (g *Groups) Update(ctx context.Context, name, description string) (*Group, error)
- type Identity
- type Kind
- type ListAccessOptions
- type ListOptions
- type MembershipChange
- type MissingPermission
- type Options
- type Permission
- type PrincipalKind
- type RetryOptions
- type Role
- type RoleRef
- type WhoHasAccessOptions
Examples ¶
Constants ¶
const ( ScopeSubtree = "subtree" ScopePattern = "pattern" ScopeExact = "exact" ScopeUnknown = "unknown" )
Scope kinds.
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" )
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.
const BatchCheckConcurrency = 4
BatchCheckConcurrency is how many BatchCheck requests (of up to 100 resources each) one Client.BatchCheck call keeps in flight.
const DefaultMaxBatchResources = 10_000
DefaultMaxBatchResources is the default cap on resources per Client.BatchCheck call.
const DefaultMaxResults = 10_000
DefaultMaxResults is the default cap on the results Client.WhoHasAccess and Client.ListAccess collect into memory.
const GroupPrefix = "group:"
GroupPrefix starts the principal ID of every group.
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.
const MaxPageSize = 100
MaxPageSize is the largest page the API accepts (PaginationRequest.limit is 1..100).
const MaxResourceURILength = 2048
MaxResourceURILength is the longest resource URI the API accepts, in bytes.
const WildcardSegment = "_"
WildcardSegment is the "_" segment: in a stored grant URI or a pattern scope it matches exactly one segment.
Variables ¶
var ( ErrConfig error = KindConfig ErrAuth error = KindAuth ErrPermission error = KindPermission ErrValidation error = KindValidation ErrNotFound error = KindNotFound ErrAlreadyExists error = KindAlreadyExists ErrTimeout error = KindTimeout ErrRateLimited error = KindRateLimited ErrDenied error = KindDenied ErrTooManyResults error = KindTooManyResults )
Sentinels for errors.Is. Each is the matching Kind.
Functions ¶
func Collect ¶
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 ¶
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 ¶
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 ¶
IsIdempotent reports whether the SDK retries the RPC with this full gRPC method name ("/package.Service/Method").
func IsNotFound ¶
IsNotFound reports whether err is a NotFound error.
func MustResource ¶
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 ¶
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 ¶
ValidateGroupName checks a group slug: 1-64 lower-case letters, digits and hyphens. Errors are of kind KindValidation.
func ValidateResourceURI ¶
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 BatchCheckOptions ¶ added in v0.2.0
type BatchCheckOptions struct {
// MaxResources caps the resources one call accepts, counted as given
// (before duplicates are removed). Zero means [DefaultMaxBatchResources]
// (10,000); more fail with [KindValidation].
MaxResources int
}
BatchCheckOptions refines Client.BatchCheck and Client.BatchCheckDetailed.
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 ¶
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 ¶
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, opts ...BatchCheckOptions) (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. Resources are sent in requests of 100, at most BatchCheckConcurrency at a time; the first failing request stops the rest and its error is returned. At most MaxResources resources (default 10,000) per call. Pass at most one opts.
func (*Client) BatchCheckDetailed ¶
func (c *Client) BatchCheckDetailed(ctx context.Context, principal string, permissions []string, resources []string, opts ...BatchCheckOptions) ([]BatchResult, error)
BatchCheckDetailed is BatchCheck returning one entry per distinct resource, in request order, with the missing permissions.
func (*Client) Check ¶
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 ¶
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 ¶
DeletePermission deletes a permission and every grant of it. Deleting one that does not exist succeeds.
func (*Client) DeleteRole ¶
DeleteRole deletes a role by ID.
func (*Client) GetGrant ¶
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 ¶
GetPermission returns one permission by name.
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")
}
}
Output:
func (*Client) ListAccess ¶
func (c *Client) ListAccess(ctx context.Context, principal, permission, under string, opts ...ListAccessOptions) (*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.
At most MaxResults scopes (default 10,000) are collected: if there are more, it stops fetching and fails with KindTooManyResults. Pass at most one opts; use Client.ListAccessSeq to stream larger answers.
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())
}
}
Output:
func (*Client) ListAccessSeq ¶ added in v0.2.0
func (c *Client) ListAccessSeq(ctx context.Context, principal, permission, under string) iter.Seq2[AccessScope, error]
ListAccessSeq is the streaming form of Client.ListAccess: it yields every scope, fetching pages as the loop asks for them, so any number of scopes can be processed without holding them in memory. When the principal holds the permission on under itself (CoversAll), it yields exactly one scope: URI under, Kind ScopeSubtree. A failing page yields its error once and ends the iteration.
for scope, err := range client.ListAccessSeq(ctx, userID, "docs.read", "/docs") {
if err != nil { ... }
...
}
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 ¶
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 ¶
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)
}
}
Output:
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 ¶
func (c *Client) Permissions() permissionsv1.PermissionsServiceClient
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)
}
Output:
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 ¶
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.
At most MaxResults entries (default 10,000) are collected: if there are more, it stops fetching and fails with KindTooManyResults. A resource can have millions of principals with access; use Client.WhoHasAccessSeq to stream them.
The API key needs principal.list and principal.assignment.list on the project.
func (*Client) WhoHasAccessSeq ¶ added in v0.2.0
func (c *Client) WhoHasAccessSeq(ctx context.Context, resource string, opts *WhoHasAccessOptions) iter.Seq2[AccessEntry, error]
WhoHasAccessSeq is the streaming form of Client.WhoHasAccess: it yields every entry in the API's order (by principal ID), fetching pages as the loop asks for them, so millions of principals can be processed without holding them in memory. opts may be nil; MaxResults is ignored. A failing page yields its error once and ends the iteration.
for entry, err := range client.WhoHasAccessSeq(ctx, resource, nil) {
if err != nil { ... }
...
}
func (*Client) WithProject ¶
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), InvalidArgument
// (its own input checks) and OutOfRange (too many results). 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
// MaxResults is the cap that was exceeded, for KindTooManyResults.
MaxResults int
// 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) GRPCStatus ¶
GRPCStatus lets status.FromError and status.Code read the gRPC code. The code is never codes.OK: an *Error is always a failure.
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 ¶
Create creates a group. It fails with KindAlreadyExists if the project already has a group with this name.
func (*Groups) Delete ¶
Delete deletes a group, its memberships and every grant made to it. It succeeds whether or not the group existed.
func (*Groups) Get ¶
Get returns a group, or an error of kind KindNotFound.
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.
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 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" // KindTooManyResults: a collect-all call such as [Client.WhoHasAccess] or // [Client.ListAccess] found more results than its MaxResults cap. Use the // streaming form ([Client.WhoHasAccessSeq], [Client.ListAccessSeq]) or // raise MaxResults. [Error.MaxResults] is the cap. KindTooManyResults Kind = "too_many_results" // KindOther: anything else, including cancellation by the caller. KindOther Kind = "error" )
type ListAccessOptions ¶ added in v0.2.0
type ListAccessOptions struct {
// MaxResults caps how many scopes ListAccess collects. Zero means
// [DefaultMaxResults] (10,000); more scopes than that fail with
// [KindTooManyResults]. Use [Client.ListAccessSeq] to stream any number.
MaxResults int
}
ListAccessOptions refines Client.ListAccess.
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
// ListTimeout is the per-attempt timeout for each page request made by
// the listing helpers (iterators, Seq forms, WhoHasAccess, ListAccess).
// Zero means the same as Timeout; a negative value disables it.
ListTimeout 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 ¶
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 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
// MaxResults caps how many entries WhoHasAccess collects. Zero means
// [DefaultMaxResults] (10,000); more entries than that fail with
// [KindTooManyResults]. Ignored by WhoHasAccessSeq, which streams any
// number.
MaxResults int
}
WhoHasAccessOptions refines Client.WhoHasAccess and Client.WhoHasAccessSeq.