service

package
v0.0.0-...-c848cff Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package service holds Tether's tenant management on top of a store.

Index

Constants

View Source
const (
	// KeyPrefix is the keysmith prefix of every device key.
	KeyPrefix = "tdk"

	// MetadataDeviceID is the key metadata field that holds the Tether device
	// ID the key was issued for.
	MetadataDeviceID = "tether_device_id"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type CreateTenantInput

type CreateTenantInput struct {
	AppID      string
	ExternalID string
	Name       string
	Limits     tenant.Limits
}

CreateTenantInput describes a tenant to create. AppID and ExternalID name the host application's own tenant and are required.

type Credentials

type Credentials struct {
	Device   *device.Device
	Username string
	Password string
}

Credentials is what a device connects with. Username is the keysmith key ID and Password the raw key. Password is shown once: Provision and Rotate return it and nothing stores it.

type Devices

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

Devices manages the devices of a tenant: the keysmith key a device authenticates with, the warden roles that decide what it may publish and subscribe to, and the record that ties them to a tenant.

Every key and role call runs under the tenant's own scope (its AppID and ExternalID), whatever scope the caller's context carries. Devices only ever get keys this service created, and there is no way to attach an existing key.

The auth layer caches a validated key for a short time. After Revoke, Delete or Rotate with a zero grace, call (*auth.Keysmith).Evict with the device's KeyID so the change takes effect at once.

func NewDevices

func NewDevices(s device.Store, t *Tenants, k KeyManager, r RoleAssigner, opts ...DevicesOption) *Devices

NewDevices returns a device service. s holds the records, t resolves a tenant ID to the scope for keysmith and warden, k issues keys and r assigns roles.

func (*Devices) Delete

func (s *Devices) Delete(ctx context.Context, tenantID id.TenantID, deviceID id.DeviceID) error

Delete revokes the device's key, removes its role assignments and deletes the record. A key that is already revoked or gone does not stop it. If a step fails the record is kept, so calling Delete again finishes the job. With WithSessions, the session of a device pinned to a client ID is deleted once the record is.

func (*Devices) Get

func (s *Devices) Get(ctx context.Context, tenantID id.TenantID, deviceID id.DeviceID) (*device.Device, error)

Get returns the device. A device of another tenant wraps tether.ErrNotFound.

func (*Devices) List

func (s *Devices) List(ctx context.Context, f device.ListFilter) ([]*device.Device, error)

List returns the devices of the tenant named by f.TenantID.

func (*Devices) Provision

func (s *Devices) Provision(ctx context.Context, tenantID id.TenantID, in ProvisionInput) (*Credentials, error)

Provision creates a device: a keysmith key (prefix "tdk", environment "live", metadata "tether_device_id"), the requested roles on that key, and the device record, in that order. If a role cannot be assigned or the record cannot be stored, the roles are unassigned and the key is revoked before the error is returned.

A missing Name, an invalid ClientID or an empty role slug wraps tether.ErrInvalidInput, and an unknown tenant wraps tether.ErrNotFound.

func (*Devices) Revoke

func (s *Devices) Revoke(ctx context.Context, tenantID id.TenantID, deviceID id.DeviceID, reason string) error

Revoke permanently disables the device's key. The record stays, so an operator still sees the device, but it can no longer connect. Revoking a device whose key is already revoked, or gone, succeeds. With WithSessions, the session of a device pinned to a client ID is deleted too.

func (*Devices) Rotate

func (s *Devices) Rotate(ctx context.Context, tenantID id.TenantID, deviceID id.DeviceID, grace time.Duration) (*Credentials, error)

Rotate issues a new password for the device. The username does not change. The old password keeps working for grace, then stops. A zero grace uses keysmith's default (the key's policy grace period, else 24 hours), and a negative grace wraps tether.ErrInvalidInput. A revoked device cannot be rotated.

func (*Devices) SetRoles

