Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
af18037
fix(store): bound every Postgres lock wait
pyropy Sep 24, 2026
3f1adea
fix(store): bound the memory principal Lock wait too
pyropy Sep 25, 2026
0868321
test(store): a share-locked ListByPrincipal gives up at the lock timeout
pyropy Sep 30, 2026
6817d0b
chore: rename pglock helper arg
pyropy Oct 1, 2026
8181a87
fix(store): take the tenant lock exclusive when adding a principal
pyropy Oct 1, 2026
eb8a3b0
docs(store): describe what the principal removal's callback does unde…
pyropy Oct 1, 2026
b565a17
test(store): drop the 10s tenant-lock timeout case
pyropy Oct 1, 2026
6f92eaf
fix(store): hold one removal slot per principal in the memory store
pyropy Oct 2, 2026
03daa39
fix(store): let the principal removal's and Lock's callbacks join the…
pyropy Oct 2, 2026
8facafa
test(store): follow the principal store's Tombstone and WithLock renames
pyropy Oct 5, 2026
978e983
feat(api): bind access keys to principals
pyropy Sep 10, 2026
a8d68b3
refactor(store): take the access key read lock as a functional option
pyropy Sep 30, 2026
7d8bfd4
fix(store): copy the principal into the memory access key record
pyropy Oct 1, 2026
f46d71a
docs(api): say what the key creation's share-locked reads wait on
pyropy Oct 1, 2026
b81462e
fix(store): open the access-key store's transactions through pglock.B…
pyropy Oct 2, 2026
c2716a0
feat(grant): rotate and revoke principal-bound key delegations
pyropy Sep 10, 2026
6c01b38
fix(grant): keep a rotation from re-granting a key deleted under its …
pyropy Sep 23, 2026
69b8655
fix(store): bound the memory delegation writers' lock wait
pyropy Oct 1, 2026
ee07bc3
fix(api): read the tenant key only when a deleted access key has dele…
pyropy Oct 1, 2026
9353f4b
fix(grant): bound each rotation and revocation at BatchTimeout
pyropy Oct 2, 2026
bcd89f7
fix(store): refuse a replacement filed under another audience and joi…
pyropy Oct 2, 2026
45a0a54
test(api): a key deletion that fails after the row delete leaves the …
pyropy Oct 2, 2026
0e2360e
fix(grant): bound every revocation publish made under a delegation lock
pyropy Oct 2, 2026
f09c3f0
feat(api): add principals and principal-bound access keys
pyropy Sep 22, 2026
4fe3d71
feat(api): list a principal's keys through the tenant key-list route
pyropy Sep 28, 2026
59b81d8
fix(api): strip a removed principal's policies before locking its row
pyropy Oct 1, 2026
57c95e1
fix(api): delete a removed principal's key rows under their delegatio…
pyropy Oct 1, 2026
3ef5e8c
fix(api): decode the principalId route parameter
pyropy Oct 1, 2026
33caadf
fix(api): report a principal removed as it was created as a conflict
pyropy Oct 1, 2026
c056b35
fix(api): import the access-key store once in the key handlers
pyropy Oct 1, 2026
cb228b9
fix(api): reject a principalId that is not valid UTF-8 or holds a NUL
pyropy Oct 1, 2026
80ed51f
docs(api): describe the principal removal order in the package doc
pyropy Oct 1, 2026
7bfc2e8
test(store): drop the Postgres NUL probe
pyropy Oct 1, 2026
082cf9b
fix(api): decode the tenantId route parameter, and decode a parameter…
pyropy Oct 2, 2026
029470d
fix(api): refuse a memory key for a removed principal and answer ever…
pyropy Oct 2, 2026
ce60377
feat(api): say which kind an access key is with a type field
pyropy Oct 5, 2026
351f382
refactor(api): follow the principal store's Tombstone, WithLock and f…
pyropy Oct 5, 2026
ba5cb10
feat(api): add bucket policies and the principal access reads
pyropy Sep 22, 2026
e1ee312
test(bucketpolicy): expect the policy write to lose the removal race
pyropy Sep 25, 2026
662281d
refactor(api): drop the bucket policy management routes
pyropy Sep 28, 2026
b53e9cb
test(bucketpolicy): require the policy write and the principal remova…
pyropy Oct 1, 2026
79d639b
fix(api): refuse a policy write naming a principal removed meanwhile
pyropy Oct 1, 2026
c21204e
fix(api): report a bucket gone before the policy write as not found
pyropy Oct 1, 2026
59d7c05
test(bucketpolicy): pass the principal service its delegation store a…
pyropy Oct 1, 2026
3ecb4ef
fix(api): decode the principalId parameter on the policy read routes
pyropy Oct 1, 2026
4c02e4f
test(api): drop the policy route path left behind by the management r…
pyropy Oct 1, 2026
bd9e70d
test(api): a principal added after a policy write's re-list gets the …
pyropy Oct 1, 2026
1ddd588
fix(api): name the bucketpolicy package in its doc and decode the pol…
pyropy Oct 2, 2026
9b2e53d
test(api): a policy write's rotation commits with its document, and a…
pyropy Oct 2, 2026
9533299
refactor(api): follow the principal store's WithLock and the policy s…
pyropy Oct 5, 2026
f8e080f
feat(rpc): authorize principal-bound keys against bucket policies
pyropy Sep 11, 2026
5046bcb
refactor(auth): read with the share lock as a functional option
pyropy Sep 30, 2026
76d3c2a
test(rpc): pass the principal flag in the catalog grant test
pyropy Sep 30, 2026
01953a8
test(auth): a principal-bound read during a policy write waits withou…
pyropy Sep 30, 2026
94aae06
test(rpc): pass the key kind to the deletion fixture in the policy-wr…
pyropy Oct 2, 2026
66c89d3
test(rpc): follow the principal store's Tombstone and WithLock renames
pyropy Oct 5, 2026
8ac2a4d
feat(rpc): answer /s3/bucket/info for a principal-bound key
pyropy Sep 24, 2026
d488269
fix(rpc): answer /s3/bucket/info from one policy revision
pyropy Sep 25, 2026
adba9c5
test(auth): check the policy ETag the concurrent read reports
pyropy Sep 30, 2026
d0e7c07
fix(rpc): wait out a policy write before rereading the policy etag in…
pyropy Oct 1, 2026
29425b0
fix(rpc): reread the policy etag in info after every delegation read
pyropy Oct 1, 2026
423620f
fix(rpc): refuse a bucket info whose grants moved under an unchanged …
pyropy Oct 2, 2026
5d1f5f3
test(itest): cover the tenant IAM model end to end
pyropy Sep 11, 2026
295f4fd
test(itest): write bucket policies over S3 PutBucketPolicy
pyropy Sep 28, 2026
4bd80d6
docs: name the share-lock read option
pyropy Sep 30, 2026
169811b
docs: say that a write's callback joins its transaction
pyropy Oct 2, 2026
93489ac
docs: name the write callback fn, as the stores now do
pyropy Oct 5, 2026
89a11e2
feat(rpc): store the policy a CreateBucket request carries in x-bucke…
pyropy Sep 16, 2026
2a9bec1
fix(rpc): keep a bucket whose rollback could not revoke its grants
pyropy Oct 1, 2026
2972628
fix(rpc): refuse a blank x-bucket-policy header
pyropy Oct 1, 2026
3f445e6
fix(rpc): delete a rolled-back bucket that has no grants without the …
pyropy Oct 1, 2026
17077de
fix(rpc): bound the bucket rollback's revocation publish with a deadline
pyropy Oct 1, 2026
87232f7
test(rpc): hold the create-with-policy fixture's publisher once
pyropy Oct 1, 2026
4fdd190
fix(rpc): make a create's retained bucket deletable and its rollback'…
pyropy Oct 2, 2026
efb4ea7
revert(rpc): drop the x-bucket-policy header from CreateBucket
pyropy Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 72 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,23 @@

