scopes

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jun 12, 2026 License: MIT Imports: 6 Imported by: 0

README

Scoped API Key Permissions

Hypeman API keys can be restricted to specific operations using scoped permissions. This lets you create least-privilege tokens — for example, a token that can only read instance status but not create or delete anything.

How It Works

Permissions are embedded in the JWT token as a permissions claim containing an array of scope strings. When a request hits an API endpoint, the middleware checks whether the token carries the required scope for that endpoint. If the scope is missing, the request is rejected with 403 Forbidden.

Tokens without a permissions claim are treated as having full access. This means all existing tokens continue to work without any changes.

Available Scopes

Scopes follow the pattern resource:action. Each resource type supports read, write, and delete actions.

Instances
Scope Grants access to
instance:read List instances, get instance details, view logs, get stats, stat paths
instance:write Create, start, stop, standby, restore, fork instances; exec and cp (WebSocket)
instance:delete Delete instances
Images
Scope Grants access to
image:read List images, get image details
image:write Pull/create images
image:delete Delete images
Volumes
Scope Grants access to
volume:read List volumes, get volume details
volume:write Create volumes, create from archive, attach/detach volumes
volume:delete Delete volumes
Snapshots
Scope Grants access to
snapshot:read List snapshots, get snapshot details
snapshot:write Create snapshots, restore snapshots, fork from snapshots
snapshot:delete Delete snapshots
Builds
Scope Grants access to
build:read List builds, get build details, stream build events
build:write Create builds
build:delete Cancel/delete builds
Devices
Scope Grants access to
device:read List devices, get device details, list available devices
device:write Register devices
device:delete Unregister devices
Ingresses
Scope Grants access to
ingress:read List ingresses, get ingress details
ingress:write Create ingresses
ingress:delete Delete ingresses
Resources
Scope Grants access to
resource:read Health check, resource capacity/allocations
Wildcard

The * scope grants access to all endpoints. It is equivalent to a full-access token but explicitly declared in the permissions claim.

Creating Scoped Tokens

Use the hypeman-token CLI to generate tokens with specific scopes.

# List all available scopes
hypeman-token -list-scopes

# Create a read-only token for instances and images
hypeman-token -user-id myuser -scopes "instance:read,image:read"

# Create a token that can manage instances but not delete them
hypeman-token -user-id myuser -scopes "instance:read,instance:write"

# Create a full-access token with explicit wildcard
hypeman-token -user-id myuser -scopes "*"

# Create a full-access token (legacy style, no permissions claim)
hypeman-token -user-id myuser

Multiple scopes are comma-separated. Whitespace around scope names is trimmed.

Backward Compatibility

Existing tokens that were generated before this feature was added do not have a permissions claim in the JWT. These tokens are treated as having full access to all endpoints — no action is required to keep them working.

Only tokens generated with the -scopes flag carry a permissions claim and are subject to scope enforcement.

Example Scenarios

Monitoring / dashboard token — can read instance status and stats but cannot modify anything:

hypeman-token -user-id dashboard -scopes "instance:read,resource:read"

CI/CD build token — can create builds and pull images, but has no access to instances or volumes:

hypeman-token -user-id ci -scopes "build:read,build:write,image:read,image:write"

Instance operator — full instance lifecycle management without access to images or builds:

hypeman-token -user-id operator -scopes "instance:read,instance:write,instance:delete,volume:read,volume:write,snapshot:read,snapshot:write"

Read-only audit token — can view everything but change nothing:

hypeman-token -user-id auditor -scopes "instance:read,image:read,volume:read,snapshot:read,build:read,device:read,ingress:read,resource:read"

Error Responses

When a scoped token attempts an operation it does not have permission for, the API returns:

{"code": "Forbidden", "message": "missing required scope: instance:write"}

The response includes the specific scope that was required, making it straightforward to diagnose permission issues.

Documentation

Overview

Package scopes defines API permission scopes for hypeman API keys.

