diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 57b61dd..eafcefd 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -880,6 +880,56 @@ const docTemplate = `{ } } }, + "/obligations/import": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Import obligations by uploading a json file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Import obligations by uploading a json file", + "operationId": "ImportObligations", + "parameters": [ + { + "type": "file", + "description": "obligations json file list", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "207": { + "description": "Multi-Status", + "schema": { + "$ref": "#/definitions/models.ImportObligationsResponse" + } + }, + "400": { + "description": "input file must be present", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/obligations/{topic}": { "get": { "description": "Get an active based on given topic", @@ -1356,6 +1406,20 @@ const docTemplate = `{ } } }, + "models.ImportObligationsResponse": { + "type": "object", + "properties": { + "data": { + "description": "can be of type models.LicenseError or models.ObligationImportStatus", + "type": "array", + "items": {} + }, + "status": { + "type": "integer", + "example": 207 + } + } + }, "models.LicenseDB": { "type": "object", "required": [ diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index e884247..7bce839 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -873,6 +873,56 @@ } } }, + "/obligations/import": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Import obligations by uploading a json file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Obligations" + ], + "summary": "Import obligations by uploading a json file", + "operationId": "ImportObligations", + "parameters": [ + { + "type": "file", + "description": "obligations json file list", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "207": { + "description": "Multi-Status", + "schema": { + "$ref": "#/definitions/models.ImportObligationsResponse" + } + }, + "400": { + "description": "input file must be present", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/obligations/{topic}": { "get": { "description": "Get an active based on given topic", @@ -1349,6 +1399,20 @@ } } }, + "models.ImportObligationsResponse": { + "type": "object", + "properties": { + "data": { + "description": "can be of type models.LicenseError or models.ObligationImportStatus", + "type": "array", + "items": {} + }, + "status": { + "type": "integer", + "example": 207 + } + } + }, "models.LicenseDB": { "type": "object", "required": [ diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 2561813..22cc815 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -62,6 +62,16 @@ definitions: example: 200 type: integer type: object + models.ImportObligationsResponse: + properties: + data: + description: can be of type models.LicenseError or models.ObligationImportStatus + items: {} + type: array + status: + example: 207 + type: integer + type: object models.LicenseDB: properties: external_ref: @@ -1231,6 +1241,38 @@ paths: summary: Fetches audits corresponding to an obligation tags: - Obligations + /obligations/import: + post: + consumes: + - multipart/form-data + description: Import obligations by uploading a json file + operationId: ImportObligations + parameters: + - description: obligations json file list + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "207": + description: Multi-Status + schema: + $ref: '#/definitions/models.ImportObligationsResponse' + "400": + description: input file must be present + schema: + $ref: '#/definitions/models.LicenseError' + "500": + description: Internal server error + schema: + $ref: '#/definitions/models.LicenseError' + security: + - ApiKeyAuth: [] + summary: Import obligations by uploading a json file + tags: + - Obligations /search: post: consumes: diff --git a/pkg/api/api.go b/pkg/api/api.go index 86254f1..5805249 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -121,6 +121,7 @@ func Router() *gin.Engine { obligations := authorizedv1.Group("/obligations") { obligations.POST("", CreateObligation) + obligations.POST("import", ImportObligations) obligations.PATCH(":topic", UpdateObligation) obligations.DELETE(":topic", DeleteObligation) } diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index 61ae38b..f405ff3 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -9,9 +9,12 @@ package api import ( "crypto/md5" "encoding/hex" + "encoding/json" "errors" "fmt" + "log" "net/http" + "path/filepath" "strconv" "time" @@ -562,3 +565,149 @@ func GetObligationAudits(c *gin.Context) { c.JSON(http.StatusOK, response) } + +// ImportObligations creates new obligation records via a json file. +// +// @Summary Import obligations by uploading a json file +// @Description Import obligations by uploading a json file +// @Id ImportObligations +// @Tags Obligations +// @Accept multipart/form-data +// @Produce json +// @Param file formData file true "obligations json file list" +// @Success 207 {object} models.ImportObligationsResponse +// @Failure 400 {object} models.LicenseError "input file must be present" +// @Failure 500 {object} models.LicenseError "Internal server error" +// @Security ApiKeyAuth +// @Router /obligations/import [post] +func ImportObligations(c *gin.Context) { + file, header, err := c.Request.FormFile("file") + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "input file must be present", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + defer file.Close() + + if filepath.Ext(header.Filename) != ".json" { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "only files with format *.json are allowed", + Error: "only files with format *.json are allowed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + var obligations []models.ObligationImport + decoder := json.NewDecoder(file) + if err := decoder.Decode(&obligations); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "invalid json", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + res := models.ImportObligationsResponse{ + Status: http.StatusMultiStatus, + } + + for _, obligation := range obligations { + _ = db.DB.Transaction(func(tx *gorm.DB) error { + ob := models.Obligation{ + Topic: obligation.Topic, + Type: obligation.Type, + Text: obligation.Text, + Classification: obligation.Classification, + Modifications: obligation.Modifications, + Comment: obligation.Comment, + Active: obligation.Active, + TextUpdatable: obligation.TextUpdatable, + } + + hash := md5.Sum([]byte(ob.Text)) + md5hash := hex.EncodeToString(hash[:]) + ob.Md5 = md5hash + + result := tx. + Where(&models.Obligation{Topic: ob.Topic}). + Or(&models.Obligation{Md5: ob.Md5}). + FirstOrCreate(&ob) + + if result.RowsAffected == 0 { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusConflict, + Message: fmt.Sprintf("Obligation with topic '%s' or MD5 '%s' already exists", + ob.Topic, ob.Md5), + Error: ob.Topic, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return errors.New("obligation already exists") + } + if result.Error != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: fmt.Sprintf("Failed to create obligation: %s", result.Error.Error()), + Error: ob.Topic, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + + var maps []models.ObligationMap + for _, i := range obligation.Shortnames { + var license models.LicenseDB + if err := tx.Debug().Where(models.LicenseDB{Shortname: i}).First(&license).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: fmt.Sprintf("Error finding license with shortname: %s", i), + Error: ob.Topic, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + log.Println(license.Shortname) + maps = append(maps, models.ObligationMap{ + ObligationPk: ob.Id, + RfPk: license.Id, + }) + } + + if err := tx.Create(&maps).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Error linking obligation with license", + Error: ob.Topic, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + + res.Data = append(res.Data, models.ObligationImportStatus{ + Data: models.ObligationId{Id: ob.Id, Topic: ob.Topic}, + Status: http.StatusCreated, + }) + + return nil + }) + } + + c.JSON(http.StatusMultiStatus, res) +} diff --git a/pkg/models/types.go b/pkg/models/types.go index 41e253f..ad7850b 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -325,3 +325,38 @@ type ObligationMapResponse struct { Data []ObligationMapUser `json:"data"` Meta PaginationMeta `json:"paginationmeta"` } + +type ObligationImportRequest struct { + ObligationFile string `form:"file"` +} + +// Obligation represents an obligation record in the import json file. +type ObligationImport struct { + Topic string `json:"topic" example:"copyleft" validate:"required"` + Type string `json:"type" enums:"obligation,restriction,risk,right" validate:"required"` + Text string `json:"text" example:"Source code be made available when distributing the software." validate:"required"` + Classification string `json:"classification" enums:"green,white,yellow,red" validate:"required"` + Modifications bool `json:"modifications" validate:"required"` + Comment string `json:"comment" example:"This is a comment." validate:"required"` + Active bool `json:"active" validate:"required"` + TextUpdatable bool `json:"text_updatable" validate:"required"` + Shortnames []string `json:"shortnames" example:"GPL-2.0-only,GPL-2.0-or-later" validate:"required"` +} + +// Id of successfully imported obligation +type ObligationId struct { + Id int64 `json:"id"` + Topic string `json:"topic" example:"copyleft"` +} + +// Status of obligation records successfully inserted in the database during import +type ObligationImportStatus struct { + Status int `json:"status" example:"200"` + Data ObligationId `json:"data"` +} + +// Response structure for import obligation response +type ImportObligationsResponse struct { + Status int `json:"status" example:"207"` + Data []interface{} `json:"data"` // can be of type models.LicenseError or models.ObligationImportStatus +}