Tenant-management service for the Forge network. Hilt owns tenants, their access
keys, and their buckets — plus the UCAN delegations and key material that back
them. It exposes two APIs and talks to one external service:
them. It exposes two APIs and talks to three external services:

- **Tenant REST API** (`pkg/api`, echo) — partner-facing CRUD for tenants and
access keys, guarded by a pre-shared partner key.
- **Tenant REST API** (`pkg/api`, echo) — partner-facing CRUD for tenants,
access keys, principals and bucket policies, guarded by a pre-shared partner
key. `POST /tenants/{id}/access-keys` creates both key kinds. Without
`principalId` it is a **service key**: it carries its own `permissions` and
`buckets`, and the tenant→access-key delegations for them are issued at
creation. With `principalId` it is a **principal-bound key**: it takes no
permissions or buckets (both stored as `NULL`) and names a principal (the
console's `principalId`). What that principal may do on a bucket is the
bucket policy's effective set for it, and the key holds, over each bucket
the principal can reach, the tenant→access-key delegations for the Forge
commands that set maps to (`pkg/grant`), rewritten on every policy change.
Each authorize evaluates the policy and re-delegates from the key to the
gateway, as a service key does. Removing a principal tombstones its row
(`deleted_at`): no read returns it, and a later `PUT` for the same id
revives it with no keys and named in no statement.
- **Hilt UCAN RPC API** (`pkg/rpc`, ucantone server mounted at `POST /`) — the
`/s3/*` commands Ingot (the S3 gateway) invokes: `/s3/request/authorize`,
`/s3/bucket/{create,delete,info,list}`; and the self-issued admin commands
Expand All @@ -14,6 +27,14 @@ them. It exposes two APIs and talks to one external service:
bucket's storage space and to manage routing policies (`pkg/client`): each
provider owns a policy whose candidates are its storage nodes, and every
bucket's space is pointed at its provider's policy on creation.
- **Swarf** (the revocation service) — Hilt calls its `/ucan/revoke` to
revoke delegations, every revocation of one write in a single request
(`PublishBatch`): a deleted key's grants, a deleted bucket's, and the grants
a policy change or a principal removal takes off a principal-bound key
(`pkg/grant`). No revocation carries a nonce, so a retry is a duplicate
Swarf records once.
- **PLC** (the did:plc directory) — Hilt creates a tenant's did:plc there on
provisioning and deactivates it on deletion (`pkg/fx/plc.go`).

Module: `github.com/fil-forge/hilt` (Go 1.27). Sibling repos it builds on:
`ucantone` (UCAN primitives: `did`, `multikey`, `ucan/delegation`, `binding`,
Expand Down Expand Up @@ -41,7 +62,16 @@ and `sprue` (the upload service; mirror its patterns where relevant).
images Docker never re-pulls — `docker pull` them when the stack misbehaves,
or override per run with `HILT_ITEST_UPLOAD_IMAGE` / `HILT_ITEST_PIRI_IMAGE`
/ `HILT_ITEST_INGOT_IMAGE` / `HILT_ITEST_SWARF_IMAGE` / `HILT_ITEST_PIRI_BINARY`
/ `HILT_ITEST_SWARF_BINARY`. CI runs the suite on
/ `HILT_ITEST_SWARF_BINARY` / `HILT_ITEST_INGOT_BINARY`. A binary override
wants a static linux build for the Docker host's architecture
(`GOOS=linux GOARCH=<host arch> CGO_ENABLED=0 GOWORK=off go build`), and is
how the IAM scenarios run against ingot changes that the `:main` image does
not carry yet. Until the published `:main` ingot image carries the IAM
changes, the IAM scenarios skip unless `HILT_ITEST_INGOT_BINARY` is set (or
`HILT_ITEST_IAM=1`); drop that guard once it does. CI runs the suite after
the unit job, on pull requests, on pushes to `main`, and on manual dispatch
(`.github/workflows/go-test.yml`). It sets neither override, so the IAM
scenarios stay skipped there until the published image carries the change.
- Editor/LSP diagnostics can lag after cross-file or cross-package edits —
`go build` / `go vet` are authoritative, prefer them over stale squiggles.

Expand All @@ -60,11 +90,20 @@ and `sprue` (the upload service; mirror its patterns where relevant).
- `pkg/sigv4` — stdlib-only SigV4 / SigV4a verification, key derivation
(`DeriveKey`), and local verification (`VerifyWithKey`).
- `pkg/s3perm` — S3-permission → Forge-command mapping (shared by `api` and `rpc`).
- `pkg/store/{tenant,accesskey,bucket,delegation,provider}` — each an interface
with `memory` and `postgres` backends.
- `pkg/bucketpolicy` — the bucket policy document and its evaluation
(`Decode`, `Validate`, `Canonical`/`ETag`, `Effective`, `Changed`); pure, no
store or transport dependencies. The package doc has the document shape.
- `pkg/grant` — an access key's stored delegations: `Issue` builds the
tenant→key set both key kinds hold, `Rotator` rewrites a principal-bound
key's set as its policies change. See the package doc.
- `pkg/store/{tenant,accesskey,bucket,delegation,provider,principal,bucketpolicy}` —
each an interface with `memory` and `postgres` backends.
- `pkg/store/pglock` — the Postgres locking helpers the stores share: bounded
lock waits, advisory locks, and the mapping to `store.ErrLockTimeout`.
- `pkg/vault` (`memory`, `openbao`) — private-key storage; `paths.go` has the
key path helpers (`TenantKeyPath`, `AccessKeyPath`).
- `pkg/client` — clients for external services (the Sprue `UploadClient`).
- `pkg/client` — clients for external services: the Sprue `UploadClient`, and
`pkg/client/management`, the partner-key REST client for the tenant API.
- `pkg/tracing` — OpenTelemetry: `Setup` installs the OTLP exporter for
`serve`; the Echo server-span middleware; `SpanNamer`, which names a UCAN
request's span for its commands; `Handler`, which gives each invocation its
Expand All @@ -82,6 +121,24 @@ and `sprue` (the upload service; mirror its patterns where relevant).
implementations kept in lockstep and exercised by one backend-parametrized test
suite (`<entity>_test.go`). Add a method to all three (interface + both backends)
and cover it in that suite.
- **Locking and callbacks**: a write that another service must learn about runs
in one transaction: lock the row (`SELECT … FOR UPDATE`), run the
caller-supplied callback (`fn`, which runs inside the write before it takes
effect), commit. The callback's ctx carries
the transaction (`pglock.WithTx`), and a store that opens its own through
`pglock.Begin` joins it, so what the callback writes commits with the
caller's write or not at all. A policy write's callback rewrites the
affected keys' delegations and publishes the revocations before anything is
stored, so a failure before the commit, a failed publish included, rolls the
write and the rotation back together and leaves the principal with its old
access, never with more; a publish that succeeded followed by a commit that
failed leaves revoked grants without a replacement, the one window the
gateway's revoked set covers (`pkg/api/service/bucketpolicy` and
`grant.Rotator` document the sequence). A reader that
must not be answered from a snapshot older than an in-flight write passes
`store.WithShareLock()` (`SELECT … FOR SHARE`). Every lock wait is bounded by
`store.LockTimeout` and a statement that gives up returns
`store.ErrLockTimeout`, which the caller retries.
- **RPC handlers** follow one shape: a `New<Cmd>Handler(logger, deps…) server.Route`
constructor that returns the libforge bound command's `.Route(...)`, whose closure
extracts `req.Invocation().Issuer()` / `req.Task().Arguments()` and delegates to an
Expand Down Expand Up @@ -111,10 +168,14 @@ and `sprue` (the upload service; mirror its patterns where relevant).
and resolves every bucket it addresses within the tenant and the key's scope. A
copy (`x-amz-copy-source` on a PUT) is two decisions: the write on the
destination and `s3:GetObject` on the source, and the header must be a signed
header. Command-specific S3-permission checks stay in each handler.
`/s3/bucket/info` carries no signed request, so it authorizes on the
invocation issuer instead: the issuer must be the provider of the bucket's
tenant, and the access key in the arguments must belong to that same tenant.
header. Command-specific S3-permission checks stay in each handler. Both key
kinds re-delegate from the key to the gateway on `/s3/request/authorize`,
each per-request delegation keyed to itself. `/s3/bucket/info` carries no
signed request, so it authorizes on the invocation issuer instead: the issuer
must be the provider of the bucket's tenant, and the access key in the
arguments must belong to that same tenant. It answers the proof chains from
the bucket root through the key's stored grants over the bucket, and a
principal-bound key's effective actions as its permissions.
- **Identities & keys**: tenants are secp256k1 → did:plc; access keys and buckets
are ed25519 → did:key. Build issuers with `multikey.NewIssuer(did, signer)`. Bucket
keys are **ephemeral** — used once to sign the bucket→tenant root delegation, then
Expand Down
14 changes: 11 additions & 3 deletions cmd/client/accesskey/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,28 @@ import (
var (
createPermissions []string
createBuckets []string
createPrincipal string
createExpiresAt string
)

var createCmd = &cobra.Command{
Use: "create <tenant-id> <name>",
Short: "Create an access key for a tenant",
Long: "Create an access key for a tenant. The response includes the secret " +
"access key — this is the only time it is exposed, so capture it now.",
Long: "Create an access key for a tenant. Without --principal it is a service " +
"key carrying the permissions and buckets given here; with --principal it is " +
"bound to that principal and takes neither, its access coming from the " +
"tenant's bucket policies. The response includes the secret access key — " +
"this is the only time it is exposed, so capture it now.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
if createPrincipal == "" && len(createPermissions) == 0 {
return fmt.Errorf("--permissions is required for a service key (or pass --principal)")
}
req := api.CreateAccessKeyRequest{
Name: args[1],
Permissions: createPermissions,
Buckets: createBuckets,
PrincipalID: createPrincipal,
}
if createExpiresAt != "" {
expires, err := time.Parse(time.RFC3339, createExpiresAt)
Expand All @@ -49,6 +57,6 @@ var createCmd = &cobra.Command{
func init() {
createCmd.Flags().StringSliceVar(&createPermissions, "permissions", nil, "S3 permissions to grant (e.g. s3:GetObject,s3:PutObject)")
createCmd.Flags().StringSliceVar(&createBuckets, "buckets", nil, "bucket DIDs the key is restricted to (default: all the tenant's buckets)")
createCmd.Flags().StringVar(&createPrincipal, "principal", "", "bind the key to this principal (principalId); the key takes no permissions or buckets")
createCmd.Flags().StringVar(&createExpiresAt, "expires-at", "", "expiry as an RFC3339 timestamp (default: never expires)")
_ = createCmd.MarkFlagRequired("permissions")
}
44 changes: 44 additions & 0 deletions cmd/client/policy/principal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package policy

import (
"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list <tenant-id> <principal-id>",
Short: "List the policies naming a principal",
Long: "List every policy of the tenant with a statement naming the principal " +
"or the wildcard.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
recs, err := c.ListPrincipalPolicies(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
return lib.PrintJSON(cmd, recs)
},
}

var accessCmd = &cobra.Command{
Use: "access <tenant-id> <principal-id>",
Short: "Show a principal's effective actions per bucket",
Long: "Show the actions the principal holds on each bucket, computed from the " +
"tenant's stored policies. Buckets it has no action on are omitted.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
buckets, err := c.GetPrincipalAccess(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
return lib.PrintJSON(cmd, buckets)
},
}
17 changes: 17 additions & 0 deletions cmd/client/policy/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Package policy provides the `hilt client policy` command tree: the two
// principal reads computed from bucket policies, via the Tenant REST API, authenticated with the partner key from config
// (auth.partner_key) or the HILT_PARTNER_KEY env var.
package policy

import "github.com/spf13/cobra"

// Cmd is the `hilt client policy` command group.
var Cmd = &cobra.Command{
Use: "policy",
Short: "Read bucket policies by principal via the Tenant REST API",
}

func init() {
Cmd.AddCommand(listCmd)
Cmd.AddCommand(accessCmd)
}
23 changes: 23 additions & 0 deletions cmd/client/principal/access_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package principal

import (
"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var listAccessKeysCmd = &cobra.Command{
Use: "access-keys <tenant-id> <principal-id>",
Short: "List the access keys bound to a principal",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
recs, err := c.ListPrincipalAccessKeys(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
return lib.PrintJSON(cmd, recs)
},
}
26 changes: 26 additions & 0 deletions cmd/client/principal/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package principal

import (
"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var createCmd = &cobra.Command{
Use: "create <tenant-id> <principal-id>",
Short: "Record a principal of a tenant",
Long: "Record a principal of a tenant. A principal holds no key material and no " +
"delegation: its access is computed from the bucket policies naming it. " +
"The call is idempotent.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
rec, err := c.CreatePrincipal(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
return lib.PrintJSON(cmd, rec)
},
}
27 changes: 27 additions & 0 deletions cmd/client/principal/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package principal

import (
"fmt"

"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var deleteCmd = &cobra.Command{
Use: "delete <tenant-id> <principal-id>",
Short: "Remove a principal from a tenant",
Long: "Remove a principal, its access to every bucket, and its access keys. " +
"The call is idempotent.",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
if err := c.DeletePrincipal(cmd.Context(), args[0], args[1]); err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "Deleted principal %s\n", args[1])
return nil
},
}
23 changes: 23 additions & 0 deletions cmd/client/principal/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package principal

import (
"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var getCmd = &cobra.Command{
Use: "get <tenant-id> <principal-id>",
Short: "Show one of a tenant's principals",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
rec, err := c.GetPrincipal(cmd.Context(), args[0], args[1])
if err != nil {
return err
}
return lib.PrintJSON(cmd, rec)
},
}
23 changes: 23 additions & 0 deletions cmd/client/principal/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package principal

import (
"github.com/fil-forge/hilt/cmd/client/lib"
"github.com/spf13/cobra"
)

var listCmd = &cobra.Command{
Use: "list <tenant-id>",
Short: "List a tenant's principals",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, _, err := lib.InitManagementClient(cmd)
if err != nil {
return err
}
recs, err := c.ListPrincipals(cmd.Context(), args[0])
if err != nil {
return err
}
return lib.PrintJSON(cmd, recs)
},
}
21 changes: 21 additions & 0 deletions cmd/client/principal/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package principal provides the `hilt client principal` command tree: partner
// operations on a tenant's principals and their access keys via the Tenant REST
// API, authenticated with the partner key from config (auth.partner_key) or the
// HILT_PARTNER_KEY env var.
package principal

import "github.com/spf13/cobra"

// Cmd is the `hilt client principal` command group.
var Cmd = &cobra.Command{
Use: "principal",
Short: "Manage a tenant's principals via the Tenant REST API",
}

func init() {
Cmd.AddCommand(createCmd)
Cmd.AddCommand(listCmd)
Cmd.AddCommand(getCmd)
Cmd.AddCommand(deleteCmd)
Cmd.AddCommand(listAccessKeysCmd)
}
4 changes: 4 additions & 0 deletions cmd/client/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package client
import (
"github.com/fil-forge/hilt/cmd/client/accesskey"
"github.com/fil-forge/hilt/cmd/client/admin"
"github.com/fil-forge/hilt/cmd/client/policy"
"github.com/fil-forge/hilt/cmd/client/principal"
"github.com/fil-forge/hilt/cmd/client/tenant"
"github.com/spf13/cobra"
)
Expand All @@ -20,4 +22,6 @@ func init() {
Cmd.AddCommand(admin.Cmd)
Cmd.AddCommand(tenant.Cmd)
Cmd.AddCommand(accesskey.Cmd)
Cmd.AddCommand(principal.Cmd)
Cmd.AddCommand(policy.Cmd)
}
Loading
Loading