Integrating with a host¶
connectforge imports no key manager, secret manager, authorization system or application framework. Your application, the host, supplies five small interfaces and a store. This page shows each.
| Port | You supply | connectforge uses it to |
|---|---|---|
Cryptographer |
Encryption under a managed key | Seal and open credentials |
Authorizer |
Your authorization | Decide whether a principal may view, use, manage, share or disconnect |
Auditor |
Your audit trail | Record each operation and each refusal |
Leaser |
Short exclusive leases | Make a refresh single-flight across processes (the SQL store provides one) |
SecretSource |
Your secret manager | Read deployment secrets such as OAuth client secrets and app keys |
Store |
Persistence | Hold connections, credentials, grants and bindings: Storage |
Cryptographer¶
type Cryptographer interface {
Seal(ctx context.Context, purpose string, plaintext, aad []byte) ([]byte, error)
Open(ctx context.Context, purpose string, ciphertext, aad []byte) ([]byte, error)
}
purpose is always connectforge.CredentialPurpose
(connectforge.credential). aad binds the ciphertext to its owner,
connection and credential version, so it cannot be moved to another record:
an implementation must fail to open a ciphertext with different aad or a
different purpose. Run cryptotest.Run against your implementation to check.
An adapter over keyforge:
type keyforgeCrypto struct{ c *keyforge.Client }
func (k keyforgeCrypto) Seal(ctx context.Context, purpose string, pt, aad []byte) ([]byte, error) {
e, err := k.c.Encrypt(ctx, keyforge.Purpose(purpose), pt, aad)
if err != nil {
return nil, err
}
return e.MarshalBinary()
}
func (k keyforgeCrypto) Open(ctx context.Context, purpose string, ct, aad []byte) ([]byte, error) {
e, err := envelope.Parse(ct)
if err != nil {
return nil, err
}
pt, _, err := k.c.Decrypt(ctx, keyforge.Purpose(purpose), e, aad)
return pt, err
}
Declare connectforge.credential as a purpose when you open the key
system. cryptotest.New() is an in-memory AES-256-GCM implementation for
tests; do not use it in production, because its key lives only in memory.
Authorizer¶
type Authorizer interface {
Allowed(ctx context.Context, p Principal, a Action, c Connection) error
}
Return nil to allow, or an error wrapping connectforge.ErrDenied to
refuse. Any other error is reported as a failure, not a refusal, so a broken
authorization backend is never mistaken for "no".
The actions are view, use, manage, share and disconnect. A natural
mapping onto a resource-role ladder is: view to view, use to edit, and
manage, share and disconnect to manage. Creating a connection is
authorized as manage on the connection about to exist; its Owner is set
and its Revision is zero, which lets you decide by organization role alone.
The principal's Kind is user, service or agent. For an agent acting on
someone's behalf the broker records OnBehalfOf; your Authorizer receives
only the principal, so decide there whether an agent needs its own grant.
Auditor¶
An event has a time, a type, the owner, the actor, the connection, the provider, success and a reason. It never carries a secret or a response body. Event types:
connection.created, connection.validated, connection.state_changed,
connection.updated, connection.disconnected, connection.deleted,
credential.replaced, credential.refreshed, credential.revoked,
grant.changed and access.denied.
Return an error if you cannot record. The service reports it as
ErrNotAudited after the change was made; it does not roll back.
SecretSource¶
For secrets that belong to the deployment rather than to a tenant. Per-tenant credentials never go through it: they are sealed and stored by connectforge.
Tenancy¶
Every record carries an Owner, an opaque string. Use your tenant identifier,
for example an organization ID, and never a name or slug that can be released
and reassigned. Every store method takes the owner and returns
ErrNotFound for a record of another owner.
With PostgreSQL you can enforce this a second time in the database with row-level security. See Storage.
Putting it together¶
store, err := sqlstore.New(db, sqlstore.Postgres, sqlstore.WithTxHook(setTenant))
if err != nil {
return err
}
if err := store.Migrate(ctx); err != nil {
return err
}
b, err := broker.New(broker.Options{
Store: store, Crypto: crypto, Providers: providers, Authorizer: authz, Auditor: audit,
Egress: egress.Policy{}, // HTTPS to each provider's hosts only
})
if err != nil {
return err
}
svc, err := service.New(service.Options{
Store: store, Broker: b, Providers: providers, Authorizer: authz, Auditor: audit,
})
SystemForge is an example of a
host: its connections package composes these ports from its key manager,
authorization, audit trail and row-level security, and serves the service as
an HTTP API.