func (s *Devices) SetRoles(ctx context.Context, tenantID id.TenantID, deviceID id.DeviceID, roles []string) error

SetRoles replaces the device's roles with the given slugs and stores them on the device record. It applies only the difference: every new role is assigned first (a role the key already holds is a no-op), then every other role the key holds in the tenant's root namespace is unassigned. A role in both lists is never unassigned, so a connected device keeps its grants and subscriptions for it throughout (Broker.ReloadPrincipal runs on every assignment event and prunes what the grants no longer allow).

If a step fails, the previous roles are put back the same way and the error is returned. Calls for one device (and Delete) run one at a time in this process; with several processes on one store they can still interleave.

type DevicesOption

type DevicesOption func(*Devices)

DevicesOption configures a Devices service.

func WithLogger

func WithLogger(l *slog.Logger) DevicesOption

WithLogger is where Devices logs what it cannot report as an error: a session it could not delete after the device's key was already revoked. Nil discards.

func WithSessions

func WithSessions(d SessionDeleter) DevicesOption

WithSessions makes Revoke and Delete delete the session of a device pinned to a client ID, through d. Without it, or for an unpinned device (whose client IDs Tether does not know), the session stays until its expiry runs out.

type KeyManager

type KeyManager interface {
	CreateKey(ctx context.Context, in *keysmith.CreateKeyInput) (*key.CreateResult, error)
	RotateKey(ctx context.Context, keyID kid.KeyID, reason rotation.Reason, opts ...keysmith.RotateOption) (*key.CreateResult, error)
	RevokeKey(ctx context.Context, keyID kid.KeyID, reason string) error
}

KeyManager is the part of keysmith's engine that Devices uses. It is satisfied by *keysmith.Engine.

type ProvisionInput

type ProvisionInput struct {
	Name      string
	ClientID  string
	Roles     []string
	ExpiresAt *time.Time
}

ProvisionInput describes a device to create. Name is required. ClientID, if set, pins the device to one MQTT client ID and must be a single topic level: it becomes "%c" in the device's grants.

type RoleAssigner

type RoleAssigner interface {
	AssignRoleToAPIKey(ctx context.Context, tenantID, keyID, roleSlug string) error
	UnassignRoleFromAPIKey(ctx context.Context, tenantID, keyID string) error
	UnassignOtherRolesFromAPIKey(ctx context.Context, tenantID, keyID string, keep []string) error
}

RoleAssigner assigns warden roles to API keys. It is satisfied by *WardenRoles (NewWardenRoles). tenantID is the warden tenant, which is the Tether tenant's ExternalID.

AssignRoleToAPIKey must be a no-op for a role the key already holds, and UnassignOtherRolesFromAPIKey must leave the roles in keep assigned: SetRoles relies on both so a role a device keeps is never unassigned, not even for a moment.

type SessionDeleter

type SessionDeleter interface {
	DeleteSession(tenantID, clientID string) error
}

SessionDeleter deletes an MQTT session: the client's subscriptions and queued messages. It is satisfied by *broker.Broker. tenantID is the Tether tenant ID and clientID the raw client ID the device connects with.

type Tenants

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

Tenants manages tenants and resolves the host application's (app, external) pair to one.