Scopes follow the pattern "resource:action" where resource is one of the API resource types and action is read, write, or delete. Tokens without a "permissions" claim are treated as having full access for backward compatibility with existing tokens.

Index

Constants

This section is empty.

Variables

View Source
var DirectScopeRoutes = map[string]Scope{
	"GET /instances/{id}/exec": InstanceWrite,
	"GET /instances/{id}/cp":   InstanceWrite,
}

DirectScopeRoutes lists route keys that enforce scopes via RequireScope middleware directly (not via the Middleware() scope checker). These are outside the OpenAPI router group (e.g. WebSocket endpoints).

View Source
var PublicRoutes = map[string]bool{
	"GET /spec.yaml": true,
	"GET /spec.json": true,
	"GET /swagger":   true,
}

PublicRoutes lists route keys ("METHOD /path") that are intentionally unscoped — they do not require authentication or scope checks. The test uses this to distinguish "intentionally public" from "forgot to add a scope mapping".

View Source
var RouteScopes = map[string]Scope{

	"GET /builds":             BuildRead,
	"POST /builds":            BuildWrite,
	"DELETE /builds/{id}":     BuildDelete,
	"GET /builds/{id}":        BuildRead,
	"GET /builds/{id}/events": BuildRead,

	"GET /devices":           DeviceRead,
	"POST /devices":          DeviceWrite,
	"GET /devices/available": DeviceRead,
	"DELETE /devices/{id}":   DeviceDelete,
	"GET /devices/{id}":      DeviceRead,

	"GET /health":                    ResourceRead,
	"GET /resources":                 ResourceRead,
	"POST /resources/memory/reclaim": ResourceWrite,

	"GET /images":           ImageRead,
	"POST /images":          ImageWrite,
	"DELETE /images/{name}": ImageDelete,
	"GET /images/{name}":    ImageRead,

	"GET /ingresses":         IngressRead,
	"POST /ingresses":        IngressWrite,
	"DELETE /ingresses/{id}": IngressDelete,
	"GET /ingresses/{id}":    IngressRead,

	"GET /instances":                                      InstanceRead,
	"POST /instances":                                     InstanceWrite,
	"DELETE /instances/{id}":                              InstanceDelete,
	"GET /instances/{id}":                                 InstanceRead,
	"POST /instances/{id}/fork":                           InstanceWrite,
	"GET /instances/{id}/logs":                            InstanceRead,
	"POST /instances/{id}/restore":                        InstanceWrite,
	"DELETE /instances/{id}/snapshot-schedule":            SnapshotDelete,
	"GET /instances/{id}/snapshot-schedule":               SnapshotRead,
	"PUT /instances/{id}/snapshot-schedule":               SnapshotWrite,
	"POST /instances/{id}/snapshots":                      SnapshotWrite,
	"POST /instances/{id}/snapshots/{snapshotId}/restore": SnapshotWrite,
	"POST /instances/{id}/standby":                        InstanceWrite,
	"POST /instances/{id}/start":                          InstanceWrite,
	"GET /instances/{id}/stat":                            InstanceRead,
	"GET /instances/{id}/stats":                           InstanceRead,
	"GET /instances/{id}/auto-standby/status":             InstanceRead,
	"GET /instances/{id}/wait":                            InstanceRead,
	"POST /instances/{id}/stop":                           InstanceWrite,
	"PATCH /instances/{id}":                               InstanceWrite,
	"DELETE /instances/{id}/volumes/{volumeId}":           VolumeWrite,
	"POST /instances/{id}/volumes/{volumeId}":             VolumeWrite,

	"GET /snapshots":                    SnapshotRead,
	"DELETE /snapshots/{snapshotId}":    SnapshotDelete,
	"GET /snapshots/{snapshotId}":       SnapshotRead,
	"POST /snapshots/{snapshotId}/fork": SnapshotWrite,

	"GET /volumes":               VolumeRead,
	"POST /volumes":              VolumeWrite,
	"POST /volumes/from-archive": VolumeWrite,
	"DELETE /volumes/{id}":       VolumeDelete,
	"GET /volumes/{id}":          VolumeRead,
}

