github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/controllers/v1/decisions.go (about)

     1  package v1
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/gin-gonic/gin"
    11  	log "github.com/sirupsen/logrus"
    12  
    13  	"github.com/crowdsecurity/crowdsec/pkg/database/ent"
    14  	"github.com/crowdsecurity/crowdsec/pkg/fflag"
    15  	"github.com/crowdsecurity/crowdsec/pkg/models"
    16  )
    17  
    18  // Format decisions for the bouncers
    19  func FormatDecisions(decisions []*ent.Decision) []*models.Decision {
    20  	var results []*models.Decision
    21  
    22  	for _, dbDecision := range decisions {
    23  		duration := dbDecision.Until.Sub(time.Now().UTC()).String()
    24  		decision := models.Decision{
    25  			ID:       int64(dbDecision.ID),
    26  			Duration: &duration,
    27  			Scenario: &dbDecision.Scenario,
    28  			Scope:    &dbDecision.Scope,
    29  			Value:    &dbDecision.Value,
    30  			Type:     &dbDecision.Type,
    31  			Origin:   &dbDecision.Origin,
    32  			UUID:     dbDecision.UUID,
    33  		}
    34  		results = append(results, &decision)
    35  	}
    36  
    37  	return results
    38  }
    39  
    40  func (c *Controller) GetDecision(gctx *gin.Context) {
    41  	var (
    42  		results []*models.Decision
    43  		data    []*ent.Decision
    44  	)
    45  
    46  	bouncerInfo, err := getBouncerFromContext(gctx)
    47  	if err != nil {
    48  		gctx.JSON(http.StatusUnauthorized, gin.H{"message": "not allowed"})
    49  
    50  		return
    51  	}
    52  
    53  	data, err = c.DBClient.QueryDecisionWithFilter(gctx.Request.URL.Query())
    54  	if err != nil {
    55  		c.HandleDBErrors(gctx, err)
    56  
    57  		return
    58  	}
    59  
    60  	results = FormatDecisions(data)
    61  	/*let's follow a naive logic : when a bouncer queries /decisions, if the answer is empty, we assume there is no decision for this ip/user/...,
    62  	but if it's non-empty, it means that there is one or more decisions for this target*/
    63  	if len(results) > 0 {
    64  		PrometheusBouncersHasNonEmptyDecision(gctx)
    65  	} else {
    66  		PrometheusBouncersHasEmptyDecision(gctx)
    67  	}
    68  
    69  	if gctx.Request.Method == http.MethodHead {
    70  		gctx.String(http.StatusOK, "")
    71  
    72  		return
    73  	}
    74  
    75  	if time.Now().UTC().Sub(bouncerInfo.LastPull) >= time.Minute {
    76  		if err := c.DBClient.UpdateBouncerLastPull(time.Now().UTC(), bouncerInfo.ID); err != nil {
    77  			log.Errorf("failed to update bouncer last pull: %v", err)
    78  		}
    79  	}
    80  
    81  	gctx.JSON(http.StatusOK, results)
    82  }
    83  
    84  func (c *Controller) DeleteDecisionById(gctx *gin.Context) {
    85  	decisionIDStr := gctx.Param("decision_id")
    86  
    87  	decisionID, err := strconv.Atoi(decisionIDStr)
    88  	if err != nil {
    89  		gctx.JSON(http.StatusBadRequest, gin.H{"message": "decision_id must be valid integer"})
    90  
    91  		return
    92  	}
    93  
    94  	nbDeleted, deletedFromDB, err := c.DBClient.SoftDeleteDecisionByID(decisionID)
    95  	if err != nil {
    96  		c.HandleDBErrors(gctx, err)
    97  
    98  		return
    99  	}
   100  
   101  	// transform deleted decisions to be sendable to capi
   102  	deletedDecisions := FormatDecisions(deletedFromDB)
   103  
   104  	if c.DecisionDeleteChan != nil {
   105  		c.DecisionDeleteChan <- deletedDecisions
   106  	}
   107  
   108  	deleteDecisionResp := models.DeleteDecisionResponse{
   109  		NbDeleted: strconv.Itoa(nbDeleted),
   110  	}
   111  
   112  	gctx.JSON(http.StatusOK, deleteDecisionResp)
   113  }
   114  
   115  func (c *Controller) DeleteDecisions(gctx *gin.Context) {
   116  	nbDeleted, deletedFromDB, err := c.DBClient.SoftDeleteDecisionsWithFilter(gctx.Request.URL.Query())
   117  	if err != nil {
   118  		c.HandleDBErrors(gctx, err)
   119  
   120  		return
   121  	}
   122  
   123  	// transform deleted decisions to be sendable to capi
   124  	deletedDecisions := FormatDecisions(deletedFromDB)
   125  
   126  	if c.DecisionDeleteChan != nil {
   127  		c.DecisionDeleteChan <- deletedDecisions
   128  	}
   129  
   130  	deleteDecisionResp := models.DeleteDecisionResponse{
   131  		NbDeleted: nbDeleted,
   132  	}
   133  
   134  	gctx.JSON(http.StatusOK, deleteDecisionResp)
   135  }
   136  
   137  func writeStartupDecisions(gctx *gin.Context, filters map[string][]string, dbFunc func(map[string][]string) ([]*ent.Decision, error)) error {
   138  	// respBuffer := bytes.NewBuffer([]byte{})
   139  	limit := 30000 //FIXME : make it configurable
   140  	needComma := false
   141  	lastId := 0
   142  
   143  	limitStr := fmt.Sprintf("%d", limit)
   144  	filters["limit"] = []string{limitStr}
   145  	for {
   146  		if lastId > 0 {
   147  			lastIdStr := fmt.Sprintf("%d", lastId)
   148  			filters["id_gt"] = []string{lastIdStr}
   149  		}
   150  
   151  		data, err := dbFunc(filters)
   152  		if err != nil {
   153  			return err
   154  		}
   155  		if len(data) > 0 {
   156  			lastId = data[len(data)-1].ID
   157  			results := FormatDecisions(data)
   158  			for _, decision := range results {
   159  				decisionJSON, _ := json.Marshal(decision)
   160  
   161  				if needComma {
   162  					//respBuffer.Write([]byte(","))
   163  					gctx.Writer.Write([]byte(","))
   164  				} else {
   165  					needComma = true
   166  				}
   167  				//respBuffer.Write(decisionJSON)
   168  				//_, err := gctx.Writer.Write(respBuffer.Bytes())
   169  				_, err := gctx.Writer.Write(decisionJSON)
   170  				if err != nil {
   171  					gctx.Writer.Flush()
   172  
   173  					return err
   174  				}
   175  				//respBuffer.Reset()
   176  			}
   177  		}
   178  		log.Debugf("startup: %d decisions returned (limit: %d, lastid: %d)", len(data), limit, lastId)
   179  		if len(data) < limit {
   180  			gctx.Writer.Flush()
   181  
   182  			break
   183  		}
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  func writeDeltaDecisions(gctx *gin.Context, filters map[string][]string, lastPull time.Time, dbFunc func(time.Time, map[string][]string) ([]*ent.Decision, error)) error {
   190  	//respBuffer := bytes.NewBuffer([]byte{})
   191  	limit := 30000 //FIXME : make it configurable
   192  	needComma := false
   193  	lastId := 0
   194  
   195  	limitStr := fmt.Sprintf("%d", limit)
   196  	filters["limit"] = []string{limitStr}
   197  	for {
   198  		if lastId > 0 {
   199  			lastIdStr := fmt.Sprintf("%d", lastId)
   200  			filters["id_gt"] = []string{lastIdStr}
   201  		}
   202  
   203  		data, err := dbFunc(lastPull, filters)
   204  		if err != nil {
   205  			return err
   206  		}
   207  		if len(data) > 0 {
   208  			lastId = data[len(data)-1].ID
   209  			results := FormatDecisions(data)
   210  			for _, decision := range results {
   211  				decisionJSON, _ := json.Marshal(decision)
   212  
   213  				if needComma {
   214  					//respBuffer.Write([]byte(","))
   215  					gctx.Writer.Write([]byte(","))
   216  				} else {
   217  					needComma = true
   218  				}
   219  				//respBuffer.Write(decisionJSON)
   220  				//_, err := gctx.Writer.Write(respBuffer.Bytes())
   221  				_, err := gctx.Writer.Write(decisionJSON)
   222  				if err != nil {
   223  					gctx.Writer.Flush()
   224  
   225  					return err
   226  				}
   227  				//respBuffer.Reset()
   228  			}
   229  		}
   230  		log.Debugf("startup: %d decisions returned (limit: %d, lastid: %d)", len(data), limit, lastId)
   231  		if len(data) < limit {
   232  			gctx.Writer.Flush()
   233  
   234  			break
   235  		}
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  func (c *Controller) StreamDecisionChunked(gctx *gin.Context, bouncerInfo *ent.Bouncer, streamStartTime time.Time, filters map[string][]string) error {
   242  	var err error
   243  
   244  	gctx.Writer.Header().Set("Content-Type", "application/json")
   245  	gctx.Writer.Header().Set("Transfer-Encoding", "chunked")
   246  	gctx.Writer.WriteHeader(http.StatusOK)
   247  	gctx.Writer.Write([]byte(`{"new": [`)) //No need to check for errors, the doc says it always returns nil
   248  
   249  	// if the blocker just started, return all decisions
   250  	if val, ok := gctx.Request.URL.Query()["startup"]; ok && val[0] == "true" {
   251  		// Active decisions
   252  		err := writeStartupDecisions(gctx, filters, c.DBClient.QueryAllDecisionsWithFilters)
   253  		if err != nil {
   254  			log.Errorf("failed sending new decisions for startup: %v", err)
   255  			gctx.Writer.Write([]byte(`], "deleted": []}`))
   256  			gctx.Writer.Flush()
   257  
   258  			return err
   259  		}
   260  
   261  		gctx.Writer.Write([]byte(`], "deleted": [`))
   262  		//Expired decisions
   263  		err = writeStartupDecisions(gctx, filters, c.DBClient.QueryExpiredDecisionsWithFilters)
   264  		if err != nil {
   265  			log.Errorf("failed sending expired decisions for startup: %v", err)
   266  			gctx.Writer.Write([]byte(`]}`))
   267  			gctx.Writer.Flush()
   268  
   269  			return err
   270  		}
   271  
   272  		gctx.Writer.Write([]byte(`]}`))
   273  		gctx.Writer.Flush()
   274  	} else {
   275  		err = writeDeltaDecisions(gctx, filters, bouncerInfo.LastPull, c.DBClient.QueryNewDecisionsSinceWithFilters)
   276  		if err != nil {
   277  			log.Errorf("failed sending new decisions for delta: %v", err)
   278  			gctx.Writer.Write([]byte(`], "deleted": []}`))
   279  			gctx.Writer.Flush()
   280  
   281  			return err
   282  		}
   283  
   284  		gctx.Writer.Write([]byte(`], "deleted": [`))
   285  
   286  		err = writeDeltaDecisions(gctx, filters, bouncerInfo.LastPull, c.DBClient.QueryExpiredDecisionsSinceWithFilters)
   287  
   288  		if err != nil {
   289  			log.Errorf("failed sending expired decisions for delta: %v", err)
   290  			gctx.Writer.Write([]byte(`]}`))
   291  			gctx.Writer.Flush()
   292  
   293  			return err
   294  		}
   295  
   296  		gctx.Writer.Write([]byte(`]}`))
   297  		gctx.Writer.Flush()
   298  	}
   299  
   300  	return nil
   301  }
   302  
   303  func (c *Controller) StreamDecisionNonChunked(gctx *gin.Context, bouncerInfo *ent.Bouncer, streamStartTime time.Time, filters map[string][]string) error {
   304  	var data []*ent.Decision
   305  	var err error
   306  
   307  	ret := make(map[string][]*models.Decision, 0)
   308  	ret["new"] = []*models.Decision{}
   309  	ret["deleted"] = []*models.Decision{}
   310  
   311  	if val, ok := gctx.Request.URL.Query()["startup"]; ok {
   312  		if val[0] == "true" {
   313  			data, err = c.DBClient.QueryAllDecisionsWithFilters(filters)
   314  			if err != nil {
   315  				log.Errorf("failed querying decisions: %v", err)
   316  				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
   317  
   318  				return err
   319  			}
   320  			//data = KeepLongestDecision(data)
   321  			ret["new"] = FormatDecisions(data)
   322  
   323  			// getting expired decisions
   324  			data, err = c.DBClient.QueryExpiredDecisionsWithFilters(filters)
   325  			if err != nil {
   326  				log.Errorf("unable to query expired decision for '%s' : %v", bouncerInfo.Name, err)
   327  				gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
   328  
   329  				return err
   330  			}
   331  
   332  			ret["deleted"] = FormatDecisions(data)
   333  
   334  			gctx.JSON(http.StatusOK, ret)
   335  
   336  			return nil
   337  		}
   338  	}
   339  
   340  	// getting new decisions
   341  	data, err = c.DBClient.QueryNewDecisionsSinceWithFilters(bouncerInfo.LastPull, filters)
   342  	if err != nil {
   343  		log.Errorf("unable to query new decision for '%s' : %v", bouncerInfo.Name, err)
   344  		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
   345  
   346  		return err
   347  	}
   348  	//data = KeepLongestDecision(data)
   349  	ret["new"] = FormatDecisions(data)
   350  
   351  	// getting expired decisions
   352  	data, err = c.DBClient.QueryExpiredDecisionsSinceWithFilters(bouncerInfo.LastPull.Add((-2 * time.Second)), filters) // do we want to give exactly lastPull time ?
   353  	if err != nil {
   354  		log.Errorf("unable to query expired decision for '%s' : %v", bouncerInfo.Name, err)
   355  		gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
   356  
   357  		return err
   358  	}
   359  
   360  	ret["deleted"] = FormatDecisions(data)
   361  	gctx.JSON(http.StatusOK, ret)
   362  
   363  	return nil
   364  }
   365  
   366  func (c *Controller) StreamDecision(gctx *gin.Context) {
   367  	var err error
   368  
   369  	streamStartTime := time.Now().UTC()
   370  
   371  	bouncerInfo, err := getBouncerFromContext(gctx)
   372  	if err != nil {
   373  		gctx.JSON(http.StatusUnauthorized, gin.H{"message": "not allowed"})
   374  
   375  		return
   376  	}
   377  
   378  	if gctx.Request.Method == http.MethodHead {
   379  		//For HEAD, just return as the bouncer won't get a body anyway, so no need to query the db
   380  		//We also don't update the last pull time, as it would mess with the delta sent on the next request (if done without startup=true)
   381  		gctx.String(http.StatusOK, "")
   382  
   383  		return
   384  	}
   385  
   386  	filters := gctx.Request.URL.Query()
   387  	if _, ok := filters["scopes"]; !ok {
   388  		filters["scopes"] = []string{"ip,range"}
   389  	}
   390  
   391  	if fflag.ChunkedDecisionsStream.IsEnabled() {
   392  		err = c.StreamDecisionChunked(gctx, bouncerInfo, streamStartTime, filters)
   393  	} else {
   394  		err = c.StreamDecisionNonChunked(gctx, bouncerInfo, streamStartTime, filters)
   395  	}
   396  
   397  	if err == nil {
   398  		//Only update the last pull time if no error occurred when sending the decisions to avoid missing decisions
   399  		if err := c.DBClient.UpdateBouncerLastPull(streamStartTime, bouncerInfo.ID); err != nil {
   400  			log.Errorf("unable to update bouncer '%s' pull: %v", bouncerInfo.Name, err)
   401  		}
   402  	}
   403  }