ResolveTenant keeps a 30 s cache that Update and Delete evict. The cache is per process: when several broker nodes share a store, a change made through one node reaches the others within 30 s, or at once when every node calls Evict for it (a cell's control bus, events.EvictTenantsOnUpdate).

func NewTenants

func NewTenants(s store.Store, opts ...TenantsOption) *Tenants

NewTenants returns a tenant service over s.

func (*Tenants) Create

func (s *Tenants) Create(ctx context.Context, in CreateTenantInput) (*tenant.Tenant, error)

Create stores a new, enabled tenant. A missing AppID or ExternalID wraps tether.ErrInvalidInput. An ExternalID already used by another tenant, under this app or any other, wraps tether.ErrConflict: keysmith and warden isolate by tenant ID alone, so two tenants sharing an ExternalID would share keys and roles.

The check reads every tenant and is not atomic across processes: broker nodes that create tenants on one store at the same moment can both pass it. Create tenants through one node, or let the host application guarantee that its tenant IDs are unique across apps.

func (*Tenants) Delete

func (s *Tenants) Delete(ctx context.Context, tenantID id.TenantID) error

Delete removes the tenant. It returns an error wrapping tether.ErrConflict while the tenant still has devices.

func (*Tenants) Evict

func (s *Tenants) Evict(tenantID id.TenantID)

Evict drops the tenant from the resolve cache. Update and Delete do it themselves. When several broker nodes share the store, each node calls it for a change another node made (events.EvictTenantsOnUpdate on the cell's control bus), so the change does not wait out the 30 s cache there.

func (*Tenants) Get

func (s *Tenants) Get(ctx context.Context, tenantID id.TenantID) (*tenant.Tenant, error)

Get returns the tenant with the given ID.

func (*Tenants) List

func (s *Tenants) List(ctx context.Context, f tenant.ListFilter) ([]*tenant.Tenant, error)

List returns tenants ordered by creation time.

func (*Tenants) ResolveTenant

func (s *Tenants) ResolveTenant(ctx context.Context, appID, externalID string) (*tenant.Tenant, error)

ResolveTenant returns the tenant mapped to (appID, externalID), served from a 30 s cache. A tenant that does not exist wraps tether.ErrNotFound and is not cached. A disabled tenant is returned as is: the caller checks Enabled. The returned value is a copy the caller may modify.

func (*Tenants) Update

func (s *Tenants) Update(ctx context.Context, tenantID id.TenantID, in UpdateTenantInput) (*tenant.Tenant, error)

Update applies the set fields of in to the tenant and evicts it from the resolve cache.

type TenantsOption

type TenantsOption func(*Tenants)

TenantsOption configures a Tenants.

func WithEvictHook

func WithEvictHook(fn func(tenantID id.TenantID)) TenantsOption

WithEvictHook calls fn with the tenant's ID every time the resolve cache drops it: Update (on both sides of the write), Delete and Evict. A cache kept beside the service hangs off it, so it is dropped on the same path, a cell's tenant.updated included. fn runs after the service's own lock is released, on the caller's goroutine, and must be quick.

type UpdateTenantInput

type UpdateTenantInput struct {
	Name    *string
	Enabled *bool
	Limits  *tenant.Limits
}

UpdateTenantInput patches a tenant. A nil field is left unchanged.

type WardenRoles

type WardenRoles struct {
	*keysmithbridge.Bridge
	// contains filtered or unexported fields
}

WardenRoles is the RoleAssigner over a warden engine. Assigning and unassigning every role go through warden's keysmithbridge. Removing all but some roles (UnassignOtherRolesFromAPIKey) is Tether's own, because the bridge can only remove every role of a key at once.

func NewWardenRoles

func NewWardenRoles(eng *warden.Engine) *WardenRoles

NewWardenRoles returns a WardenRoles over eng. Writes are attributed to the actor on the context, or else to the service actor "tether".

func (*WardenRoles) UnassignOtherRolesFromAPIKey

func (w *WardenRoles) UnassignOtherRolesFromAPIKey(ctx context.Context, tenantID, keyID string, keep []string) error

UnassignOtherRolesFromAPIKey removes every assignment the API key keyID holds in the tenant's root namespace whose role is not one of the slugs in keep. Assignments of roles in keep are never touched. A slug in keep that names no role keeps nothing.

It mirrors keysmithbridge's UnassignRoleFromAPIKey for each assignment it removes: it emits RoleUnassigned and an "assignment.deleted" audit event, treats an assignment someone else removed first as done, and drops the key's cached warden decisions once anything was removed, whatever happens after.

Jump to

Keyboard shortcuts

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