feat: resources segregation by tenant
This commit is contained in:
parent
79f53010a0
commit
ca4211daef
13
Makefile
13
Makefile
@ -123,15 +123,18 @@ gitea-release: .mktools tools/gitea-release/bin/gitea-release.sh goreleaser chan
|
||||
tools/gitea-release/bin/gitea-release.sh
|
||||
|
||||
.emissary-token:
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer --output .emissary-token"
|
||||
$(MAKE) run-emissary-server EMISSARY_CMD="--debug --config tmp/server.yml server auth create-token --role writer --output .emissary-token --tenant '00000000-0000-0000-0000-000000000000'"
|
||||
|
||||
AGENT_ID ?= 1
|
||||
|
||||
claim-agent:
|
||||
go run ./cmd/server api agent claim --agent-thumbprint $(shell go run ./cmd/agent agent show-thumbprint)
|
||||
|
||||
load-sample-specs:
|
||||
cat misc/spec-samples/app.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
|
||||
cat misc/spec-samples/proxy.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
||||
cat misc/spec-samples/mdns.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
|
||||
cat misc/spec-samples/uci.emissary.cadoles.com.json | ./bin/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||
cat misc/spec-samples/app.emissary.cadoles.com.json | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name app.emissary.cadoles.com
|
||||
cat misc/spec-samples/proxy.emissary.cadoles.com.json | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name proxy.emissary.cadoles.com
|
||||
cat misc/spec-samples/mdns.emissary.cadoles.com.json | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name mdns.emissary.cadoles.com
|
||||
cat misc/spec-samples/uci.emissary.cadoles.com.json | go run ./cmd/server api agent spec update -a $(AGENT_ID) --no-patch --spec-data - --spec-name uci.emissary.cadoles.com
|
||||
|
||||
version: .mktools
|
||||
@echo $(MKT_PROJECT_VERSION)
|
||||
|
6
go.mod
6
go.mod
@ -28,7 +28,7 @@ require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/qri-io/jsonschema v0.2.1
|
||||
github.com/urfave/cli/v2 v2.26.0
|
||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07
|
||||
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
@ -78,7 +78,7 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
@ -123,4 +123,4 @@ require (
|
||||
)
|
||||
|
||||
// replace forge.cadoles.com/arcad/edge => ../edge
|
||||
replace github.com/allegro/bigcache/v3 v3.1.0 => github.com/Bornholm/bigcache v0.0.0-20231201111725-1ddf51584cad
|
||||
// replace gitlab.com/wpetit/goweb => ../goweb
|
||||
|
4
go.sum
4
go.sum
@ -702,6 +702,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
@ -1317,6 +1319,8 @@ github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxt
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07 h1:0V95X1cBpdj5zyOe6oGtn/BQHlRpV8WlL3eTs3jaxiA=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20231215190137-4a8add1d3d07/go.mod h1:Nfr7aZPiSN6biFumhiHbh9k8A3rKQRzR+o0bVtv78UY=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88 h1:dsyRrmhp7fl/YaY1YIzz7lm9qfIFI5KpKNbXwuhTULA=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20240226160244-6b2826c79f88/go.mod h1:bg+TN16Rq2ygLQbB4VDSHQFNouAEzcy3AAutStehllA=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
@ -34,17 +35,19 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
|
||||
token, err := jwt.Parse([]byte(rawToken), jwt.WithVerify(false))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
logger.Debug(ctx, "could not parse jwt token", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawThumbprint, exists := token.Get(keyThumbprint)
|
||||
if !exists {
|
||||
return nil, errors.Errorf("could not find '%s' claim", keyThumbprint)
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
thumbrint, ok := rawThumbprint.(string)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("unexpected '%s' claim value: '%v'", keyThumbprint, rawThumbprint)
|
||||
logger.Debug(ctx, "unexpected claim value", logger.F("claim", rawThumbprint), logger.F("value", rawThumbprint))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
agents, _, err := a.repo.Query(
|
||||
@ -57,7 +60,8 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
}
|
||||
|
||||
if len(agents) != 1 {
|
||||
return nil, errors.Errorf("unexpected number of found agents: '%d'", len(agents))
|
||||
logger.Debug(ctx, "unexpected number of found agents", logger.F("total", len(agents)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
agent, err := a.repo.Get(
|
||||
@ -75,14 +79,15 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
jwt.WithAcceptableSkew(a.acceptableSkew),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
logger.Error(ctx, "could not parse jwt", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
contactedAt := time.Now()
|
||||
|
||||
agent, err = a.repo.Update(ctx, agent.ID, datastore.WithAgentUpdateContactedAt(contactedAt))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
|
@ -16,6 +16,15 @@ func (u *User) Subject() string {
|
||||
return fmt.Sprintf("agent-%d", u.agent.ID)
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
func (u *User) Tenant() datastore.TenantID {
|
||||
if u.agent.TenantID == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return *u.agent.TenantID
|
||||
}
|
||||
|
||||
func (u *User) Agent() *datastore.Agent {
|
||||
return u.agent
|
||||
}
|
||||
|
8
internal/auth/error.go
Normal file
8
internal/auth/error.go
Normal file
@ -0,0 +1,8 @@
|
||||
package auth
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.New("unauthenticated")
|
||||
ErrUnauthorized = errors.New(("unauthorized"))
|
||||
)
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -29,10 +30,9 @@ func CtxUser(ctx context.Context) (User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
var ErrUnauthenticated = errors.New("unauthenticated")
|
||||
|
||||
type User interface {
|
||||
Subject() string
|
||||
Tenant() datastore.TenantID
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
@ -49,11 +49,12 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||
err error
|
||||
)
|
||||
|
||||
var errs []error
|
||||
|
||||
for _, auth := range authenticators {
|
||||
user, err = auth.Authenticate(ctx, r)
|
||||
if err != nil {
|
||||
logger.Debug(ctx, "could not authenticate request", logger.E(errors.WithStack(err)))
|
||||
|
||||
errs = append(errs, errors.WithStack(err))
|
||||
continue
|
||||
}
|
||||
|
||||
@ -63,9 +64,22 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
||||
isUnauthorized, isUnauthenticated, isUnknown := checkErrors(errs)
|
||||
|
||||
return
|
||||
switch {
|
||||
case isUnauthorized && !isUnknown:
|
||||
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil)
|
||||
return
|
||||
case isUnauthenticated && !isUnknown:
|
||||
api.ErrorResponse(w, http.StatusForbidden, api.ErrCodeForbidden, nil)
|
||||
return
|
||||
case isUnknown:
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)
|
||||
return
|
||||
default:
|
||||
api.ErrorResponse(w, http.StatusUnauthorized, ErrCodeUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx = logger.With(ctx, logger.F("user", user.Subject()))
|
||||
@ -77,3 +91,24 @@ func Middleware(authenticators ...Authenticator) func(http.Handler) http.Handler
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
}
|
||||
|
||||
func checkErrors(errs []error) (isUnauthorized bool, isUnauthenticated bool, isUnknown bool) {
|
||||
isUnauthenticated = false
|
||||
isUnauthorized = false
|
||||
isUnknown = false
|
||||
|
||||
for _, e := range errs {
|
||||
switch {
|
||||
case errors.Is(e, ErrUnauthorized):
|
||||
isUnauthorized = true
|
||||
continue
|
||||
case errors.Is(e, ErrUnauthenticated):
|
||||
isUnauthenticated = true
|
||||
continue
|
||||
default:
|
||||
isUnknown = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
36
internal/auth/thirdparty/authenticator.go
vendored
36
internal/auth/thirdparty/authenticator.go
vendored
@ -7,21 +7,25 @@ import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const DefaultAcceptableSkew = 5 * time.Minute
|
||||
|
||||
type (
|
||||
GetKeySet func(context.Context) (jwk.Set, error)
|
||||
GetTokenRole func(context.Context, jwt.Token) (string, error)
|
||||
GetKeySet func(context.Context) (jwk.Set, error)
|
||||
GetTokenRole func(context.Context, jwt.Token) (string, error)
|
||||
GetTokenTenant func(context.Context, jwt.Token) (string, error)
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
getKeySet GetKeySet
|
||||
getTokenRole GetTokenRole
|
||||
getTokenTenant GetTokenTenant
|
||||
acceptableSkew time.Duration
|
||||
}
|
||||
|
||||
@ -44,29 +48,45 @@ func (a *Authenticator) Authenticate(ctx context.Context, r *http.Request) (auth
|
||||
|
||||
token, err := parseToken(ctx, keys, rawToken, a.acceptableSkew)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
logger.Debug(ctx, "could not parse jwt token", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
rawRole, err := a.getTokenRole(ctx, token)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
logger.Debug(ctx, "could not retrieve token role", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
if !isValidRole(rawRole) {
|
||||
return nil, errors.Errorf("invalid role '%s'", rawRole)
|
||||
return nil, errors.WithStack(auth.ErrUnauthorized)
|
||||
}
|
||||
|
||||
rawTenantID, err := a.getTokenTenant(ctx, token)
|
||||
if err != nil {
|
||||
logger.Debug(ctx, "could not retrieve token tenant", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
tenantID, err := datastore.ParseTenantID(rawTenantID)
|
||||
if err != nil {
|
||||
logger.Debug(ctx, "could not retrieve token tenant", logger.CapturedE(errors.WithStack(err)))
|
||||
return nil, errors.WithStack(auth.ErrUnauthenticated)
|
||||
}
|
||||
|
||||
user := &User{
|
||||
subject: token.Subject(),
|
||||
role: Role(rawRole),
|
||||
subject: token.Subject(),
|
||||
role: Role(rawRole),
|
||||
tenantID: tenantID,
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func NewAuthenticator(getKeySet GetKeySet, getTokenRole GetTokenRole, acceptableSkew time.Duration) *Authenticator {
|
||||
func NewAuthenticator(getKeySet GetKeySet, getTokenRole GetTokenRole, getTokenTenant GetTokenTenant, acceptableSkew time.Duration) *Authenticator {
|
||||
return &Authenticator{
|
||||
getTokenRole: getTokenRole,
|
||||
getTokenTenant: getTokenTenant,
|
||||
getKeySet: getKeySet,
|
||||
acceptableSkew: acceptableSkew,
|
||||
}
|
||||
|
12
internal/auth/thirdparty/jwt.go
vendored
12
internal/auth/thirdparty/jwt.go
vendored
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jws"
|
||||
@ -26,9 +27,12 @@ func parseToken(ctx context.Context, keys jwk.Set, rawToken string, acceptableSk
|
||||
return token, nil
|
||||
}
|
||||
|
||||
const DefaultRoleKey string = "role"
|
||||
const (
|
||||
DefaultRoleKey string = "role"
|
||||
DefaultTenantKey string = "tenant"
|
||||
)
|
||||
|
||||
func GenerateToken(ctx context.Context, key jwk.Key, subject string, role Role) (string, error) {
|
||||
func GenerateToken(ctx context.Context, key jwk.Key, tenant datastore.TenantID, subject string, role Role) (string, error) {
|
||||
token := jwt.New()
|
||||
|
||||
if err := token.Set(jwt.SubjectKey, subject); err != nil {
|
||||
@ -39,6 +43,10 @@ func GenerateToken(ctx context.Context, key jwk.Key, subject string, role Role)
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err := token.Set(DefaultTenantKey, tenant); err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
if err := token.Set(jwt.NotBeforeKey, now); err != nil {
|
||||
|
15
internal/auth/thirdparty/user.go
vendored
15
internal/auth/thirdparty/user.go
vendored
@ -1,6 +1,9 @@
|
||||
package thirdparty
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
)
|
||||
|
||||
type Role string
|
||||
|
||||
@ -16,8 +19,9 @@ func isValidRole(r string) bool {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
subject string
|
||||
role Role
|
||||
subject string
|
||||
tenantID datastore.TenantID
|
||||
role Role
|
||||
}
|
||||
|
||||
// Subject implements auth.User
|
||||
@ -25,6 +29,11 @@ func (u *User) Subject() string {
|
||||
return u.subject
|
||||
}
|
||||
|
||||
// Tenant implements auth.User
|
||||
func (u *User) Tenant() datastore.TenantID {
|
||||
return u.tenantID
|
||||
}
|
||||
|
||||
func (u *User) Role() Role {
|
||||
return u.role
|
||||
}
|
||||
|
51
internal/command/api/agent/claim.go
Normal file
51
internal/command/api/agent/claim.go
Normal file
@ -0,0 +1,51 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func ClaimCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "claim",
|
||||
Usage: "Claim agent",
|
||||
Flags: clientFlag.ComposeFlags(
|
||||
&cli.StringFlag{
|
||||
Name: "agent-thumbprint",
|
||||
Value: "",
|
||||
Required: true,
|
||||
},
|
||||
),
|
||||
Action: func(ctx *cli.Context) error {
|
||||
baseFlags := clientFlag.GetBaseFlags(ctx)
|
||||
|
||||
token, err := clientFlag.GetToken(baseFlags)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
agentThumbprint := ctx.String("agent-thumbprint")
|
||||
|
||||
client := client.New(baseFlags.ServerURL, client.WithToken(token))
|
||||
|
||||
agent, err := client.ClaimAgent(ctx.Context, agentThumbprint)
|
||||
if err != nil {
|
||||
return errors.WithStack(apierr.Wrap(err))
|
||||
}
|
||||
|
||||
hints := agentHints(baseFlags.OutputMode)
|
||||
|
||||
if err := format.Write(baseFlags.Format, os.Stdout, hints, agent); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
@ -5,10 +5,10 @@ import (
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func CountCommand() *cli.Command {
|
||||
|
@ -7,10 +7,10 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cli.Command {
|
||||
|
@ -6,10 +6,10 @@ import (
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func GetCommand() *cli.Command {
|
||||
|
@ -6,10 +6,10 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func QueryCommand() *cli.Command {
|
||||
|
@ -15,6 +15,7 @@ func Root() *cli.Command {
|
||||
UpdateCommand(),
|
||||
GetCommand(),
|
||||
DeleteCommand(),
|
||||
ClaimCommand(),
|
||||
spec.Root(),
|
||||
},
|
||||
}
|
||||
|
@ -6,11 +6,11 @@ import (
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func DeleteCommand() *cli.Command {
|
||||
|
@ -6,10 +6,10 @@ import (
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func GetCommand() *cli.Command {
|
||||
|
@ -7,12 +7,12 @@ import (
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/spec"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
jsonpatch "github.com/evanphx/json-patch/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func UpdateCommand() *cli.Command {
|
||||
|
@ -6,10 +6,10 @@ import (
|
||||
agentFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/agent/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/apierr"
|
||||
clientFlag "forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/pkg/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
)
|
||||
|
||||
func UpdateCommand() *cli.Command {
|
||||
|
@ -1,17 +1,21 @@
|
||||
package agent
|
||||
|
||||
import "forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
import (
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||
)
|
||||
|
||||
func agentHints(outputMode format.OutputMode) format.Hints {
|
||||
return format.Hints{
|
||||
OutputMode: outputMode,
|
||||
Props: []format.Prop{
|
||||
format.NewProp("ID", "ID"),
|
||||
format.NewProp("TenantID", "Tenant", table.WithCompactModeMaxColumnWidth(8)),
|
||||
format.NewProp("Label", "Label"),
|
||||
format.NewProp("Thumbprint", "Thumbprint"),
|
||||
format.NewProp("Status", "Status"),
|
||||
format.NewProp("ContactedAt", "ContactedAt"),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt"),
|
||||
format.NewProp("ContactedAt", "ContactedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
format.NewProp("UpdatedAt", "UpdatedAt", table.WithCompactModeMaxColumnWidth(20)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,10 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format/table"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gitlab.com/wpetit/goweb/cli/format"
|
||||
"gitlab.com/wpetit/goweb/cli/format/table"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/api/flag"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/command/common"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/jwk"
|
||||
"github.com/lithammer/shortuuid/v4"
|
||||
"github.com/pkg/errors"
|
||||
@ -29,6 +30,11 @@ func CreateTokenCommand() *cli.Command {
|
||||
Usage: "associate `SUBJECT` to the token",
|
||||
Value: fmt.Sprintf("user-%s", shortuuid.New()),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tenant",
|
||||
Usage: "associate `TENANT` to the token",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
@ -44,6 +50,7 @@ func CreateTokenCommand() *cli.Command {
|
||||
}
|
||||
|
||||
subject := ctx.String("subject")
|
||||
tenant := ctx.String("tenant")
|
||||
role := ctx.String("role")
|
||||
output := ctx.String("output")
|
||||
|
||||
@ -57,7 +64,7 @@ func CreateTokenCommand() *cli.Command {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
token, err := thirdparty.GenerateToken(ctx.Context, key, subject, thirdparty.Role(role))
|
||||
token, err := thirdparty.GenerateToken(ctx.Context, key, datastore.TenantID(tenant), subject, thirdparty.Role(role))
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
@ -23,9 +23,10 @@ func NewDefaultServerConfig() ServerConfig {
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
Local *LocalAuthConfig `yaml:"local"`
|
||||
Remote *RemoteAuthConfig `yaml:"remote"`
|
||||
RoleExtractionRules []string `yaml:"roleExtractionRules"`
|
||||
Local *LocalAuthConfig `yaml:"local"`
|
||||
Remote *RemoteAuthConfig `yaml:"remote"`
|
||||
RoleExtractionRules []string `yaml:"roleExtractionRules"`
|
||||
TenantExtractionRules []string `yaml:"tenantExtractionRules"`
|
||||
}
|
||||
|
||||
func NewDefaultAuthConfig() AuthConfig {
|
||||
@ -37,6 +38,9 @@ func NewDefaultAuthConfig() AuthConfig {
|
||||
RoleExtractionRules: []string{
|
||||
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", thirdparty.DefaultRoleKey, thirdparty.DefaultRoleKey),
|
||||
},
|
||||
TenantExtractionRules: []string{
|
||||
fmt.Sprintf("jwt.%s != nil ? str(jwt.%s) : ''", thirdparty.DefaultTenantKey, thirdparty.DefaultTenantKey),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,6 +29,7 @@ type Agent struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ContactedAt *time.Time `json:"contactedAt,omitempty"`
|
||||
TenantID *TenantID `json:"tenantId"`
|
||||
}
|
||||
|
||||
type SerializableKeySet struct {
|
||||
|
@ -9,6 +9,10 @@ import (
|
||||
|
||||
type AgentRepository interface {
|
||||
Create(ctx context.Context, thumbprint string, keySet jwk.Set, metadata map[string]any) (*Agent, error)
|
||||
|
||||
Attach(ctx context.Context, tenantID TenantID, agentID AgentID) (*Agent, error)
|
||||
Detach(ctx context.Context, agentID AgentID) (*Agent, error)
|
||||
|
||||
Get(ctx context.Context, id AgentID) (*Agent, error)
|
||||
Update(ctx context.Context, id AgentID, updates ...AgentUpdateOptionFunc) (*Agent, error)
|
||||
Query(ctx context.Context, opts ...AgentQueryOptionFunc) ([]*Agent, int, error)
|
||||
@ -25,6 +29,7 @@ type AgentQueryOptions struct {
|
||||
Limit *int
|
||||
Offset *int
|
||||
IDs []AgentID
|
||||
TenantIDs []TenantID
|
||||
Thumbprints []string
|
||||
Metadata *map[string]any
|
||||
Statuses []AgentStatus
|
||||
@ -54,6 +59,12 @@ func WithAgentQueryID(ids ...AgentID) AgentQueryOptionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentQueryTenantID(ids ...TenantID) AgentQueryOptionFunc {
|
||||
return func(opts *AgentQueryOptions) {
|
||||
opts.TenantIDs = ids
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentQueryStatus(statuses ...AgentStatus) AgentQueryOptionFunc {
|
||||
return func(opts *AgentQueryOptions) {
|
||||
opts.Statuses = statuses
|
||||
@ -75,6 +86,13 @@ type AgentUpdateOptions struct {
|
||||
Metadata *map[string]any
|
||||
KeySet *jwk.Set
|
||||
Thumbprint *string
|
||||
TenantID *TenantID
|
||||
}
|
||||
|
||||
func WithAgentUpdateTenant(id TenantID) AgentUpdateOptionFunc {
|
||||
return func(opts *AgentUpdateOptions) {
|
||||
opts.TenantID = &id
|
||||
}
|
||||
}
|
||||
|
||||
func WithAgentUpdateStatus(status AgentStatus) AgentUpdateOptionFunc {
|
||||
|
@ -6,4 +6,5 @@ var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAlreadyExist = errors.New("already exist")
|
||||
ErrUnexpectedRevision = errors.New("unexpected revision")
|
||||
ErrAlreadyAttached = errors.New("already attached")
|
||||
)
|
||||
|
@ -15,6 +15,8 @@ type Spec struct {
|
||||
Revision int `json:"revision"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
TenantID TenantID `json:"tenantId"`
|
||||
AgentID AgentID `json:"agentId"`
|
||||
}
|
||||
|
||||
func (s *Spec) SpecName() spec.Name {
|
||||
|
@ -20,6 +20,75 @@ type AgentRepository struct {
|
||||
sqliteBusyRetryMaxAttempts int
|
||||
}
|
||||
|
||||
// Attach implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) Attach(ctx context.Context, tenantID datastore.TenantID, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||
var agent datastore.Agent
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT count(id), tenant_id FROM agents WHERE id = $1`
|
||||
row := tx.QueryRowContext(ctx, query, agentID)
|
||||
|
||||
var (
|
||||
count int
|
||||
attachedTenantID *datastore.TenantID
|
||||
)
|
||||
|
||||
if err := row.Scan(&count, &attachedTenantID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
return errors.WithStack(datastore.ErrNotFound)
|
||||
}
|
||||
|
||||
if attachedTenantID != nil {
|
||||
return errors.WithStack(datastore.ErrAlreadyAttached)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
query = `
|
||||
UPDATE agents SET tenant_id = $1, updated_at = $2
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||
`
|
||||
|
||||
row = tx.QueryRowContext(
|
||||
ctx, query,
|
||||
tenantID,
|
||||
now,
|
||||
)
|
||||
|
||||
metadata := JSONMap{}
|
||||
var rawKeySet []byte
|
||||
|
||||
err := row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.Metadata = metadata
|
||||
|
||||
keySet, err := jwk.Parse(rawKeySet)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
agent.KeySet = &datastore.SerializableKeySet{keySet}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
// Detach implements datastore.AgentRepository.
|
||||
func (*AgentRepository) Detach(ctx context.Context, agentID datastore.AgentID) (*datastore.Agent, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeleteSpec implements datastore.AgentRepository.
|
||||
func (r *AgentRepository) DeleteSpec(ctx context.Context, agentID datastore.AgentID, name string) error {
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
@ -170,7 +239,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
count := 0
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at FROM agents`
|
||||
query := `SELECT id, label, thumbprint, status, contacted_at, created_at, updated_at, tenant_id FROM agents`
|
||||
|
||||
limit := 10
|
||||
if options.Limit != nil {
|
||||
@ -193,6 +262,13 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
args = append(args, newArgs...)
|
||||
}
|
||||
|
||||
if options.TenantIDs != nil && len(options.TenantIDs) > 0 {
|
||||
filter, newArgs, newParamIndex := inFilter("tenant_id", paramIndex, options.TenantIDs)
|
||||
filters += filter
|
||||
paramIndex = newParamIndex
|
||||
args = append(args, newArgs...)
|
||||
}
|
||||
|
||||
if options.Thumbprints != nil && len(options.Thumbprints) > 0 {
|
||||
if filters != "" {
|
||||
filters += " AND "
|
||||
@ -240,7 +316,7 @@ func (r *AgentRepository) Query(ctx context.Context, opts ...datastore.AgentQuer
|
||||
metadata := JSONMap{}
|
||||
contactedAt := sql.NullTime{}
|
||||
|
||||
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := rows.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -293,7 +369,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
|
||||
query = `
|
||||
INSERT INTO agents (thumbprint, keyset, metadata, status, created_at, updated_at)
|
||||
VALUES($1, $2, $3, $4, $5, $5)
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at"
|
||||
RETURNING "id", "thumbprint", "keyset", "metadata", "status", "created_at", "updated_at", "tenant_id"
|
||||
`
|
||||
|
||||
rawKeySet, err := json.Marshal(keySet)
|
||||
@ -308,7 +384,7 @@ func (r *AgentRepository) Create(ctx context.Context, thumbprint string, keySet
|
||||
|
||||
metadata := JSONMap{}
|
||||
|
||||
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt)
|
||||
err = row.Scan(&agent.ID, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
@ -363,7 +439,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
|
||||
err := r.withTxRetry(ctx, func(tx *sql.Tx) error {
|
||||
query := `
|
||||
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
SELECT "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
|
||||
FROM agents
|
||||
WHERE id = $1
|
||||
`
|
||||
@ -374,7 +450,7 @@ func (r *AgentRepository) Get(ctx context.Context, id datastore.AgentID) (*datas
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -476,7 +552,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
|
||||
query += `
|
||||
WHERE id = $1
|
||||
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at"
|
||||
RETURNING "id", "label", "thumbprint", "keyset", "metadata", "status", "contacted_at", "created_at", "updated_at", "tenant_id"
|
||||
`
|
||||
|
||||
logger.Debug(ctx, "executing query", logger.F("query", query), logger.F("args", args))
|
||||
@ -487,7 +563,7 @@ func (r *AgentRepository) Update(ctx context.Context, id datastore.AgentID, opts
|
||||
contactedAt := sql.NullTime{}
|
||||
var rawKeySet []byte
|
||||
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&agent.ID, &agent.Label, &agent.Thumbprint, &rawKeySet, &metadata, &agent.Status, &contactedAt, &agent.CreatedAt, &agent.UpdatedAt, &agent.TenantID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return datastore.ErrNotFound
|
||||
}
|
||||
@ -622,23 +698,3 @@ func NewAgentRepository(db *sql.DB, sqliteBusyRetryMaxAttempts int) *AgentReposi
|
||||
}
|
||||
|
||||
var _ datastore.AgentRepository = &AgentRepository{}
|
||||
|
||||
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
|
||||
args := make([]any, 0, len(items))
|
||||
filter := fmt.Sprintf("%s in (", column)
|
||||
|
||||
for idx, item := range items {
|
||||
if idx != 0 {
|
||||
filter += ","
|
||||
}
|
||||
|
||||
filter += fmt.Sprintf("$%d", paramIndex)
|
||||
paramIndex++
|
||||
|
||||
args = append(args, item)
|
||||
}
|
||||
|
||||
filter += ")"
|
||||
|
||||
return filter, args, paramIndex
|
||||
}
|
||||
|
23
internal/datastore/sqlite/sql.go
Normal file
23
internal/datastore/sqlite/sql.go
Normal file
@ -0,0 +1,23 @@
|
||||
package sqlite
|
||||
|
||||
import "fmt"
|
||||
|
||||
func inFilter[T any](column string, paramIndex int, items []T) (string, []any, int) {
|
||||
args := make([]any, 0, len(items))
|
||||
filter := fmt.Sprintf("%s in (", column)
|
||||
|
||||
for idx, item := range items {
|
||||
if idx != 0 {
|
||||
filter += ","
|
||||
}
|
||||
|
||||
filter += fmt.Sprintf("$%d", paramIndex)
|
||||
paramIndex++
|
||||
|
||||
args = append(args, item)
|
||||
}
|
||||
|
||||
filter += ")"
|
||||
|
||||
return filter, args, paramIndex
|
||||
}
|
32
internal/datastore/tenant.go
Normal file
32
internal/datastore/tenant.go
Normal file
@ -0,0 +1,32 @@
|
||||
package datastore
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const DefaultTenantID TenantID = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
type TenantID string
|
||||
|
||||
func NewTenantID() TenantID {
|
||||
return TenantID(uuid.New().String())
|
||||
}
|
||||
|
||||
func ParseTenantID(raw string) (TenantID, error) {
|
||||
uuid, err := uuid.Parse(raw)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
return TenantID(uuid.String()), nil
|
||||
}
|
||||
|
||||
type Tenant struct {
|
||||
ID TenantID `json:"id"`
|
||||
Label string `json:"label"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const Format format.Format = "json"
|
||||
|
||||
func init() {
|
||||
format.Register(Format, NewWriter())
|
||||
}
|
||||
|
||||
type Writer struct{}
|
||||
|
||||
// Format implements format.Writer.
|
||||
func (*Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
||||
encoder := json.NewEncoder(writer)
|
||||
|
||||
if hints.OutputMode == format.OutputModeWide {
|
||||
encoder.SetIndent("", " ")
|
||||
}
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWriter() *Writer {
|
||||
return &Writer{}
|
||||
}
|
||||
|
||||
var _ format.Writer = &Writer{}
|
@ -1,18 +0,0 @@
|
||||
package format
|
||||
|
||||
type Prop struct {
|
||||
name string
|
||||
label string
|
||||
}
|
||||
|
||||
func (p *Prop) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *Prop) Label() string {
|
||||
return p.label
|
||||
}
|
||||
|
||||
func NewProp(name, label string) Prop {
|
||||
return Prop{name, label}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Format string
|
||||
|
||||
type Registry map[Format]Writer
|
||||
|
||||
var defaultRegistry = Registry{}
|
||||
|
||||
var ErrUnknownFormat = errors.New("unknown format")
|
||||
|
||||
func Write(format Format, writer io.Writer, hints Hints, data ...any) error {
|
||||
formatWriter, exists := defaultRegistry[format]
|
||||
if !exists {
|
||||
return errors.WithStack(ErrUnknownFormat)
|
||||
}
|
||||
|
||||
if hints.OutputMode == "" {
|
||||
hints.OutputMode = OutputModeCompact
|
||||
}
|
||||
|
||||
if err := formatWriter.Write(writer, hints, data...); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Available() []Format {
|
||||
formats := make([]Format, 0, len(defaultRegistry))
|
||||
|
||||
for f := range defaultRegistry {
|
||||
formats = append(formats, f)
|
||||
}
|
||||
|
||||
return formats
|
||||
}
|
||||
|
||||
func Register(format Format, writer Writer) {
|
||||
defaultRegistry[format] = writer
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func getProps(d any) []format.Prop {
|
||||
props := make([]format.Prop, 0)
|
||||
|
||||
v := reflect.Indirect(reflect.ValueOf(d))
|
||||
typeOf := v.Type()
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
name := typeOf.Field(i).Name
|
||||
props = append(props, format.NewProp(name, name))
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
func getFieldValue(obj any, name string) string {
|
||||
v := reflect.Indirect(reflect.ValueOf(obj))
|
||||
|
||||
fieldValue := v.FieldByName(name)
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Map:
|
||||
fallthrough
|
||||
case reflect.Struct:
|
||||
fallthrough
|
||||
case reflect.Slice:
|
||||
fallthrough
|
||||
case reflect.Interface:
|
||||
json, err := json.Marshal(fieldValue.Interface())
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
return string(json)
|
||||
|
||||
default:
|
||||
return fmt.Sprintf("%v", fieldValue.Interface())
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
)
|
||||
|
||||
const Format format.Format = "table"
|
||||
|
||||
const DefaultCompactModeMaxColumnWidth = 30
|
||||
|
||||
func init() {
|
||||
format.Register(Format, NewWriter(DefaultCompactModeMaxColumnWidth))
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
compactModeMaxColumnWidth int
|
||||
}
|
||||
|
||||
// Write implements format.Writer.
|
||||
func (w *Writer) Write(writer io.Writer, hints format.Hints, data ...any) error {
|
||||
t := table.NewWriter()
|
||||
|
||||
t.SetOutputMirror(writer)
|
||||
|
||||
var props []format.Prop
|
||||
|
||||
if hints.Props != nil {
|
||||
props = hints.Props
|
||||
} else {
|
||||
if len(data) > 0 {
|
||||
props = getProps(data[0])
|
||||
} else {
|
||||
props = make([]format.Prop, 0)
|
||||
}
|
||||
}
|
||||
|
||||
labels := table.Row{}
|
||||
|
||||
for _, p := range props {
|
||||
labels = append(labels, p.Label())
|
||||
}
|
||||
|
||||
t.AppendHeader(labels)
|
||||
|
||||
isCompactMode := hints.OutputMode == format.OutputModeCompact
|
||||
|
||||
for _, d := range data {
|
||||
row := table.Row{}
|
||||
|
||||
for _, p := range props {
|
||||
value := getFieldValue(d, p.Name())
|
||||
|
||||
if isCompactMode && len(value) > w.compactModeMaxColumnWidth {
|
||||
value = value[:w.compactModeMaxColumnWidth] + "..."
|
||||
}
|
||||
|
||||
row = append(row, value)
|
||||
}
|
||||
|
||||
t.AppendRow(row)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewWriter(compactModeMaxColumnWidth int) *Writer {
|
||||
return &Writer{compactModeMaxColumnWidth}
|
||||
}
|
||||
|
||||
var _ format.Writer = &Writer{}
|
@ -1,86 +0,0 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/format"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type dummyItem struct {
|
||||
MyString string
|
||||
MyInt int
|
||||
MySub subItem
|
||||
}
|
||||
|
||||
type subItem struct {
|
||||
MyBool bool
|
||||
}
|
||||
|
||||
var dummyItems = []any{
|
||||
dummyItem{
|
||||
MyString: "Foo",
|
||||
MyInt: 1,
|
||||
MySub: subItem{
|
||||
MyBool: false,
|
||||
},
|
||||
},
|
||||
dummyItem{
|
||||
MyString: "Bar",
|
||||
MyInt: 0,
|
||||
MySub: subItem{
|
||||
MyBool: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestWriterNoHints(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
||||
|
||||
if err := writer.Write(&buf, format.Hints{}, dummyItems...); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
expected := `+----------+-------+------------------+
|
||||
| MYSTRING | MYINT | MYSUB |
|
||||
+----------+-------+------------------+
|
||||
| Foo | 1 | {"MyBool":false} |
|
||||
| Bar | 0 | {"MyBool":true} |
|
||||
+----------+-------+------------------+`
|
||||
|
||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriterWithPropHints(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer := NewWriter(DefaultCompactModeMaxColumnWidth)
|
||||
|
||||
hints := format.Hints{
|
||||
Props: []format.Prop{
|
||||
format.NewProp("MyString", "MyString"),
|
||||
format.NewProp("MyInt", "MyInt"),
|
||||
},
|
||||
}
|
||||
|
||||
if err := writer.Write(&buf, hints, dummyItems...); err != nil {
|
||||
t.Fatalf("%+v", errors.WithStack(err))
|
||||
}
|
||||
|
||||
expected := `+----------+-------+
|
||||
| MYSTRING | MYINT |
|
||||
+----------+-------+
|
||||
| Foo | 1 |
|
||||
| Bar | 0 |
|
||||
+----------+-------+`
|
||||
|
||||
if e, g := strings.TrimSpace(expected), strings.TrimSpace(buf.String()); e != g {
|
||||
t.Errorf("buf.String(): expected \n%v\ngot\n%v", e, g)
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package format
|
||||
|
||||
import "io"
|
||||
|
||||
type OutputMode string
|
||||
|
||||
const (
|
||||
OutputModeWide OutputMode = "wide"
|
||||
OutputModeCompact OutputMode = "compact"
|
||||
)
|
||||
|
||||
type Hints struct {
|
||||
Props []Prop
|
||||
OutputMode OutputMode
|
||||
}
|
||||
|
||||
type Writer interface {
|
||||
Write(writer io.Writer, hints Hints, data ...any) error
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/format/json"
|
||||
_ "forge.cadoles.com/Cadoles/emissary/internal/format/table"
|
||||
_ "gitlab.com/wpetit/goweb/cli/format/json"
|
||||
_ "gitlab.com/wpetit/goweb/cli/format/table"
|
||||
)
|
||||
|
@ -20,6 +20,8 @@ const (
|
||||
ErrCodeNotFound api.ErrorCode = "not-found"
|
||||
ErrCodeInvalidSignature api.ErrorCode = "invalid-signature"
|
||||
ErrCodeConflict api.ErrorCode = "conflict"
|
||||
ErrCodeMultipleResults api.ErrorCode = "multiple-results"
|
||||
ErrCodeAlreadyClaimed api.ErrorCode = "already-claimed"
|
||||
)
|
||||
|
||||
type registerAgentRequest struct {
|
||||
@ -130,7 +132,8 @@ func (s *Server) registerAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
agent, err = s.agentRepo.Update(
|
||||
ctx, agents[0].ID,
|
||||
ctx,
|
||||
agents[0].ID,
|
||||
datastore.WithAgentUpdateKeySet(keySet),
|
||||
datastore.WithAgentUpdateMetadata(metadata),
|
||||
datastore.WithAgentUpdateThumbprint(registerAgentReq.Thumbprint),
|
||||
@ -201,6 +204,11 @@ func (s *Server) updateAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
limit, ok := getIntQueryParam(w, r, "limit", 10)
|
||||
if !ok {
|
||||
return
|
||||
@ -214,6 +222,7 @@ func (s *Server) queryAgents(w http.ResponseWriter, r *http.Request) {
|
||||
options := []datastore.AgentQueryOptionFunc{
|
||||
datastore.WithAgentQueryLimit(int(limit)),
|
||||
datastore.WithAgentQueryOffset(int(offset)),
|
||||
datastore.WithAgentQueryTenantID(user.Tenant()),
|
||||
}
|
||||
|
||||
ids, ok := getIntSliceValues(w, r, "ids", nil)
|
||||
@ -290,6 +299,10 @@ func (s *Server) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
err := s.agentRepo.Delete(
|
||||
@ -323,6 +336,10 @@ func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if ok := s.assertTenantOwns(w, r, agentID); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
agent, err := s.agentRepo.Get(
|
||||
@ -350,6 +367,73 @@ func (s *Server) getAgent(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
type claimAgentRequest struct {
|
||||
Thumbprint string `json:"thumbprint" validate:"required"`
|
||||
}
|
||||
|
||||
func (s *Server) claimAgent(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
claimAgentReq := &claimAgentRequest{}
|
||||
if ok := api.Bind(w, r, claimAgentReq); !ok {
|
||||
return
|
||||
}
|
||||
|
||||
results, _, err := s.agentRepo.Query(
|
||||
ctx,
|
||||
datastore.WithAgentQueryThumbprints(claimAgentReq.Thumbprint),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not query agents", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
api.ErrorResponse(w, http.StatusNotFound, ErrCodeNotFound, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(results) > 1 {
|
||||
logger.Error(ctx, "multiple results for agent thumbprint", logger.F("agentThumbprint", claimAgentReq.Thumbprint))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeMultipleResults, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent := results[0]
|
||||
|
||||
if agent.TenantID != nil {
|
||||
logger.Error(ctx, "agent already claimed", logger.F("agentThumbprint", claimAgentReq.Thumbprint), logger.F("agentID", agent.ID), logger.F("tenantID", agent.TenantID))
|
||||
api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyClaimed, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
agent, err = s.agentRepo.Attach(ctx, user.Tenant(), agent.ID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not attach agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
api.DataResponse(w, http.StatusOK, struct {
|
||||
Agent *datastore.Agent `json:"agent"`
|
||||
}{
|
||||
Agent: agent,
|
||||
})
|
||||
}
|
||||
|
||||
func getAgentID(w http.ResponseWriter, r *http.Request) (datastore.AgentID, bool) {
|
||||
rawAgentID := chi.URLParam(r, "agentID")
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/agent"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/auth/thirdparty"
|
||||
"forge.cadoles.com/Cadoles/emissary/internal/datastore"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/api"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -132,7 +133,7 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
if user == nil || user.Tenant() == "" {
|
||||
forbidden(w, r)
|
||||
|
||||
return nil, false
|
||||
@ -141,6 +142,30 @@ func assertRequestUser(w http.ResponseWriter, r *http.Request) (auth.User, bool)
|
||||
return user, true
|
||||
}
|
||||
|
||||
func (s *Server) assertTenantOwns(w http.ResponseWriter, r *http.Request, agentID datastore.AgentID) bool {
|
||||
ctx := r.Context()
|
||||
|
||||
user, ok := assertRequestUser(w, r)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
agent, err := s.agentRepo.Get(ctx, agentID)
|
||||
if err != nil {
|
||||
err = errors.WithStack(err)
|
||||
logger.Error(ctx, "could not get agent", logger.CapturedE(err))
|
||||
api.ErrorResponse(w, http.StatusInternalServerError, ErrCodeUnknownError, nil)
|
||||
}
|
||||
|
||||
if agent.TenantID != nil && *agent.TenantID == user.Tenant() {
|
||||
return true
|
||||
}
|
||||
|
||||
api.ErrorResponse(w, http.StatusForbidden, ErrCodeForbidden, nil)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Warn(r.Context(), "forbidden", logger.F("path", r.URL.Path))
|
||||
|
||||
|
@ -110,7 +110,10 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e
|
||||
))
|
||||
|
||||
r.Route("/agents", func(r chi.Router) {
|
||||
r.Post("/claim", s.claimAgent)
|
||||
|
||||
r.With(assertGlobalReadAccess).Get("/", s.queryAgents)
|
||||
|
||||
r.With(assertAgentReadAccess).Get("/{agentID}", s.getAgent)
|
||||
r.With(assertAgentWriteAccess).Put("/{agentID}", s.updateAgent)
|
||||
r.With(assertAgentWriteAccess).Delete("/{agentID}", s.deleteAgent)
|
||||
@ -205,18 +208,16 @@ func (s *Server) getThirdPartyAuthenticator() (*thirdparty.Authenticator, error)
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, thirdparty.DefaultAcceptableSkew), nil
|
||||
}
|
||||
|
||||
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
||||
rawRules := s.conf.Auth.RoleExtractionRules
|
||||
rules := make([]*vm.Program, 0, len(rawRules))
|
||||
|
||||
type Env struct {
|
||||
JWT map[string]any `expr:"jwt"`
|
||||
getTenantRole, err := s.createGetTokenTenantFunc()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
strFunc := expr.Function(
|
||||
return thirdparty.NewAuthenticator(getKeySet, getTokenRole, getTenantRole, thirdparty.DefaultAcceptableSkew), nil
|
||||
}
|
||||
|
||||
var ruleFuncs = []expr.Option{
|
||||
expr.Function(
|
||||
"str",
|
||||
func(params ...any) (any, error) {
|
||||
var builder strings.Builder
|
||||
@ -230,14 +231,24 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
|
||||
return builder.String(), nil
|
||||
},
|
||||
new(func(any) string),
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
||||
rawRules := s.conf.Auth.RoleExtractionRules
|
||||
rules := make([]*vm.Program, 0, len(rawRules))
|
||||
|
||||
type Env struct {
|
||||
JWT map[string]any `expr:"jwt"`
|
||||
}
|
||||
|
||||
opts := append([]expr.Option{
|
||||
expr.Env(Env{}),
|
||||
expr.AsKind(reflect.String),
|
||||
}, ruleFuncs...)
|
||||
|
||||
for _, rr := range rawRules {
|
||||
r, err := expr.Compile(rr,
|
||||
expr.Env(Env{}),
|
||||
expr.AsKind(reflect.String),
|
||||
strFunc,
|
||||
)
|
||||
r, err := expr.Compile(rr, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
|
||||
}
|
||||
@ -276,6 +287,59 @@ func (s *Server) createGetTokenRoleFunc() (func(ctx context.Context, token jwt.T
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) createGetTokenTenantFunc() (func(ctx context.Context, token jwt.Token) (string, error), error) {
|
||||
rawRules := s.conf.Auth.TenantExtractionRules
|
||||
rules := make([]*vm.Program, 0, len(rawRules))
|
||||
|
||||
type Env struct {
|
||||
JWT map[string]any `expr:"jwt"`
|
||||
}
|
||||
|
||||
opts := append([]expr.Option{
|
||||
expr.Env(Env{}),
|
||||
expr.AsKind(reflect.String),
|
||||
}, ruleFuncs...)
|
||||
|
||||
for _, rr := range rawRules {
|
||||
r, err := expr.Compile(rr, opts...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not compile role extraction rule '%s'", rr)
|
||||
}
|
||||
|
||||
rules = append(rules, r)
|
||||
}
|
||||
|
||||
return func(ctx context.Context, token jwt.Token) (string, error) {
|
||||
jwt, err := token.AsMap(ctx)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
vm := vm.VM{}
|
||||
|
||||
for _, r := range rules {
|
||||
result, err := vm.Run(r, Env{
|
||||
JWT: jwt,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
tenant, ok := result.(string)
|
||||
if !ok {
|
||||
logger.Debug(ctx, "ignoring unexpected tenant extraction result", logger.F("result", result))
|
||||
continue
|
||||
}
|
||||
|
||||
if tenant != "" {
|
||||
return tenant, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("could not extract tenant from token")
|
||||
}, nil
|
||||
}
|
||||
|
||||
func New(funcs ...OptionFunc) *Server {
|
||||
opt := defaultOption()
|
||||
for _, fn := range funcs {
|
||||
|
41
migrations/sqlite/0000003_tenant.down.sql
Normal file
41
migrations/sqlite/0000003_tenant.down.sql
Normal file
@ -0,0 +1,41 @@
|
||||
ALTER TABLE agents RENAME TO _agents;
|
||||
|
||||
CREATE TABLE agents
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
thumbprint TEXT UNIQUE,
|
||||
keyset TEXT,
|
||||
metadata TEXT,
|
||||
status INTEGER NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
label TEXT DEFAULT "",
|
||||
contacted_at datetime
|
||||
);
|
||||
|
||||
INSERT INTO agents SELECT id, thumbprint, keyset, metadata, status, created_at, updated_at, label, contacted_at FROM _agents;
|
||||
DROP TABLE _agents;
|
||||
|
||||
---
|
||||
|
||||
ALTER TABLE specs RENAME TO _specs;
|
||||
|
||||
CREATE TABLE specs
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
thumbprint TEXT UNIQUE,
|
||||
keyset TEXT,
|
||||
metadata TEXT,
|
||||
status INTEGER NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
label TEXT DEFAULT ""
|
||||
);
|
||||
|
||||
INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at FROM _specs;
|
||||
DROP TABLE _specs;
|
||||
|
||||
---
|
||||
|
||||
DROP TABLE tenants;
|
||||
|
64
migrations/sqlite/0000003_tenant.up.sql
Normal file
64
migrations/sqlite/0000003_tenant.up.sql
Normal file
@ -0,0 +1,64 @@
|
||||
CREATE TABLE tenants (
|
||||
id TEXT PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL
|
||||
);
|
||||
|
||||
-- Insert default tenant
|
||||
|
||||
INSERT INTO tenants
|
||||
( id, label, created_at, updated_at )
|
||||
VALUES (
|
||||
'00000000-0000-0000-0000-000000000000',
|
||||
'Default',
|
||||
date('now'),
|
||||
date('now')
|
||||
)
|
||||
;
|
||||
|
||||
-- Add foreign key to agents
|
||||
|
||||
ALTER TABLE agents RENAME TO _agents;
|
||||
|
||||
CREATE TABLE agents
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
thumbprint TEXT UNIQUE,
|
||||
keyset TEXT,
|
||||
metadata TEXT,
|
||||
status INTEGER NOT NULL,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
label TEXT DEFAULT "",
|
||||
contacted_at datetime,
|
||||
tenant_id TEXT,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants (id)
|
||||
);
|
||||
|
||||
INSERT INTO agents SELECT id, thumbprint, keyset, metadata, status, created_at, updated_at, label, contacted_at, 0 FROM _agents;
|
||||
|
||||
DROP TABLE _agents;
|
||||
|
||||
-- Add foreign key to specs
|
||||
|
||||
ALTER TABLE specs RENAME TO _specs;
|
||||
|
||||
CREATE TABLE specs
|
||||
(
|
||||
id INTEGER PRIMARY KEY,
|
||||
agent_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
revision INTEGER DEFAULT 0,
|
||||
data TEXT,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
tenant_id TEXT,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants (id),
|
||||
FOREIGN KEY (agent_id) REFERENCES agents (id) ON DELETE CASCADE,
|
||||
UNIQUE(agent_id, name) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
INSERT INTO specs SELECT id, agent_id, name, revision, data, created_at, updated_at, 0 FROM _specs;
|
||||
|
||||
DROP TABLE _specs;
|
27
pkg/client/claim_agent.go
Normal file
27
pkg/client/claim_agent.go
Normal file
@ -0,0 +1,27 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (c *Client) ClaimAgent(ctx context.Context, agentThumbprint string, funcs ...OptionFunc) (*Agent, error) {
|
||||
response := withResponse[struct {
|
||||
Agent *Agent `json:"agent"`
|
||||
}]()
|
||||
|
||||
payload := map[string]any{
|
||||
"thumbprint": agentThumbprint,
|
||||
}
|
||||
|
||||
if err := c.apiPost(ctx, "/api/v1/agents/claim", payload, &response, funcs...); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if response.Error != nil {
|
||||
return nil, errors.WithStack(response.Error)
|
||||
}
|
||||
|
||||
return response.Data.Agent, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user