Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file modified examples/delegated/generate.sh
100644 → 100755
Empty file.
31 changes: 31 additions & 0 deletions examples/dynamic_accounts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Dynamic Accounts (Proof of Concept)

Using a NATS resolver it is possible to create accounts on the fly to place
users. This has an interesting niche use-case. All of it can be accomplished via
a callout, so long as the callout can create an account, deploy it, and create
the user within the authentication window.

Worse that could happen is the first connection could fail, but eventually the
server would be aware of the account, and the connection would proceed.

And a [Go program](dynamic.go) that uses the library and the results of the
above script to run a service. And a client [Go Program](client/client.go)

To run, execute the script, and then

```bash
# the script will put things in /tmp/DA
nats-server -c /tmp/DA/server.conf

# in another terminal run the callout service:
go run dynamic.go -operator-key /tmp/DA/operator.nk -sys /tmp/DA/sys.creds -callout-issuer /tmp/DA/C.nk -creds /tmp/DA/service.creds

# in another terminal try the callout with the client program:
cd client
go run client.go -account-name B -creds /tmp/DA/sentinel.creds

{"server":{"name":"ND4SGVPVMOHYB3BISPYKWE3RUSILPCAFBV74AEFYFJBRWSXPN6FGKSGG","host":"0.0.0.0","id":"ND4SGVPVMOHYB3BISPYKWE3RUSILPCAFBV74AEFYFJBRWSXPN6FGKSGG","ver":"2.11.0-dev","jetstream":false,"flags":0,"seq":127,"time":"2025-02-04T20:30:09.615143Z"},"data":{"user":"B","account":"AADWDO5UGBLQT2MBC4NCUUHE34TG6KED47OHCPWWXTRHCFOKXGWTRO4F"}}
# but work for anything else
nats -s localhost:4222 --creds /tmp/DA/sentinel.creds pub hello hi
13:03:13 Published 2 bytes to "hello"
```
46 changes: 46 additions & 0 deletions examples/dynamic_accounts/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"errors"
"flag"
"fmt"
"time"

"github.com/nats-io/nats.go"
)

func getConnectionOptions(fp string) ([]nats.Option, error) {
if fp == "" {
return nil, errors.New("creds file required")
}
return []nats.Option{nats.UserCredentials(fp)}, nil
}

func main() {
// load the creds, and keys
var credsFp, accountName string
// sentinel creds
flag.StringVar(&credsFp, "creds", "./sentinel.creds", "creds file for the client")
// the account the user wants to be placed in
flag.StringVar(&accountName, "account-name", "", "account name")
flag.Parse()

// connect
opts, err := getConnectionOptions(credsFp)
opts = append(opts, nats.Token(accountName))
if err != nil {
panic(err)
}
nc, err := nats.Connect("nats://localhost:4222", opts...)
if err != nil {
panic(err)
}
defer nc.Close()

// find out where we got placed
r, err := nc.Request("$SYS.REQ.USER.INFO", nil, time.Second*2)
if err != nil {
panic(err)
}
fmt.Println(string(r.Data))
}
203 changes: 203 additions & 0 deletions examples/dynamic_accounts/dynamic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package main

import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/aricart/callout.go"
"github.com/nats-io/jwt/v2"
"github.com/nats-io/nats.go"
"github.com/nats-io/nkeys"
)

func loadAndParseKeys(fp string, kind byte) (nkeys.KeyPair, error) {
if fp == "" {
return nil, errors.New("key file required")
}
seed, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("error reading key file: %w", err)
}
if !bytes.HasPrefix(seed, []byte{'S', kind}) {
return nil, fmt.Errorf("key must be a private key")
}
kp, err := nkeys.FromSeed(seed)
if err != nil {
return nil, fmt.Errorf("error parsing key: %w", err)
}
return kp, nil
}

func getConnectionOptions(fp string) ([]nats.Option, error) {
if fp == "" {
return nil, errors.New("creds file required")
}
return []nats.Option{nats.UserCredentials(fp)}, nil
}

func UpdateAccount(nc *nats.Conn, token string) (*ResolverUpdateResponse, error) {
var r ResolverUpdateResponse
m, err := nc.Request("$SYS.REQ.CLAIMS.UPDATE", []byte(token), time.Second*2)
if err != nil {
return nil, err
}
err = json.Unmarshal(m.Data, &r)
if err != nil {
return nil, err
}
return &r, nil
}

type UpdateData struct {
Account string `json:"account"`
Code int `json:"code"`
Message string `json:"message"`
}

type ResolverResponse struct {
Error *ErrorDetails `json:"error,omitempty"`
Server ServerDetails `json:"server"`
}

type ServerDetails struct {
Name string `json:"name"`
Host string `json:"host"`
ID string `json:"id"`
Version string `json:"ver"`
Jetstream bool `json:"jetstream"`
Flags int `json:"flags"`
Sequence int `json:"seq"`
Time time.Time `json:"time"`
}

type ErrorDetails struct {
Account string `json:"account"`
Code int `json:"code"`
Description string `json:"description"`
}

type ResolverUpdateResponse struct {
ResolverResponse
UpdateData UpdateData `json:"data"`
}