RouteScopes maps "METHOD /path-pattern" to the required scope. Path patterns use chi-style {param} placeholders.

Functions

func ContextWithPermissions

func ContextWithPermissions(ctx context.Context, perms []Scope) context.Context

ContextWithPermissions stores the granted scopes in the context. A nil slice means full access (legacy behavior).

func HasFullAccess

func HasFullAccess(ctx context.Context) bool

HasFullAccess returns true if the request context indicates full access (no scopes restriction). This is the case for legacy tokens without a permissions claim.

func HasScope

func HasScope(ctx context.Context, required Scope) bool

HasScope checks whether the context has the required scope. Returns true if:

  • No permissions claim was set (legacy full-access token)
  • The wildcard scope "*" is present
  • The specific scope is present

func Middleware

func Middleware() func(http.Handler) http.Handler

Middleware returns a chi middleware that enforces scoped permissions. It looks up the required scope for the matched route pattern and rejects requests that lack the required scope with 403 Forbidden.

Routes not in the scope map are allowed through (e.g. health check or unauthenticated routes that shouldn't reach this middleware).

func RequireScope

func RequireScope(required Scope) func(http.Handler) http.Handler

RequireScope returns an HTTP middleware that rejects requests lacking the specified scope with 403 Forbidden.

func ScopeStrings

func ScopeStrings(scopes []Scope) []string

ScopeStrings converts a slice of Scopes to strings.

Types

type Scope

type Scope string

Scope represents a permission scope for API access.

const (
	// Instance scopes
	InstanceRead   Scope = "instance:read"
	InstanceWrite  Scope = "instance:write" // create, start, stop, standby, restore, fork, exec, cp
	InstanceDelete Scope = "instance:delete"

	// Image scopes
	ImageRead   Scope = "image:read"
	ImageWrite  Scope = "image:write" // pull/create
	ImageDelete Scope = "image:delete"

	// Volume scopes
	VolumeRead   Scope = "volume:read"
	VolumeWrite  Scope = "volume:write" // create, attach, detach
	VolumeDelete Scope = "volume:delete"

	// Snapshot scopes
	SnapshotRead   Scope = "snapshot:read"
	SnapshotWrite  Scope = "snapshot:write" // create, restore, fork
	SnapshotDelete Scope = "snapshot:delete"

	// Build scopes
	BuildRead   Scope = "build:read"
	BuildWrite  Scope = "build:write"  // create
	BuildDelete Scope = "build:delete" // cancel

	// Device scopes
	DeviceRead   Scope = "device:read"
	DeviceWrite  Scope = "device:write"  // register
	DeviceDelete Scope = "device:delete" // unregister

	// Ingress scopes
	IngressRead   Scope = "ingress:read"
	IngressWrite  Scope = "ingress:write"
	IngressDelete Scope = "ingress:delete"

	// Resource/health scopes
	ResourceRead  Scope = "resource:read"
	ResourceWrite Scope = "resource:write"

	// Wildcard scope — grants all permissions
	All Scope = "*"
)

func AllScopes

func AllScopes() []Scope

AllScopes returns the complete list of valid scopes (excluding wildcard).

func GetPermissions

func GetPermissions(ctx context.Context) []Scope

GetPermissions extracts the granted scopes from context. Returns nil if no permissions are set (legacy full-access token).

func ParseScopes

func ParseScopes(s string) ([]Scope, error)

ParseScopes parses a comma-separated scope string into a slice of Scopes. Returns an error if any scope is unrecognized.

func ScopeForRoute

func ScopeForRoute(method, pattern string) (Scope, bool)

ScopeForRoute looks up the required scope for a given HTTP method and chi route pattern (e.g. "GET", "/instances/{id}"). Returns the scope and true if found, or ("", false) if the route is not mapped.

func (Scope) Valid

func (s Scope) Valid() bool

Valid returns true if s is a recognized scope.

Jump to

Keyboard shortcuts

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