Allow bouncers to share API keys (#3323)

This commit is contained in:
blotus 2024-11-19 14:47:59 +01:00 committed by GitHub
parent 36e2c6c8be
commit fb733ee43a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 392 additions and 42 deletions

View file

@ -24,7 +24,7 @@ func (cli *cliBouncers) add(ctx context.Context, bouncerName string, key string)
}
}
_, err = cli.db.CreateBouncer(ctx, bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
_, err = cli.db.CreateBouncer(ctx, bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType, false)
if err != nil {
return fmt.Errorf("unable to create bouncer: %w", err)
}

View file

@ -77,6 +77,7 @@ type bouncerInfo struct {
AuthType string `json:"auth_type"`
OS string `json:"os,omitempty"`
Featureflags []string `json:"featureflags,omitempty"`
AutoCreated bool `json:"auto_created"`
}
func newBouncerInfo(b *ent.Bouncer) bouncerInfo {
@ -92,6 +93,7 @@ func newBouncerInfo(b *ent.Bouncer) bouncerInfo {
AuthType: b.AuthType,
OS: clientinfo.GetOSNameAndVersion(b),
Featureflags: clientinfo.GetFeatureFlagList(b),
AutoCreated: b.AutoCreated,
}
}

View file

@ -4,25 +4,73 @@ import (
"context"
"errors"
"fmt"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
func (cli *cliBouncers) delete(ctx context.Context, bouncers []string, ignoreMissing bool) error {
for _, bouncerID := range bouncers {
if err := cli.db.DeleteBouncer(ctx, bouncerID); err != nil {
var notFoundErr *database.BouncerNotFoundError
if ignoreMissing && errors.As(err, &notFoundErr) {
return nil
}
func (cli *cliBouncers) findParentBouncer(bouncerName string, bouncers []*ent.Bouncer) (string, error) {
bouncerPrefix := strings.Split(bouncerName, "@")[0]
for _, bouncer := range bouncers {
if strings.HasPrefix(bouncer.Name, bouncerPrefix) && !bouncer.AutoCreated {
return bouncer.Name, nil
}
}
return fmt.Errorf("unable to delete bouncer: %w", err)
return "", errors.New("no parent bouncer found")
}
func (cli *cliBouncers) delete(ctx context.Context, bouncers []string, ignoreMissing bool) error {
allBouncers, err := cli.db.ListBouncers(ctx)
if err != nil {
return fmt.Errorf("unable to list bouncers: %w", err)
}
for _, bouncerName := range bouncers {
bouncer, err := cli.db.SelectBouncerByName(ctx, bouncerName)
if err != nil {
var notFoundErr *ent.NotFoundError
if ignoreMissing && errors.As(err, &notFoundErr) {
continue
}
return fmt.Errorf("unable to delete bouncer %s: %w", bouncerName, err)
}
log.Infof("bouncer '%s' deleted successfully", bouncerID)
// For TLS bouncers, always delete them, they have no parents
if bouncer.AuthType == types.TlsAuthType {
if err := cli.db.DeleteBouncer(ctx, bouncerName); err != nil {
return fmt.Errorf("unable to delete bouncer %s: %w", bouncerName, err)
}
continue
}
if bouncer.AutoCreated {
parentBouncer, err := cli.findParentBouncer(bouncerName, allBouncers)
if err != nil {
log.Errorf("bouncer '%s' is auto-created, but couldn't find a parent bouncer", err)
continue
}
log.Warnf("bouncer '%s' is auto-created and cannot be deleted, delete parent bouncer %s instead", bouncerName, parentBouncer)
continue
}
//Try to find all child bouncers and delete them
for _, childBouncer := range allBouncers {
if strings.HasPrefix(childBouncer.Name, bouncerName+"@") && childBouncer.AutoCreated {
if err := cli.db.DeleteBouncer(ctx, childBouncer.Name); err != nil {
return fmt.Errorf("unable to delete bouncer %s: %w", childBouncer.Name, err)
}
log.Infof("bouncer '%s' deleted successfully", childBouncer.Name)
}
}
if err := cli.db.DeleteBouncer(ctx, bouncerName); err != nil {
return fmt.Errorf("unable to delete bouncer %s: %w", bouncerName, err)
}
log.Infof("bouncer '%s' deleted successfully", bouncerName)
}
return nil

View file

@ -40,6 +40,7 @@ func (cli *cliBouncers) inspectHuman(out io.Writer, bouncer *ent.Bouncer) {
{"Last Pull", lastPull},
{"Auth type", bouncer.AuthType},
{"OS", clientinfo.GetOSNameAndVersion(bouncer)},
{"Auto Created", bouncer.AutoCreated},
})
for _, ff := range clientinfo.GetFeatureFlagList(bouncer) {

View file

@ -59,6 +59,9 @@ func (l *LAPI) RecordResponse(t *testing.T, ctx context.Context, verb string, ur
t.Fatal("auth type not supported")
}
// Port is required for gin to properly parse the client IP
req.RemoteAddr = "127.0.0.1:1234"
l.router.ServeHTTP(w, req)
return w

View file

@ -20,28 +20,74 @@ func TestAPIKey(t *testing.T) {
w := httptest.NewRecorder()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.RemoteAddr = "127.0.0.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code)
assert.Equal(t, `{"message":"access forbidden"}`, w.Body.String())
assert.Equal(t, http.StatusForbidden, w.Code)
assert.JSONEq(t, `{"message":"access forbidden"}`, w.Body.String())
// Login with invalid token
w = httptest.NewRecorder()
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", "a1b2c3d4e5f6")
req.RemoteAddr = "127.0.0.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code)
assert.Equal(t, `{"message":"access forbidden"}`, w.Body.String())
assert.Equal(t, http.StatusForbidden, w.Code)
assert.JSONEq(t, `{"message":"access forbidden"}`, w.Body.String())
// Login with valid token
w = httptest.NewRecorder()
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
req.RemoteAddr = "127.0.0.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "null", w.Body.String())
// Login with valid token from another IP
w = httptest.NewRecorder()
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
req.RemoteAddr = "4.3.2.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "null", w.Body.String())
// Make the requests multiple times to make sure we only create one
w = httptest.NewRecorder()
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
req.RemoteAddr = "4.3.2.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "null", w.Body.String())
// Use the original bouncer again
w = httptest.NewRecorder()
req, _ = http.NewRequestWithContext(ctx, http.MethodGet, "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
req.RemoteAddr = "127.0.0.1:1234"
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "null", w.Body.String())
// Check if our second bouncer was properly created
bouncers := GetBouncers(t, config.API.Server.DbConfig)
assert.Len(t, bouncers, 2)
assert.Equal(t, "test@4.3.2.1", bouncers[1].Name)
assert.Equal(t, bouncers[0].APIKey, bouncers[1].APIKey)
assert.Equal(t, bouncers[0].AuthType, bouncers[1].AuthType)
assert.False(t, bouncers[0].AutoCreated)
assert.True(t, bouncers[1].AutoCreated)
}

View file

@ -24,6 +24,7 @@ import (
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -62,6 +63,7 @@ func LoadTestConfig(t *testing.T) csconfig.Config {
}
apiServerConfig := csconfig.LocalApiServerCfg{
ListenURI: "http://127.0.0.1:8080",
LogLevel: ptr.Of(log.DebugLevel),
DbConfig: &dbconfig,
ProfilesPath: "./tests/profiles.yaml",
ConsoleConfig: &csconfig.ConsoleConfig{
@ -206,6 +208,18 @@ func GetMachineIP(t *testing.T, machineID string, config *csconfig.DatabaseCfg)
return ""
}
func GetBouncers(t *testing.T, config *csconfig.DatabaseCfg) []*ent.Bouncer {
ctx := context.Background()
dbClient, err := database.NewClient(ctx, config)
require.NoError(t, err)
bouncers, err := dbClient.ListBouncers(ctx)
require.NoError(t, err)
return bouncers
}
func GetAlertReaderFromFile(t *testing.T, path string) *strings.Reader {
alertContentBytes, err := os.ReadFile(path)
require.NoError(t, err)
@ -290,7 +304,7 @@ func CreateTestBouncer(t *testing.T, ctx context.Context, config *csconfig.Datab
apiKey, err := middlewares.GenerateAPIKey(keyLength)
require.NoError(t, err)
_, err = dbClient.CreateBouncer(ctx, "test", "127.0.0.1", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType)
_, err = dbClient.CreateBouncer(ctx, "test", "127.0.0.1", middlewares.HashSHA512(apiKey), types.ApiKeyAuthType, false)
require.NoError(t, err)
return apiKey

View file

@ -89,7 +89,7 @@ func (a *APIKey) authTLS(c *gin.Context, logger *log.Entry) *ent.Bouncer {
logger.Infof("Creating bouncer %s", bouncerName)
bouncer, err = a.DbClient.CreateBouncer(ctx, bouncerName, c.ClientIP(), HashSHA512(apiKey), types.TlsAuthType)
bouncer, err = a.DbClient.CreateBouncer(ctx, bouncerName, c.ClientIP(), HashSHA512(apiKey), types.TlsAuthType, true)
if err != nil {
logger.Errorf("while creating bouncer db entry: %s", err)
return nil
@ -114,18 +114,69 @@ func (a *APIKey) authPlain(c *gin.Context, logger *log.Entry) *ent.Bouncer {
return nil
}
clientIP := c.ClientIP()
ctx := c.Request.Context()
hashStr := HashSHA512(val[0])
bouncer, err := a.DbClient.SelectBouncer(ctx, hashStr)
// Appsec case, we only care if the key is valid
// No content is returned, no last_pull update or anything
if c.Request.Method == http.MethodHead {
bouncer, err := a.DbClient.SelectBouncers(ctx, hashStr, types.ApiKeyAuthType)
if err != nil {
logger.Errorf("while fetching bouncer info: %s", err)
return nil
}
return bouncer[0]
}
// most common case, check if this specific bouncer exists
bouncer, err := a.DbClient.SelectBouncerWithIP(ctx, hashStr, clientIP)
if err != nil && !ent.IsNotFound(err) {
logger.Errorf("while fetching bouncer info: %s", err)
return nil
}
// We found the bouncer with key and IP, we can use it
if bouncer != nil {
if bouncer.AuthType != types.ApiKeyAuthType {
logger.Errorf("bouncer isn't allowed to auth by API key")
return nil
}
return bouncer
}
// We didn't find the bouncer with key and IP, let's try to find it with the key only
bouncers, err := a.DbClient.SelectBouncers(ctx, hashStr, types.ApiKeyAuthType)
if err != nil {
logger.Errorf("while fetching bouncer info: %s", err)
return nil
}
if bouncer.AuthType != types.ApiKeyAuthType {
logger.Errorf("bouncer %s attempted to login using an API key but it is configured to auth with %s", bouncer.Name, bouncer.AuthType)
if len(bouncers) == 0 {
logger.Debugf("no bouncer found with this key")
return nil
}
logger.Debugf("found %d bouncers with this key", len(bouncers))
// We only have one bouncer with this key and no IP
// This is the first request made by this bouncer, keep this one
if len(bouncers) == 1 && bouncers[0].IPAddress == "" {
return bouncers[0]
}
// Bouncers are ordered by ID, first one *should* be the manually created one
// Can probably get a bit weird if the user deletes the manually created one
bouncerName := fmt.Sprintf("%s@%s", bouncers[0].Name, clientIP)
logger.Infof("Creating bouncer %s", bouncerName)
bouncer, err = a.DbClient.CreateBouncer(ctx, bouncerName, clientIP, hashStr, types.ApiKeyAuthType, true)
if err != nil {
logger.Errorf("while creating bouncer db entry: %s", err)
return nil
}
@ -156,27 +207,20 @@ func (a *APIKey) MiddlewareFunc() gin.HandlerFunc {
return
}
// Appsec request, return immediately if we found something
if c.Request.Method == http.MethodHead {
c.Set(BouncerContextKey, bouncer)
return
}
logger = logger.WithField("name", bouncer.Name)
// 1st time we see this bouncer, we update its IP
if bouncer.IPAddress == "" {
if err := a.DbClient.UpdateBouncerIP(ctx, clientIP, bouncer.ID); err != nil {
logger.Errorf("Failed to update ip address for '%s': %s\n", bouncer.Name, err)
c.JSON(http.StatusForbidden, gin.H{"message": "access forbidden"})
c.Abort()
return
}
}
// Don't update IP on HEAD request, as it's used by the appsec to check the validity of the API key provided
if bouncer.IPAddress != clientIP && bouncer.IPAddress != "" && c.Request.Method != http.MethodHead {
log.Warningf("new IP address detected for bouncer '%s': %s (old: %s)", bouncer.Name, clientIP, bouncer.IPAddress)
if err := a.DbClient.UpdateBouncerIP(ctx, clientIP, bouncer.ID); err != nil {
logger.Errorf("Failed to update ip address for '%s': %s\n", bouncer.Name, err)
c.JSON(http.StatusForbidden, gin.H{"message": "access forbidden"})
c.Abort()
return
}
}

View file

@ -41,8 +41,19 @@ func (c *Client) BouncerUpdateBaseMetrics(ctx context.Context, bouncerName strin
return nil
}
func (c *Client) SelectBouncer(ctx context.Context, apiKeyHash string) (*ent.Bouncer, error) {
result, err := c.Ent.Bouncer.Query().Where(bouncer.APIKeyEQ(apiKeyHash)).First(ctx)
func (c *Client) SelectBouncers(ctx context.Context, apiKeyHash string, authType string) ([]*ent.Bouncer, error) {
//Order by ID so manually created bouncer will be first in the list to use as the base name
//when automatically creating a new entry if API keys are shared
result, err := c.Ent.Bouncer.Query().Where(bouncer.APIKeyEQ(apiKeyHash), bouncer.AuthTypeEQ(authType)).Order(ent.Asc(bouncer.FieldID)).All(ctx)
if err != nil {
return nil, err
}
return result, nil
}
func (c *Client) SelectBouncerWithIP(ctx context.Context, apiKeyHash string, clientIP string) (*ent.Bouncer, error) {
result, err := c.Ent.Bouncer.Query().Where(bouncer.APIKeyEQ(apiKeyHash), bouncer.IPAddressEQ(clientIP)).First(ctx)
if err != nil {
return nil, err
}
@ -68,13 +79,15 @@ func (c *Client) ListBouncers(ctx context.Context) ([]*ent.Bouncer, error) {
return result, nil
}
func (c *Client) CreateBouncer(ctx context.Context, name string, ipAddr string, apiKey string, authType string) (*ent.Bouncer, error) {
func (c *Client) CreateBouncer(ctx context.Context, name string, ipAddr string, apiKey string, authType string, autoCreated bool) (*ent.Bouncer, error) {
bouncer, err := c.Ent.Bouncer.
Create().
SetName(name).
SetAPIKey(apiKey).
SetRevoked(false).
SetAuthType(authType).
SetIPAddress(ipAddr).
SetAutoCreated(autoCreated).
Save(ctx)
if err != nil {
if ent.IsConstraintError(err) {

View file

@ -43,6 +43,8 @@ type Bouncer struct {
Osversion string `json:"osversion,omitempty"`
// Featureflags holds the value of the "featureflags" field.
Featureflags string `json:"featureflags,omitempty"`
// AutoCreated holds the value of the "auto_created" field.
AutoCreated bool `json:"auto_created"`
selectValues sql.SelectValues
}
@ -51,7 +53,7 @@ func (*Bouncer) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case bouncer.FieldRevoked:
case bouncer.FieldRevoked, bouncer.FieldAutoCreated:
values[i] = new(sql.NullBool)
case bouncer.FieldID:
values[i] = new(sql.NullInt64)
@ -159,6 +161,12 @@ func (b *Bouncer) assignValues(columns []string, values []any) error {
} else if value.Valid {
b.Featureflags = value.String
}
case bouncer.FieldAutoCreated:
if value, ok := values[i].(*sql.NullBool); !ok {
return fmt.Errorf("unexpected type %T for field auto_created", values[i])
} else if value.Valid {
b.AutoCreated = value.Bool
}
default:
b.selectValues.Set(columns[i], values[i])
}
@ -234,6 +242,9 @@ func (b *Bouncer) String() string {
builder.WriteString(", ")
builder.WriteString("featureflags=")
builder.WriteString(b.Featureflags)
builder.WriteString(", ")
builder.WriteString("auto_created=")
builder.WriteString(fmt.Sprintf("%v", b.AutoCreated))
builder.WriteByte(')')
return builder.String()
}

View file

@ -39,6 +39,8 @@ const (
FieldOsversion = "osversion"
// FieldFeatureflags holds the string denoting the featureflags field in the database.
FieldFeatureflags = "featureflags"
// FieldAutoCreated holds the string denoting the auto_created field in the database.
FieldAutoCreated = "auto_created"
// Table holds the table name of the bouncer in the database.
Table = "bouncers"
)
@ -59,6 +61,7 @@ var Columns = []string{
FieldOsname,
FieldOsversion,
FieldFeatureflags,
FieldAutoCreated,
}
// ValidColumn reports if the column name is valid (part of the table columns).
@ -82,6 +85,8 @@ var (
DefaultIPAddress string
// DefaultAuthType holds the default value on creation for the "auth_type" field.
DefaultAuthType string
// DefaultAutoCreated holds the default value on creation for the "auto_created" field.
DefaultAutoCreated bool
)
// OrderOption defines the ordering options for the Bouncer queries.
@ -156,3 +161,8 @@ func ByOsversion(opts ...sql.OrderTermOption) OrderOption {
func ByFeatureflags(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldFeatureflags, opts...).ToFunc()
}
// ByAutoCreated orders the results by the auto_created field.
func ByAutoCreated(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldAutoCreated, opts...).ToFunc()
}

View file

@ -119,6 +119,11 @@ func Featureflags(v string) predicate.Bouncer {
return predicate.Bouncer(sql.FieldEQ(FieldFeatureflags, v))
}
// AutoCreated applies equality check predicate on the "auto_created" field. It's identical to AutoCreatedEQ.
func AutoCreated(v bool) predicate.Bouncer {
return predicate.Bouncer(sql.FieldEQ(FieldAutoCreated, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.Bouncer {
return predicate.Bouncer(sql.FieldEQ(FieldCreatedAt, v))
@ -904,6 +909,16 @@ func FeatureflagsContainsFold(v string) predicate.Bouncer {
return predicate.Bouncer(sql.FieldContainsFold(FieldFeatureflags, v))
}
// AutoCreatedEQ applies the EQ predicate on the "auto_created" field.
func AutoCreatedEQ(v bool) predicate.Bouncer {
return predicate.Bouncer(sql.FieldEQ(FieldAutoCreated, v))
}
// AutoCreatedNEQ applies the NEQ predicate on the "auto_created" field.
func AutoCreatedNEQ(v bool) predicate.Bouncer {
return predicate.Bouncer(sql.FieldNEQ(FieldAutoCreated, v))
}
// And groups predicates with the AND operator between them.
func And(predicates ...predicate.Bouncer) predicate.Bouncer {
return predicate.Bouncer(sql.AndPredicates(predicates...))

View file

@ -178,6 +178,20 @@ func (bc *BouncerCreate) SetNillableFeatureflags(s *string) *BouncerCreate {
return bc
}
// SetAutoCreated sets the "auto_created" field.
func (bc *BouncerCreate) SetAutoCreated(b bool) *BouncerCreate {
bc.mutation.SetAutoCreated(b)
return bc
}
// SetNillableAutoCreated sets the "auto_created" field if the given value is not nil.
func (bc *BouncerCreate) SetNillableAutoCreated(b *bool) *BouncerCreate {
if b != nil {
bc.SetAutoCreated(*b)
}
return bc
}
// Mutation returns the BouncerMutation object of the builder.
func (bc *BouncerCreate) Mutation() *BouncerMutation {
return bc.mutation
@ -229,6 +243,10 @@ func (bc *BouncerCreate) defaults() {
v := bouncer.DefaultAuthType
bc.mutation.SetAuthType(v)
}
if _, ok := bc.mutation.AutoCreated(); !ok {
v := bouncer.DefaultAutoCreated
bc.mutation.SetAutoCreated(v)
}
}
// check runs all checks and user-defined validators on the builder.
@ -251,6 +269,9 @@ func (bc *BouncerCreate) check() error {
if _, ok := bc.mutation.AuthType(); !ok {
return &ValidationError{Name: "auth_type", err: errors.New(`ent: missing required field "Bouncer.auth_type"`)}
}
if _, ok := bc.mutation.AutoCreated(); !ok {
return &ValidationError{Name: "auto_created", err: errors.New(`ent: missing required field "Bouncer.auto_created"`)}
}
return nil
}
@ -329,6 +350,10 @@ func (bc *BouncerCreate) createSpec() (*Bouncer, *sqlgraph.CreateSpec) {
_spec.SetField(bouncer.FieldFeatureflags, field.TypeString, value)
_node.Featureflags = value
}
if value, ok := bc.mutation.AutoCreated(); ok {
_spec.SetField(bouncer.FieldAutoCreated, field.TypeBool, value)
_node.AutoCreated = value
}
return _node, _spec
}

View file

@ -74,6 +74,7 @@ var (
{Name: "osname", Type: field.TypeString, Nullable: true},
{Name: "osversion", Type: field.TypeString, Nullable: true},
{Name: "featureflags", Type: field.TypeString, Nullable: true},
{Name: "auto_created", Type: field.TypeBool, Default: false},
}
// BouncersTable holds the schema information for the "bouncers" table.
BouncersTable = &schema.Table{

View file

@ -2471,6 +2471,7 @@ type BouncerMutation struct {
osname *string
osversion *string
featureflags *string
auto_created *bool
clearedFields map[string]struct{}
done bool
oldValue func(context.Context) (*Bouncer, error)
@ -3134,6 +3135,42 @@ func (m *BouncerMutation) ResetFeatureflags() {
delete(m.clearedFields, bouncer.FieldFeatureflags)
}
// SetAutoCreated sets the "auto_created" field.
func (m *BouncerMutation) SetAutoCreated(b bool) {
m.auto_created = &b
}
// AutoCreated returns the value of the "auto_created" field in the mutation.
func (m *BouncerMutation) AutoCreated() (r bool, exists bool) {
v := m.auto_created
if v == nil {
return
}
return *v, true
}
// OldAutoCreated returns the old "auto_created" field's value of the Bouncer entity.
// If the Bouncer object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *BouncerMutation) OldAutoCreated(ctx context.Context) (v bool, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldAutoCreated is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldAutoCreated requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldAutoCreated: %w", err)
}
return oldValue.AutoCreated, nil
}
// ResetAutoCreated resets all changes to the "auto_created" field.
func (m *BouncerMutation) ResetAutoCreated() {
m.auto_created = nil
}
// Where appends a list predicates to the BouncerMutation builder.
func (m *BouncerMutation) Where(ps ...predicate.Bouncer) {
m.predicates = append(m.predicates, ps...)
@ -3168,7 +3205,7 @@ func (m *BouncerMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *BouncerMutation) Fields() []string {
fields := make([]string, 0, 13)
fields := make([]string, 0, 14)
if m.created_at != nil {
fields = append(fields, bouncer.FieldCreatedAt)
}
@ -3208,6 +3245,9 @@ func (m *BouncerMutation) Fields() []string {
if m.featureflags != nil {
fields = append(fields, bouncer.FieldFeatureflags)
}
if m.auto_created != nil {
fields = append(fields, bouncer.FieldAutoCreated)
}
return fields
}
@ -3242,6 +3282,8 @@ func (m *BouncerMutation) Field(name string) (ent.Value, bool) {
return m.Osversion()
case bouncer.FieldFeatureflags:
return m.Featureflags()
case bouncer.FieldAutoCreated:
return m.AutoCreated()
}
return nil, false
}
@ -3277,6 +3319,8 @@ func (m *BouncerMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldOsversion(ctx)
case bouncer.FieldFeatureflags:
return m.OldFeatureflags(ctx)
case bouncer.FieldAutoCreated:
return m.OldAutoCreated(ctx)
}
return nil, fmt.Errorf("unknown Bouncer field %s", name)
}
@ -3377,6 +3421,13 @@ func (m *BouncerMutation) SetField(name string, value ent.Value) error {
}
m.SetFeatureflags(v)
return nil
case bouncer.FieldAutoCreated:
v, ok := value.(bool)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetAutoCreated(v)
return nil
}
return fmt.Errorf("unknown Bouncer field %s", name)
}
@ -3510,6 +3561,9 @@ func (m *BouncerMutation) ResetField(name string) error {
case bouncer.FieldFeatureflags:
m.ResetFeatureflags()
return nil
case bouncer.FieldAutoCreated:
m.ResetAutoCreated()
return nil
}
return fmt.Errorf("unknown Bouncer field %s", name)
}

View file

@ -76,6 +76,10 @@ func init() {
bouncerDescAuthType := bouncerFields[9].Descriptor()
// bouncer.DefaultAuthType holds the default value on creation for the auth_type field.
bouncer.DefaultAuthType = bouncerDescAuthType.Default.(string)
// bouncerDescAutoCreated is the schema descriptor for auto_created field.
bouncerDescAutoCreated := bouncerFields[13].Descriptor()
// bouncer.DefaultAutoCreated holds the default value on creation for the auto_created field.
bouncer.DefaultAutoCreated = bouncerDescAutoCreated.Default.(bool)
configitemFields := schema.ConfigItem{}.Fields()
_ = configitemFields
// configitemDescCreatedAt is the schema descriptor for created_at field.

View file

@ -33,6 +33,8 @@ func (Bouncer) Fields() []ent.Field {
field.String("osname").Optional(),
field.String("osversion").Optional(),
field.String("featureflags").Optional(),
// Old auto-created TLS bouncers will have a wrong value for this field
field.Bool("auto_created").StructTag(`json:"auto_created"`).Default(false).Immutable(),
}
}

View file

@ -63,7 +63,7 @@ teardown() {
@test "delete non-existent bouncer" {
# this is a fatal error, which is not consistent with "machines delete"
rune -1 cscli bouncers delete something
assert_stderr --partial "unable to delete bouncer: 'something' does not exist"
assert_stderr --partial "unable to delete bouncer something: ent: bouncer not found"
rune -0 cscli bouncers delete something --ignore-missing
refute_stderr
}
@ -144,3 +144,56 @@ teardown() {
rune -0 cscli bouncers prune
assert_output 'No bouncers to prune.'
}
curl_localhost() {
[[ -z "$API_KEY" ]] && { fail "${FUNCNAME[0]}: missing API_KEY"; }
local path=$1
shift
curl "localhost:8080$path" -sS --fail-with-body -H "X-Api-Key: $API_KEY" "$@"
}
# We can't use curl-with-key here, as we want to query localhost, not 127.0.0.1
@test "multiple bouncers sharing api key" {
export API_KEY=bouncerkey
# crowdsec needs to listen on all interfaces
rune -0 ./instance-crowdsec stop
rune -0 config_set 'del(.api.server.listen_socket) | del(.api.server.listen_uri)'
echo "{'api':{'server':{'listen_uri':0.0.0.0:8080}}}" >"${CONFIG_YAML}.local"
rune -0 ./instance-crowdsec start
# add a decision for our bouncers
rune -0 cscli decisions add -i '1.2.3.5'
rune -0 cscli bouncers add test-auto -k "$API_KEY"
# query with 127.0.0.1 as source ip
rune -0 curl_localhost "/v1/decisions/stream" -4
rune -0 jq -r '.new' <(output)
assert_output --partial '1.2.3.5'
# now with ::1, we should get the same IP, even though we are using the same key
rune -0 curl_localhost "/v1/decisions/stream" -6
rune -0 jq -r '.new' <(output)
assert_output --partial '1.2.3.5'
rune -0 cscli bouncers list -o json
rune -0 jq -c '[.[] | [.name,.revoked,.ip_address,.auto_created]]' <(output)
assert_json '[["test-auto",false,"127.0.0.1",false],["test-auto@::1",false,"::1",true]]'
# check the 2nd bouncer was created automatically
rune -0 cscli bouncers inspect "test-auto@::1" -o json
rune -0 jq -r '.ip_address' <(output)
assert_output --partial '::1'
# attempt to delete the auto-created bouncer, it should fail
rune -0 cscli bouncers delete 'test-auto@::1'
assert_stderr --partial 'cannot be deleted'
# delete the "real" bouncer, it should delete both
rune -0 cscli bouncers delete 'test-auto'
rune -0 cscli bouncers list -o json
assert_json []
}

View file

@ -51,7 +51,11 @@ stop() {
PGID="$(ps -o pgid= -p "$(cat "${DAEMON_PID}")" | tr -d ' ')"
# ps above should work on linux, freebsd, busybox..
if [[ -n "${PGID}" ]]; then
kill -- "-${PGID}"
kill -- "-${PGID}"
while pgrep -g "${PGID}" >/dev/null; do
sleep .05
done
fi
rm -f -- "${DAEMON_PID}"