func main() {
// load the creds, and keys
var credsFp, sysCreds, calloutKeyFp, operatorKeyFp string
flag.StringVar(&credsFp, "creds", "./service.creds", "creds file for the service")
flag.StringVar(&sysCreds, "sys", "./sys.creds", "system creds")
flag.StringVar(&calloutKeyFp, "callout-issuer", "./C.nk", "key for signing callout responses")
flag.StringVar(&operatorKeyFp, "operator-key", "./operator.nk", "key for creating accounts")
flag.Parse()

okp, err := loadAndParseKeys(operatorKeyFp, 'O')
if err != nil {
panic(err)
}

sysOpts, err := getConnectionOptions(sysCreds)
if err != nil {
panic(err)
}
sys, err := nats.Connect("nats://localhost:4222", sysOpts...)
if err != nil {
panic(err)
}

_, _ = sys.Subscribe("$SYS.REQ.ACCOUNT.*.CLAIMS.LOOKUP", func(m *nats.Msg) {
chunks := strings.Split(m.Subject, ".")
id := chunks[3]
fmt.Println(id)
})

// this creates a new account named as specified returning
// the key used to sign users
createAccount := func(name string) (nkeys.KeyPair, error) {
kp, err := nkeys.CreateAccount()
if err != nil {
return nil, err
}
pk, err := kp.PublicKey()
if err != nil {
return nil, err
}
ac := jwt.NewAccountClaims(pk)
ac.Name = name
token, err := ac.Encode(okp)
if err != nil {
return nil, err
}
r, err := UpdateAccount(sys, token)
if err != nil {
return nil, err
}
// verify that the update worked
if r.UpdateData.Code != 200 {
return nil, fmt.Errorf("error creating account: %s", r.Error.Description)
}
return kp, nil
}

// keep a map of account names to keys - this would likely need to be more
// sophisticated, and be persistent. Likely some sort of cleanup logic would
// have to be added to delete (set connections to 0) and possibly remove from
// resolver once accounts are vacated - this is an exercise for the reader.
accounts := make(map[string]nkeys.KeyPair)

// load the callout key
cKP, err := loadAndParseKeys(calloutKeyFp, 'A')
if err != nil {
panic(fmt.Errorf("error loading callout issuer: %w", err))
}

// the authorizer function
authorizer := func(req *jwt.AuthorizationRequest) (string, error) {
// reading the account name from the token, likely this will be
// encoded string with more information
accountName := req.ConnectOptions.Token
if accountName == "" {
// fail
return "", errors.New("no account name")
}
// see if we have this account
akp, ok := accounts[accountName]
if !ok {
// create it and push it
akp, err = createAccount(accountName)
if err != nil {
return "", err
}
accounts[accountName] = akp
}
// issue the user
uc := jwt.NewUserClaims(req.UserNkey)
return uc.Encode(akp)
}

// connect the service with the creds
opts, err := getConnectionOptions(credsFp)
if err != nil {
panic(fmt.Errorf("error loading creds: %w", err))
}
nc, err := nats.Connect("nats://localhost:4222", opts...)
if err != nil {
panic(fmt.Errorf("error connecting: %w", err))
}
defer nc.Close()

// start the service
_, err = callout.NewAuthorizationService(nc, callout.Authorizer(authorizer), callout.ResponseSignerKey(cKP))

// don't exit until sigterm
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
}
48 changes: 48 additions & 0 deletions examples/dynamic_accounts/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -x

# put the nsc artifacts where we can find them
export TMPDIR=/tmp
export OUTDIR=$TMPDIR/DA
export XDG_CONFIG_HOME=$OUTDIR/config
export XDG_DATA_HOME=$OUTDIR/data


# add an operator
nsc add operator O
#nsc edit operator --account-jwt-server-url nats://localhost:4222
nsc export keys --operator --dir $OUTDIR
OPERATOR=$(nsc describe operator --json | jq .sub -r)
mv "$OUTDIR/$OPERATOR.nk" $OUTDIR/operator.nk

# add and register the system account
nsc add account SYS
nsc edit operator --system-account SYS
nsc add user --account SYS --name sys
nsc generate creds --account SYS --name sys -o $OUTDIR/sys.creds

# add the callout account
nsc add account C
# capture the ID (subject) for the callout account
CALLOUT=$(nsc describe account C --json | jq .sub -r)
cp "$XDG_DATA_HOME/nats/nsc/keys/keys/A/${CALLOUT:1:2}/${CALLOUT}.nk" $OUTDIR/C.nk
# add the service user, this user is for the callout service to connect to NATS
nsc add user service
SERVICE=$(nsc describe user service --json | jq .sub -r)
# add the sentinel users (no permissions) this is a callout user that will be given
# to all clients to authorize via the callout
nsc add user --account C --name sentinel --deny-pubsub \>
# the callout account needs to specify the ID of the service user, and the accounts
# that it can generate authorizations for
nsc edit authcallout --account C --auth-user $SERVICE --allowed-account "*"

# make a server configuration file
nsc generate config --nats-resolver --config-file /tmp/DA/server.conf
# extract the creds for the service and callout so we can use them
nsc generate creds --account C --name service -o $OUTDIR/service.creds
nsc generate creds --account C --name sentinel -o $OUTDIR/sentinel.creds

mkdir -p $OUTDIR/jwt
nsc describe account C --raw > "$OUTDIR/jwt/$CALLOUT.jwt"