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 }