2020-11-30 09:37:17 +00:00
package v1
import (
2023-02-20 14:26:30 +00:00
"encoding/json"
"fmt"
2020-11-30 09:37:17 +00:00
"net/http"
"strconv"
"time"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
2023-02-20 14:26:30 +00:00
"github.com/crowdsecurity/crowdsec/pkg/fflag"
2020-11-30 09:37:17 +00:00
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
2023-02-20 14:26:30 +00:00
// Format decisions for the bouncers
func FormatDecisions ( decisions [ ] * ent . Decision ) [ ] * models . Decision {
2020-11-30 09:37:17 +00:00
var results [ ] * models . Decision
2022-06-22 08:29:02 +00:00
2020-11-30 09:37:17 +00:00
for _ , dbDecision := range decisions {
2022-01-19 13:56:05 +00:00
duration := dbDecision . Until . Sub ( time . Now ( ) . UTC ( ) ) . String ( )
2020-11-30 09:37:17 +00:00
decision := models . Decision {
ID : int64 ( dbDecision . ID ) ,
Duration : & duration ,
Scenario : & dbDecision . Scenario ,
Scope : & dbDecision . Scope ,
Value : & dbDecision . Value ,
Type : & dbDecision . Type ,
Origin : & dbDecision . Origin ,
2023-01-31 13:47:44 +00:00
UUID : dbDecision . UUID ,
2020-11-30 09:37:17 +00:00
}
results = append ( results , & decision )
}
2023-02-20 14:26:30 +00:00
return results
2020-11-30 09:37:17 +00:00
}
func ( c * Controller ) GetDecision ( gctx * gin . Context ) {
var err error
var results [ ] * models . Decision
var data [ ] * ent . Decision
2022-05-27 13:23:59 +00:00
bouncerInfo , err := getBouncerFromContext ( gctx )
if err != nil {
gctx . JSON ( http . StatusUnauthorized , gin . H { "message" : "not allowed" } )
return
}
2020-11-30 09:37:17 +00:00
data , err = c . DBClient . QueryDecisionWithFilter ( gctx . Request . URL . Query ( ) )
if err != nil {
c . HandleDBErrors ( gctx , err )
return
}
2023-02-20 14:26:30 +00:00
results = FormatDecisions ( data )
2020-11-30 09:37:17 +00:00
/ * 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 / ... ,
but if it ' s non - empty , it means that there is one or more decisions for this target * /
if len ( results ) > 0 {
PrometheusBouncersHasNonEmptyDecision ( gctx )
} else {
PrometheusBouncersHasEmptyDecision ( gctx )
}
2022-09-06 11:55:03 +00:00
if gctx . Request . Method == http . MethodHead {
2020-11-30 09:37:17 +00:00
gctx . String ( http . StatusOK , "" )
return
}
2022-05-27 13:23:59 +00:00
if time . Now ( ) . UTC ( ) . Sub ( bouncerInfo . LastPull ) >= time . Minute {
if err := c . DBClient . UpdateBouncerLastPull ( time . Now ( ) . UTC ( ) , bouncerInfo . ID ) ; err != nil {
log . Errorf ( "failed to update bouncer last pull: %v" , err )
}
}
2020-11-30 09:37:17 +00:00
gctx . JSON ( http . StatusOK , results )
}
func ( c * Controller ) DeleteDecisionById ( gctx * gin . Context ) {
var err error
decisionIDStr := gctx . Param ( "decision_id" )
decisionID , err := strconv . Atoi ( decisionIDStr )
if err != nil {
gctx . JSON ( http . StatusBadRequest , gin . H { "message" : "decision_id must be valid integer" } )
return
}
2023-01-31 13:47:44 +00:00
nbDeleted , deletedFromDB , err := c . DBClient . SoftDeleteDecisionByID ( decisionID )
2020-11-30 09:37:17 +00:00
if err != nil {
c . HandleDBErrors ( gctx , err )
return
}
2023-01-31 13:47:44 +00:00
//transform deleted decisions to be sendable to capi
2023-02-20 14:26:30 +00:00
deletedDecisions := FormatDecisions ( deletedFromDB )
2023-01-31 13:47:44 +00:00
if c . DecisionDeleteChan != nil {
c . DecisionDeleteChan <- deletedDecisions
}
2020-11-30 09:37:17 +00:00
deleteDecisionResp := models . DeleteDecisionResponse {
2022-06-22 08:29:02 +00:00
NbDeleted : strconv . Itoa ( nbDeleted ) ,
2020-11-30 09:37:17 +00:00
}
gctx . JSON ( http . StatusOK , deleteDecisionResp )
}
func ( c * Controller ) DeleteDecisions ( gctx * gin . Context ) {
var err error
2023-01-31 13:47:44 +00:00
nbDeleted , deletedFromDB , err := c . DBClient . SoftDeleteDecisionsWithFilter ( gctx . Request . URL . Query ( ) )
2020-11-30 09:37:17 +00:00
if err != nil {
c . HandleDBErrors ( gctx , err )
return
}
2023-01-31 13:47:44 +00:00
//transform deleted decisions to be sendable to capi
2023-02-20 14:26:30 +00:00
deletedDecisions := FormatDecisions ( deletedFromDB )
2023-01-31 13:47:44 +00:00
if c . DecisionDeleteChan != nil {
c . DecisionDeleteChan <- deletedDecisions
}
2020-11-30 09:37:17 +00:00
deleteDecisionResp := models . DeleteDecisionResponse {
NbDeleted : nbDeleted ,
}
gctx . JSON ( http . StatusOK , deleteDecisionResp )
}
2023-02-20 14:26:30 +00:00
func writeStartupDecisions ( gctx * gin . Context , filters map [ string ] [ ] string , dbFunc func ( map [ string ] [ ] string ) ( [ ] * ent . Decision , error ) ) error {
// respBuffer := bytes.NewBuffer([]byte{})
limit := 30000 //FIXME : make it configurable
needComma := false
lastId := 0
2020-11-30 09:37:17 +00:00
2023-02-20 14:26:30 +00:00
limitStr := fmt . Sprintf ( "%d" , limit )
filters [ "limit" ] = [ ] string { limitStr }
for {
if lastId > 0 {
lastIdStr := fmt . Sprintf ( "%d" , lastId )
filters [ "id_gt" ] = [ ] string { lastIdStr }
}
2020-11-30 09:37:17 +00:00
2023-02-20 14:26:30 +00:00
data , err := dbFunc ( filters )
if err != nil {
return err
}
if len ( data ) > 0 {
lastId = data [ len ( data ) - 1 ] . ID
results := FormatDecisions ( data )
for _ , decision := range results {
decisionJSON , _ := json . Marshal ( decision )
if needComma {
//respBuffer.Write([]byte(","))
gctx . Writer . Write ( [ ] byte ( "," ) )
} else {
needComma = true
}
//respBuffer.Write(decisionJSON)
//_, err := gctx.Writer.Write(respBuffer.Bytes())
_ , err := gctx . Writer . Write ( decisionJSON )
if err != nil {
gctx . Writer . Flush ( )
return err
}
//respBuffer.Reset()
}
}
log . Debugf ( "startup: %d decisions returned (limit: %d, lastid: %d)" , len ( data ) , limit , lastId )
if len ( data ) < limit {
gctx . Writer . Flush ( )
break
}
2021-05-31 13:07:09 +00:00
}
2023-02-20 14:26:30 +00:00
return nil
}
func writeDeltaDecisions ( gctx * gin . Context , filters map [ string ] [ ] string , lastPull time . Time , dbFunc func ( time . Time , map [ string ] [ ] string ) ( [ ] * ent . Decision , error ) ) error {
//respBuffer := bytes.NewBuffer([]byte{})
limit := 30000 //FIXME : make it configurable
needComma := false
lastId := 0
2021-05-31 13:07:09 +00:00
2023-02-20 14:26:30 +00:00
limitStr := fmt . Sprintf ( "%d" , limit )
filters [ "limit" ] = [ ] string { limitStr }
for {
if lastId > 0 {
lastIdStr := fmt . Sprintf ( "%d" , lastId )
filters [ "id_gt" ] = [ ] string { lastIdStr }
}
data , err := dbFunc ( lastPull , filters )
if err != nil {
return err
}
if len ( data ) > 0 {
lastId = data [ len ( data ) - 1 ] . ID
results := FormatDecisions ( data )
for _ , decision := range results {
decisionJSON , _ := json . Marshal ( decision )
if needComma {
//respBuffer.Write([]byte(","))
gctx . Writer . Write ( [ ] byte ( "," ) )
} else {
needComma = true
}
//respBuffer.Write(decisionJSON)
//_, err := gctx.Writer.Write(respBuffer.Bytes())
_ , err := gctx . Writer . Write ( decisionJSON )
if err != nil {
gctx . Writer . Flush ( )
return err
}
//respBuffer.Reset()
}
}
log . Debugf ( "startup: %d decisions returned (limit: %d, lastid: %d)" , len ( data ) , limit , lastId )
if len ( data ) < limit {
gctx . Writer . Flush ( )
break
}
2022-08-26 12:17:46 +00:00
}
2023-02-20 14:26:30 +00:00
return nil
}
func ( c * Controller ) StreamDecisionChunked ( gctx * gin . Context , bouncerInfo * ent . Bouncer , streamStartTime time . Time , filters map [ string ] [ ] string ) error {
var err error
gctx . Writer . Header ( ) . Set ( "Content-Type" , "application/json" )
gctx . Writer . Header ( ) . Set ( "Transfer-Encoding" , "chunked" )
gctx . Writer . WriteHeader ( http . StatusOK )
gctx . Writer . Write ( [ ] byte ( ` { "new": [ ` ) ) //No need to check for errors, the doc says it always returns nil
2022-08-26 12:17:46 +00:00
2023-03-03 10:06:27 +00:00
// if the blocker just started, return all decisions
2023-02-20 14:26:30 +00:00
if val , ok := gctx . Request . URL . Query ( ) [ "startup" ] ; ok && val [ 0 ] == "true" {
//Active decisions
err := writeStartupDecisions ( gctx , filters , c . DBClient . QueryAllDecisionsWithFilters )
if err != nil {
log . Errorf ( "failed sending new decisions for startup: %v" , err )
gctx . Writer . Write ( [ ] byte ( ` ], "deleted": []} ` ) )
gctx . Writer . Flush ( )
return err
}
gctx . Writer . Write ( [ ] byte ( ` ], "deleted": [ ` ) )
//Expired decisions
err = writeStartupDecisions ( gctx , filters , c . DBClient . QueryExpiredDecisionsWithFilters )
if err != nil {
log . Errorf ( "failed sending expired decisions for startup: %v" , err )
gctx . Writer . Write ( [ ] byte ( ` ]} ` ) )
gctx . Writer . Flush ( )
return err
}
gctx . Writer . Write ( [ ] byte ( ` ]} ` ) )
gctx . Writer . Flush ( )
} else {
err = writeDeltaDecisions ( gctx , filters , bouncerInfo . LastPull , c . DBClient . QueryNewDecisionsSinceWithFilters )
if err != nil {
log . Errorf ( "failed sending new decisions for delta: %v" , err )
gctx . Writer . Write ( [ ] byte ( ` ], "deleted": []} ` ) )
gctx . Writer . Flush ( )
return err
}
gctx . Writer . Write ( [ ] byte ( ` ], "deleted": [ ` ) )
err = writeDeltaDecisions ( gctx , filters , bouncerInfo . LastPull , c . DBClient . QueryExpiredDecisionsSinceWithFilters )
if err != nil {
log . Errorf ( "failed sending expired decisions for delta: %v" , err )
gctx . Writer . Write ( [ ] byte ( ` ]} ` ) )
gctx . Writer . Flush ( )
return err
}
gctx . Writer . Write ( [ ] byte ( ` ]} ` ) )
gctx . Writer . Flush ( )
}
return nil
}
func ( c * Controller ) StreamDecisionNonChunked ( gctx * gin . Context , bouncerInfo * ent . Bouncer , streamStartTime time . Time , filters map [ string ] [ ] string ) error {
var data [ ] * ent . Decision
var err error
ret := make ( map [ string ] [ ] * models . Decision , 0 )
ret [ "new" ] = [ ] * models . Decision { }
ret [ "deleted" ] = [ ] * models . Decision { }
2020-11-30 09:37:17 +00:00
if val , ok := gctx . Request . URL . Query ( ) [ "startup" ] ; ok {
if val [ 0 ] == "true" {
2022-06-16 12:41:54 +00:00
data , err = c . DBClient . QueryAllDecisionsWithFilters ( filters )
2020-11-30 09:37:17 +00:00
if err != nil {
log . Errorf ( "failed querying decisions: %v" , err )
gctx . JSON ( http . StatusInternalServerError , gin . H { "message" : err . Error ( ) } )
2023-02-20 14:26:30 +00:00
return err
2020-11-30 09:37:17 +00:00
}
2022-06-22 08:29:02 +00:00
//data = KeepLongestDecision(data)
2023-02-20 14:26:30 +00:00
ret [ "new" ] = FormatDecisions ( data )
2020-11-30 09:37:17 +00:00
// getting expired decisions
2021-05-31 13:07:09 +00:00
data , err = c . DBClient . QueryExpiredDecisionsWithFilters ( filters )
2020-11-30 09:37:17 +00:00
if err != nil {
log . Errorf ( "unable to query expired decision for '%s' : %v" , bouncerInfo . Name , err )
gctx . JSON ( http . StatusInternalServerError , gin . H { "message" : err . Error ( ) } )
2023-02-20 14:26:30 +00:00
return err
2020-11-30 09:37:17 +00:00
}
2023-02-20 14:26:30 +00:00
ret [ "deleted" ] = FormatDecisions ( data )
2020-11-30 09:37:17 +00:00
gctx . JSON ( http . StatusOK , ret )
2023-02-20 14:26:30 +00:00
return nil
2020-11-30 09:37:17 +00:00
}
}
// getting new decisions
2021-05-31 13:07:09 +00:00
data , err = c . DBClient . QueryNewDecisionsSinceWithFilters ( bouncerInfo . LastPull , filters )
2020-11-30 09:37:17 +00:00
if err != nil {
log . Errorf ( "unable to query new decision for '%s' : %v" , bouncerInfo . Name , err )
gctx . JSON ( http . StatusInternalServerError , gin . H { "message" : err . Error ( ) } )
2023-02-20 14:26:30 +00:00
return err
2020-11-30 09:37:17 +00:00
}
2022-06-22 08:29:02 +00:00
//data = KeepLongestDecision(data)
2023-02-20 14:26:30 +00:00
ret [ "new" ] = FormatDecisions ( data )
2020-11-30 09:37:17 +00:00
// getting expired decisions
2021-05-31 13:07:09 +00:00
data , err = c . DBClient . QueryExpiredDecisionsSinceWithFilters ( bouncerInfo . LastPull . Add ( ( - 2 * time . Second ) ) , filters ) // do we want to give exactly lastPull time ?
2020-11-30 09:37:17 +00:00
if err != nil {
log . Errorf ( "unable to query expired decision for '%s' : %v" , bouncerInfo . Name , err )
gctx . JSON ( http . StatusInternalServerError , gin . H { "message" : err . Error ( ) } )
2023-02-20 14:26:30 +00:00
return err
2020-11-30 09:37:17 +00:00
}
2023-02-20 14:26:30 +00:00
ret [ "deleted" ] = FormatDecisions ( data )
gctx . JSON ( http . StatusOK , ret )
return nil
}
func ( c * Controller ) StreamDecision ( gctx * gin . Context ) {
var err error
streamStartTime := time . Now ( ) . UTC ( )
bouncerInfo , err := getBouncerFromContext ( gctx )
2020-11-30 09:37:17 +00:00
if err != nil {
2023-02-20 14:26:30 +00:00
gctx . JSON ( http . StatusUnauthorized , gin . H { "message" : "not allowed" } )
2020-11-30 09:37:17 +00:00
return
}
2023-02-20 14:26:30 +00:00
if gctx . Request . Method == http . MethodHead {
//For HEAD, just return as the bouncer won't get a body anyway, so no need to query the db
//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)
gctx . String ( http . StatusOK , "" )
2020-11-30 09:37:17 +00:00
return
}
2023-02-20 14:26:30 +00:00
filters := gctx . Request . URL . Query ( )
if _ , ok := filters [ "scopes" ] ; ! ok {
filters [ "scopes" ] = [ ] string { "ip,range" }
}
if fflag . ChunkedDecisionsStream . IsEnabled ( ) {
err = c . StreamDecisionChunked ( gctx , bouncerInfo , streamStartTime , filters )
} else {
err = c . StreamDecisionNonChunked ( gctx , bouncerInfo , streamStartTime , filters )
}
if err == nil {
//Only update the last pull time if no error occurred when sending the decisions to avoid missing decisions
if err := c . DBClient . UpdateBouncerLastPull ( streamStartTime , bouncerInfo . ID ) ; err != nil {
log . Errorf ( "unable to update bouncer '%s' pull: %v" , bouncerInfo . Name , err )
}
}
2020-11-30 09:37:17 +00:00
}