diff --git a/.github/workflows/api-swagger.yml b/.github/workflows/api-swagger.yml index e07f69d..1447c4d 100644 --- a/.github/workflows/api-swagger.yml +++ b/.github/workflows/api-swagger.yml @@ -29,7 +29,7 @@ jobs: - name: Build Swagger documents run: | - swag init --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./swag/docs + swag init --parseDependency --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./swag/docs - name: Check doc diff run: | diff --git a/README.md b/README.md index c2085f4..e942516 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,9 @@ INSERT INTO users (username, userpassword, userlevel) VALUES ('', ' ```bash - swag init --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./cmd/laas/docs + swag init --parseDependency --generalInfo api.go --dir ./pkg/api,./pkg/auth,./pkg/db,./pkg/models,./pkg/utils --output ./cmd/laas/docs ``` 3. Swagger documentation will be generated in `./cmd/laas/docs` folder. 4. Run the project and navigate to `http://localhost:8080/swagger/index.html` to view the documentation. diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index cea3fef..ad08219 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -345,6 +345,12 @@ const docTemplate = `{ "description": "Limit of responses per page", "name": "limit", "in": "query" + }, + { + "type": "string", + "description": "External reference parameters", + "name": "externalRef", + "in": "query" } ], "responses": { @@ -1261,6 +1267,9 @@ const docTemplate = `{ } }, "definitions": { + "datatypes.JSONType-models_LicenseDBSchemaExtension": { + "type": "object" + }, "models.Audit": { "type": "object", "properties": { @@ -1353,6 +1362,9 @@ const docTemplate = `{ "rf_shortname" ], "properties": { + "external_ref": { + "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" + }, "marydone": { "type": "boolean" }, @@ -1456,6 +1468,9 @@ const docTemplate = `{ "models.LicenseInput": { "type": "object", "properties": { + "external_ref": { + "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" + }, "marydone": { "type": "boolean" }, diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 26c0ed8..6cba72f 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -339,6 +339,12 @@ "description": "Limit of responses per page", "name": "limit", "in": "query" + }, + { + "type": "string", + "description": "External reference parameters", + "name": "externalRef", + "in": "query" } ], "responses": { @@ -1255,6 +1261,9 @@ } }, "definitions": { + "datatypes.JSONType-models_LicenseDBSchemaExtension": { + "type": "object" + }, "models.Audit": { "type": "object", "properties": { @@ -1347,6 +1356,9 @@ "rf_shortname" ], "properties": { + "external_ref": { + "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" + }, "marydone": { "type": "boolean" }, @@ -1450,6 +1462,9 @@ "models.LicenseInput": { "type": "object", "properties": { + "external_ref": { + "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" + }, "marydone": { "type": "boolean" }, diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 7a1ecda..7794739 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -1,5 +1,7 @@ basePath: /api/v1 definitions: + datatypes.JSONType-models_LicenseDBSchemaExtension: + type: object models.Audit: properties: id: @@ -62,6 +64,8 @@ definitions: type: object models.LicenseDB: properties: + external_ref: + $ref: '#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension' marydone: type: boolean rf_FSFfree: @@ -137,6 +141,8 @@ definitions: type: object models.LicenseInput: properties: + external_ref: + $ref: '#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension' marydone: type: boolean rf_FSFfree: @@ -741,6 +747,10 @@ paths: in: query name: limit type: integer + - description: External reference parameters + in: query + name: externalRef + type: string produces: - application/json responses: diff --git a/go.mod b/go.mod index 5de54af..bb7a5dd 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,11 @@ require ( gorm.io/gorm v1.25.1 ) +require ( + github.com/go-sql-driver/mysql v1.7.0 // indirect + gorm.io/driver/mysql v1.4.7 // indirect +) + require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect @@ -59,4 +64,5 @@ require ( google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.2.0 ) diff --git a/go.sum b/go.sum index 0b08161..7fc7a53 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -54,6 +56,7 @@ github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -176,8 +179,13 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco= +gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04= +gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= +gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index 727b16a..6933326 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -7,6 +7,7 @@ package api import ( + "encoding/json" "fmt" "net/http" "strconv" @@ -16,6 +17,10 @@ import ( "github.com/fossology/LicenseDb/pkg/models" "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" + + "github.com/gin-gonic/gin/binding" + "gorm.io/gorm" + "gorm.io/gorm/clause" ) // GetAllLicense The get all License function returns all the license data present in the database. @@ -68,6 +73,7 @@ func GetAllLicense(c *gin.Context) { // @Param copyleft query bool false "Copyleft flag status of license" // @Param page query int false "Page number" // @Param limit query int false "Limit of responses per page" +// @Param externalRef query string false "External reference parameters" // @Success 200 {object} models.LicenseResponse "Filtered licenses" // @Failure 400 {object} models.LicenseError "Invalid value" // @Router /licenses [get] @@ -81,10 +87,29 @@ func FilterLicense(c *gin.Context) { OSIapproved := c.Query("osiapproved") fsffree := c.Query("fsffree") copyleft := c.Query("copyleft") + externalRef := c.Query("externalRef") + + externalRefData := make(map[string]string) + + if len(externalRef) > 0 { + err := json.Unmarshal([]byte(externalRef), &externalRefData) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid external ref type value", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + } + var license []models.LicenseDB query := db.DB.Model(&license) - if SpdxId == "" && GPLv2compatible == "" && GPLv3compatible == "" && DetectorType == "" && marydone == "" && active == "" && fsffree == "" && OSIapproved == "" && copyleft == "" { + if SpdxId == "" && GPLv2compatible == "" && GPLv3compatible == "" && DetectorType == "" && marydone == "" && active == "" && fsffree == "" && OSIapproved == "" && copyleft == "" && externalRef == "" { GetAllLicense(c) return } @@ -164,6 +189,10 @@ func FilterLicense(c *gin.Context) { query = query.Where(models.LicenseDB{Marydone: parsedMarydone}) } + for externalRefKey, externalRefValue := range externalRefData { + query = query.Where(fmt.Sprintf("external_ref->>'%s' = ?", externalRefKey), externalRefValue) + } + if err := query.Error; err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, @@ -286,6 +315,7 @@ func CreateLicense(c *gin.Context) { Source: input.Source, SpdxId: input.SpdxId, Risk: input.Risk, + ExternalRef: input.ExternalRef, } result := db.DB. @@ -343,6 +373,7 @@ func CreateLicense(c *gin.Context) { // @Router /licenses/{shortname} [patch] func UpdateLicense(c *gin.Context) { var update models.LicenseUpdate + var externalRefsPayload models.UpdateExternalRefsJSONPayload var license models.LicenseDB var oldlicense models.LicenseDB @@ -361,7 +392,21 @@ func UpdateLicense(c *gin.Context) { return } oldlicense = license - if err := c.ShouldBindJSON(&update); err != nil { + + // https://github.com/gin-gonic/gin/pull/1341 + if err := c.ShouldBindBodyWith(&update, binding.JSON); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body update", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + if err := c.ShouldBindBodyWith(&externalRefsPayload, binding.JSON); err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, Message: "invalid json body", @@ -372,6 +417,7 @@ func UpdateLicense(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } + if update.Text != "" && !oldlicense.TextUpdatable && oldlicense.Text != update.Text { er := models.LicenseError{ Status: http.StatusBadRequest, @@ -387,7 +433,22 @@ func UpdateLicense(c *gin.Context) { // Update flag to indicate the license text was updated. update.Flag = 2 } - if err := db.DB.Model(&license).Updates(update).Error; err != nil { + + // Overwrite values of existing keys, add new key value pairs and remove keys with null values. + if err := db.DB.Model(&license).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(external_ref || ?)", externalRefsPayload.ExternalRef)).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + // Update all other fields except external_ref + if err := db.DB.Model(&license).Clauses(clause.Returning{}).Updates(update).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to update license", @@ -407,188 +468,237 @@ func UpdateLicense(c *gin.Context) { }, } - var user models.User - db.DB.Where(models.User{Username: username}).First(&user) - audit := models.Audit{ - UserId: user.Id, - TypeId: license.Id, - Timestamp: time.Now(), - Type: "license", - } - - db.DB.Create(&audit) + var changes []models.ChangeLog if oldlicense.Fullname != license.Fullname { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Fullname", - OldValue: oldlicense.Fullname, - UpdatedValue: license.Fullname, - } - db.DB.Create(&change) + OldValue: &oldlicense.Fullname, + UpdatedValue: &license.Fullname, + }) } if oldlicense.Url != license.Url { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Url", - OldValue: oldlicense.Url, - UpdatedValue: license.Url, - } - db.DB.Create(&change) + OldValue: &oldlicense.Url, + UpdatedValue: &license.Url, + }) } if oldlicense.AddDate != license.AddDate { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := oldlicense.AddDate.Format(time.RFC3339) + newVal := license.AddDate.Format(time.RFC3339) + changes = append(changes, models.ChangeLog{ Field: "Adddate", - OldValue: oldlicense.AddDate.Format(time.RFC3339), - UpdatedValue: license.AddDate.Format(time.RFC3339), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Active != license.Active { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.Active) + newVal := strconv.FormatBool(license.Active) + changes = append(changes, models.ChangeLog{ Field: "Active", - OldValue: strconv.FormatBool(oldlicense.Active), - UpdatedValue: strconv.FormatBool(license.Active), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Copyleft != license.Copyleft { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.Copyleft) + newVal := strconv.FormatBool(license.Copyleft) + changes = append(changes, models.ChangeLog{ Field: "Copyleft", - OldValue: strconv.FormatBool(oldlicense.Copyleft), - UpdatedValue: strconv.FormatBool(license.Copyleft), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.FSFfree != license.FSFfree { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.FSFfree) + newVal := strconv.FormatBool(license.FSFfree) + changes = append(changes, models.ChangeLog{ Field: "FSFfree", - OldValue: strconv.FormatBool(oldlicense.FSFfree), - UpdatedValue: strconv.FormatBool(license.FSFfree), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.GPLv2compatible != license.GPLv2compatible { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.GPLv2compatible) + newVal := strconv.FormatBool(license.GPLv2compatible) + changes = append(changes, models.ChangeLog{ Field: "GPLv2compatible", - OldValue: strconv.FormatBool(oldlicense.GPLv2compatible), - UpdatedValue: strconv.FormatBool(license.GPLv2compatible), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.GPLv3compatible != license.GPLv3compatible { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.GPLv3compatible) + newVal := strconv.FormatBool(license.GPLv3compatible) + changes = append(changes, models.ChangeLog{ Field: "GPLv3compatible", - OldValue: strconv.FormatBool(oldlicense.GPLv3compatible), - UpdatedValue: strconv.FormatBool(license.GPLv3compatible), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.OSIapproved != license.OSIapproved { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.OSIapproved) + newVal := strconv.FormatBool(license.OSIapproved) + changes = append(changes, models.ChangeLog{ Field: "OSIapproved", - OldValue: strconv.FormatBool(oldlicense.OSIapproved), - UpdatedValue: strconv.FormatBool(license.OSIapproved), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Text != license.Text { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Text", - OldValue: oldlicense.Text, - UpdatedValue: license.Text, - } - db.DB.Create(&change) + OldValue: &oldlicense.Text, + UpdatedValue: &license.Text, + }) } if oldlicense.TextUpdatable != license.TextUpdatable { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.TextUpdatable) + newVal := strconv.FormatBool(license.TextUpdatable) + changes = append(changes, models.ChangeLog{ Field: "TextUpdatable", - OldValue: strconv.FormatBool(oldlicense.TextUpdatable), - UpdatedValue: strconv.FormatBool(license.TextUpdatable), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Fedora != license.Fedora { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Fedora", - OldValue: oldlicense.Fedora, - UpdatedValue: license.Fedora, - } - db.DB.Create(&change) + OldValue: &oldlicense.Fedora, + UpdatedValue: &license.Fedora, + }) } if oldlicense.Flag != license.Flag { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatInt(oldlicense.Flag, 10) + newVal := strconv.FormatInt(license.Flag, 10) + changes = append(changes, models.ChangeLog{ Field: "Flag", - OldValue: strconv.FormatInt(oldlicense.Flag, 10), - UpdatedValue: strconv.FormatInt(license.Flag, 10), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Notes != license.Notes { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Notes", - OldValue: oldlicense.Notes, - UpdatedValue: license.Notes, - } - db.DB.Create(&change) + OldValue: &oldlicense.Notes, + UpdatedValue: &license.Notes, + }) } if oldlicense.DetectorType != license.DetectorType { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatInt(oldlicense.DetectorType, 10) + newVal := strconv.FormatInt(license.DetectorType, 10) + changes = append(changes, models.ChangeLog{ Field: "DetectorType", - OldValue: strconv.FormatInt(oldlicense.DetectorType, 10), - UpdatedValue: strconv.FormatInt(license.DetectorType, 10), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Source != license.Source { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "Source", - OldValue: oldlicense.Source, - UpdatedValue: license.Source, - } - db.DB.Create(&change) + OldValue: &oldlicense.Source, + UpdatedValue: &license.Source, + }) } if oldlicense.SpdxId != license.SpdxId { - change := models.ChangeLog{ - AuditId: audit.Id, + changes = append(changes, models.ChangeLog{ Field: "SpdxId", - OldValue: oldlicense.SpdxId, - UpdatedValue: license.SpdxId, - } - db.DB.Create(&change) + OldValue: &oldlicense.SpdxId, + UpdatedValue: &license.SpdxId, + }) } if oldlicense.Risk != license.Risk { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatInt(oldlicense.Risk, 10) + newVal := strconv.FormatInt(license.Risk, 10) + changes = append(changes, models.ChangeLog{ Field: "Risk", - OldValue: strconv.FormatInt(oldlicense.Risk, 10), - UpdatedValue: strconv.FormatInt(license.Risk, 10), - } - db.DB.Create(&change) + OldValue: &oldVal, + UpdatedValue: &newVal, + }) } if oldlicense.Marydone != license.Marydone { - change := models.ChangeLog{ - AuditId: audit.Id, + oldVal := strconv.FormatBool(oldlicense.Marydone) + newVal := strconv.FormatBool(license.Marydone) + changes = append(changes, models.ChangeLog{ Field: "Marydone", - OldValue: strconv.FormatBool(oldlicense.Marydone), - UpdatedValue: strconv.FormatBool(license.Marydone), + OldValue: &oldVal, + UpdatedValue: &newVal, + }) + } + if (oldlicense.ExternalRef.Data().InternalComment == nil && license.ExternalRef.Data().InternalComment != nil) || + (oldlicense.ExternalRef.Data().InternalComment != nil && license.ExternalRef.Data().InternalComment == nil) || + ((oldlicense.ExternalRef.Data().InternalComment != nil && license.ExternalRef.Data().InternalComment != nil) && (*oldlicense.ExternalRef.Data().InternalComment != *license.ExternalRef.Data().InternalComment)) { + changes = append(changes, models.ChangeLog{ + Field: "ExternalRef.InternalComment", + OldValue: oldlicense.ExternalRef.Data().InternalComment, + UpdatedValue: license.ExternalRef.Data().InternalComment, + }) + } + if (oldlicense.ExternalRef.Data().LicenseExplanation == nil && license.ExternalRef.Data().LicenseExplanation != nil) || + (oldlicense.ExternalRef.Data().LicenseExplanation != nil && license.ExternalRef.Data().LicenseExplanation == nil) || + ((oldlicense.ExternalRef.Data().LicenseExplanation != nil && license.ExternalRef.Data().LicenseExplanation != nil) && (*oldlicense.ExternalRef.Data().LicenseExplanation != *license.ExternalRef.Data().LicenseExplanation)) { + changes = append(changes, models.ChangeLog{ + Field: "ExternalRef.LicenseExplanation", + OldValue: oldlicense.ExternalRef.Data().LicenseExplanation, + UpdatedValue: license.ExternalRef.Data().LicenseExplanation, + }) + } + if (oldlicense.ExternalRef.Data().LicenseSuffix == nil && license.ExternalRef.Data().LicenseSuffix != nil) || + (oldlicense.ExternalRef.Data().LicenseSuffix != nil && license.ExternalRef.Data().LicenseSuffix == nil) || + ((oldlicense.ExternalRef.Data().LicenseSuffix != nil && license.ExternalRef.Data().LicenseSuffix != nil) && (*oldlicense.ExternalRef.Data().LicenseSuffix != *license.ExternalRef.Data().LicenseSuffix)) { + changes = append(changes, models.ChangeLog{ + Field: "ExternalRef.LicenseSuffix", + OldValue: oldlicense.ExternalRef.Data().LicenseSuffix, + UpdatedValue: license.ExternalRef.Data().LicenseSuffix, + }) + } + if (oldlicense.ExternalRef.Data().LicenseVersion == nil && license.ExternalRef.Data().LicenseVersion != nil) || + (oldlicense.ExternalRef.Data().LicenseVersion != nil && license.ExternalRef.Data().LicenseVersion == nil) || + ((oldlicense.ExternalRef.Data().LicenseVersion != nil && license.ExternalRef.Data().LicenseVersion != nil) && (*oldlicense.ExternalRef.Data().LicenseVersion != *license.ExternalRef.Data().LicenseVersion)) { + changes = append(changes, models.ChangeLog{ + Field: "ExternalRef.LicenseVersion", + OldValue: oldlicense.ExternalRef.Data().LicenseVersion, + UpdatedValue: license.ExternalRef.Data().LicenseVersion, + }) + } + + if len(changes) != 0 { + var user models.User + if err := db.DB.Where(models.User{Username: username}).First(&user).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + audit := models.Audit{ + UserId: user.Id, + TypeId: license.Id, + Timestamp: time.Now(), + Type: "license", + ChangeLogs: changes, + } + + if err := db.DB.Create(&audit).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return } - db.DB.Create(&change) } + c.JSON(http.StatusOK, res) } diff --git a/pkg/api/obligationmap.go b/pkg/api/obligationmap.go index fb248c1..b982f72 100644 --- a/pkg/api/obligationmap.go +++ b/pkg/api/obligationmap.go @@ -413,11 +413,13 @@ func createObligationMapChangelog(oldObMaps []models.ObligationMap, newObMaps [] newLicenses = append(newLicenses, strconv.FormatInt(newObMaps[i].RfPk, 10)) } + oldVal := strings.Join(oldLicenses, ",") + newVal := strings.Join(newLicenses, ",") change := models.ChangeLog{ AuditId: audit.Id, Field: "RfPk", - OldValue: strings.Join(oldLicenses, ","), - UpdatedValue: strings.Join(newLicenses, ","), + OldValue: &oldVal, + UpdatedValue: &newVal, } return change } diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index 37e7ed2..0645bc3 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -314,57 +314,65 @@ func UpdateObligation(c *gin.Context) { if oldobligation.Topic != obligation.Topic { changes = append(changes, models.ChangeLog{ Field: "Topic", - OldValue: oldobligation.Topic, - UpdatedValue: obligation.Topic, + OldValue: &oldobligation.Topic, + UpdatedValue: &obligation.Topic, }) } if oldobligation.Type != obligation.Type { changes = append(changes, models.ChangeLog{ Field: "Type", - OldValue: oldobligation.Type, - UpdatedValue: obligation.Type, + OldValue: &oldobligation.Type, + UpdatedValue: &obligation.Type, }) } if oldobligation.Text != obligation.Text { changes = append(changes, models.ChangeLog{ Field: "Text", - OldValue: oldobligation.Text, - UpdatedValue: obligation.Text, + OldValue: &oldobligation.Text, + UpdatedValue: &obligation.Text, }) } if oldobligation.Classification != obligation.Classification { + oldVal := strconv.FormatBool(oldobligation.Modifications) + newVal := strconv.FormatBool(obligation.Modifications) changes = append(changes, models.ChangeLog{ Field: "Classification", - OldValue: oldobligation.Classification, - UpdatedValue: obligation.Classification, + OldValue: &oldVal, + UpdatedValue: &newVal, }) } if oldobligation.Modifications != obligation.Modifications { + oldVal := strconv.FormatBool(oldobligation.Modifications) + newVal := strconv.FormatBool(obligation.Modifications) changes = append(changes, models.ChangeLog{ Field: "Modifications", - OldValue: strconv.FormatBool(oldobligation.Modifications), - UpdatedValue: strconv.FormatBool(obligation.Modifications), + OldValue: &oldVal, + UpdatedValue: &newVal, }) } if oldobligation.Comment != obligation.Comment { changes = append(changes, models.ChangeLog{ Field: "Comment", - OldValue: oldobligation.Comment, - UpdatedValue: obligation.Comment, + OldValue: &oldobligation.Comment, + UpdatedValue: &obligation.Comment, }) } if oldobligation.Active != obligation.Active { + oldVal := strconv.FormatBool(oldobligation.Active) + newVal := strconv.FormatBool(obligation.Active) changes = append(changes, models.ChangeLog{ Field: "Active", - OldValue: strconv.FormatBool(oldobligation.Active), - UpdatedValue: strconv.FormatBool(obligation.Active), + OldValue: &oldVal, + UpdatedValue: &newVal, }) } if oldobligation.TextUpdatable != obligation.TextUpdatable { + oldVal := strconv.FormatBool(oldobligation.TextUpdatable) + newVal := strconv.FormatBool(obligation.TextUpdatable) changes = append(changes, models.ChangeLog{ Field: "TextUpdatable", - OldValue: strconv.FormatBool(oldobligation.TextUpdatable), - UpdatedValue: strconv.FormatBool(obligation.TextUpdatable), + OldValue: &oldVal, + UpdatedValue: &newVal, }) } diff --git a/pkg/models/external_ref_structs.go b/pkg/models/external_ref_structs.go new file mode 100644 index 0000000..fe32c35 --- /dev/null +++ b/pkg/models/external_ref_structs.go @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Siemens AG +// SPDX-FileContributor: Dearsh Oberoi +// +// SPDX-License-Identifier: GPL-2.0-only + +package models + +type LicenseDBSchemaExtension struct { + InternalComment *string `json:"internal_comment,omitempty" example:"comment"` + LicenseExplanation *string `json:"license_explanation,omitempty" example:"explanation of license"` + LicenseSuffix *string `json:"license_suffix,omitempty" example:"license suffix"` + LicenseVersion *string `json:"license_version,omitempty"` +} diff --git a/pkg/models/types.go b/pkg/models/types.go index 987d1ef..1c5cf50 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -8,33 +8,36 @@ package models import ( "time" + + "gorm.io/datatypes" ) // The LicenseDB struct represents a license entity with various attributes and // properties associated with it. // It provides structured storage for license-related information. type LicenseDB struct { - Id int64 `json:"rf_id" gorm:"primary_key;column:rf_id" example:"123"` - Shortname string `json:"rf_shortname" gorm:"unique;not null;column:rf_shortname" binding:"required" example:"MIT"` - Fullname string `json:"rf_fullname" gorm:"column:rf_fullname" example:"MIT License"` - Text string `json:"rf_text" gorm:"column:rf_text" example:"MIT License Text here"` - Url string `json:"rf_url" gorm:"column:rf_url" example:"https://opensource.org/licenses/MIT"` - AddDate time.Time `json:"rf_add_date" gorm:"default:CURRENT_TIMESTAMP;column:rf_add_date" example:"2023-12-01T18:10:25.00+05:30"` - Copyleft bool `json:"rf_copyleft" gorm:"column:rf_copyleft"` - FSFfree bool `json:"rf_FSFfree" gorm:"column:rf_FSFfree"` - OSIapproved bool `json:"rf_OSIapproved" gorm:"column:rf_OSIapproved"` - GPLv2compatible bool `json:"rf_GPLv2compatible" gorm:"column:rf_GPLv2compatible"` - GPLv3compatible bool `json:"rf_GPLv3compatible" gorm:"column:rf_GPLv3compatible"` - Notes string `json:"rf_notes" gorm:"column:rf_notes" example:"This license has been superseded."` - Fedora string `json:"rf_Fedora" gorm:"column:rf_Fedora"` - TextUpdatable bool `json:"rf_text_updatable" gorm:"column:rf_text_updatable"` - DetectorType int64 `json:"rf_detector_type" gorm:"column:rf_detector_type" example:"1"` - Active bool `json:"rf_active" gorm:"column:rf_active"` - Source string `json:"rf_source" gorm:"column:rf_source"` - SpdxId string `json:"rf_spdx_id" gorm:"column:rf_spdx_id" example:"MIT"` - Risk int64 `json:"rf_risk" gorm:"column:rf_risk"` - Flag int64 `json:"rf_flag" gorm:"default:1;column:rf_flag" example:"1"` - Marydone bool `json:"marydone" gorm:"column:marydone"` + Id int64 `json:"rf_id" gorm:"primary_key;column:rf_id" example:"123"` + Shortname string `json:"rf_shortname" gorm:"unique;not null;column:rf_shortname" binding:"required" example:"MIT"` + Fullname string `json:"rf_fullname" gorm:"column:rf_fullname" example:"MIT License"` + Text string `json:"rf_text" gorm:"column:rf_text" example:"MIT License Text here"` + Url string `json:"rf_url" gorm:"column:rf_url" example:"https://opensource.org/licenses/MIT"` + AddDate time.Time `json:"rf_add_date" gorm:"default:CURRENT_TIMESTAMP;column:rf_add_date" example:"2023-12-01T18:10:25.00+05:30"` + Copyleft bool `json:"rf_copyleft" gorm:"column:rf_copyleft"` + FSFfree bool `json:"rf_FSFfree" gorm:"column:rf_FSFfree"` + OSIapproved bool `json:"rf_OSIapproved" gorm:"column:rf_OSIapproved"` + GPLv2compatible bool `json:"rf_GPLv2compatible" gorm:"column:rf_GPLv2compatible"` + GPLv3compatible bool `json:"rf_GPLv3compatible" gorm:"column:rf_GPLv3compatible"` + Notes string `json:"rf_notes" gorm:"column:rf_notes" example:"This license has been superseded."` + Fedora string `json:"rf_Fedora" gorm:"column:rf_Fedora"` + TextUpdatable bool `json:"rf_text_updatable" gorm:"column:rf_text_updatable"` + DetectorType int64 `json:"rf_detector_type" gorm:"column:rf_detector_type" example:"1"` + Active bool `json:"rf_active" gorm:"column:rf_active"` + Source string `json:"rf_source" gorm:"column:rf_source"` + SpdxId string `json:"rf_spdx_id" gorm:"column:rf_spdx_id" example:"MIT"` + Risk int64 `json:"rf_risk" gorm:"column:rf_risk"` + Flag int64 `json:"rf_flag" gorm:"default:1;column:rf_flag" example:"1"` + Marydone bool `json:"marydone" gorm:"column:marydone"` + ExternalRef datatypes.JSONType[LicenseDBSchemaExtension] `json:"external_ref"` } type LicenseJson struct { @@ -84,6 +87,12 @@ type LicenseUpdate struct { Marydone bool `json:"marydone" gorm:"column:marydone"` } +// UpdateExternalRefsJSONPayload struct represents the external ref key value +// pairs for update +type UpdateExternalRefsJSONPayload struct { + ExternalRef map[string]interface{} `json:"external_ref"` +} + // The PaginationMeta struct represents additional metadata associated with a // license retrieval operation. // It contains information that provides context and supplementary details @@ -143,26 +152,27 @@ type LicenseError struct { // The LicenseInput struct represents the input or payload required for creating a license. // It contains various fields that capture the necessary information for defining a license entity. type LicenseInput struct { - Shortname string `json:"rf_shortname" example:"MIT"` - Fullname string `json:"rf_fullname" example:"MIT License"` - Text string `json:"rf_text" example:"MIT License Text here"` - Url string `json:"rf_url" example:"https://opensource.org/licenses/MIT"` - AddDate time.Time `json:"rf_add_date" example:"2023-12-01T18:10:25.00+05:30"` - Copyleft bool `json:"rf_copyleft"` - FSFfree bool `json:"rf_FSFfree"` - OSIapproved bool `json:"rf_OSIapproved"` - GPLv2compatible bool `json:"rf_GPLv2compatible"` - GPLv3compatible bool `json:"rf_GPLv3compatible"` - Notes string `json:"rf_notes" example:"This license has been superseded."` - Fedora string `json:"rf_Fedora"` - TextUpdatable bool `json:"rf_text_updatable"` - DetectorType int64 `json:"rf_detector_type" example:"1"` - Active bool `json:"rf_active"` - Source string `json:"rf_source"` - SpdxId string `json:"rf_spdx_id" example:"MIT"` - Risk int64 `json:"rf_risk"` - Flag int64 `json:"rf_flag" example:"1"` - Marydone bool `json:"marydone"` + Shortname string `json:"rf_shortname" example:"MIT"` + Fullname string `json:"rf_fullname" example:"MIT License"` + Text string `json:"rf_text" example:"MIT License Text here"` + Url string `json:"rf_url" example:"https://opensource.org/licenses/MIT"` + AddDate time.Time `json:"rf_add_date" example:"2023-12-01T18:10:25.00+05:30"` + Copyleft bool `json:"rf_copyleft"` + FSFfree bool `json:"rf_FSFfree"` + OSIapproved bool `json:"rf_OSIapproved"` + GPLv2compatible bool `json:"rf_GPLv2compatible"` + GPLv3compatible bool `json:"rf_GPLv3compatible"` + Notes string `json:"rf_notes" example:"This license has been superseded."` + Fedora string `json:"rf_Fedora"` + TextUpdatable bool `json:"rf_text_updatable"` + DetectorType int64 `json:"rf_detector_type" example:"1"` + Active bool `json:"rf_active"` + Source string `json:"rf_source"` + SpdxId string `json:"rf_spdx_id" example:"MIT"` + Risk int64 `json:"rf_risk"` + Flag int64 `json:"rf_flag" example:"1"` + Marydone bool `json:"marydone"` + ExternalRef datatypes.JSONType[LicenseDBSchemaExtension] `json:"external_ref"` } // User struct is representation of user information. @@ -212,12 +222,12 @@ type Audit struct { // ChangeLog struct represents a change entity with certain attributes and properties type ChangeLog struct { - Id int64 `json:"id" gorm:"primary_key" example:"789"` - Field string `json:"field" example:"rf_text"` - UpdatedValue string `json:"updated_value" example:"New license text"` - OldValue string `json:"old_value" example:"Old license text"` - AuditId int64 `json:"audit_id" example:"456"` - Audit Audit `gorm:"foreignKey:AuditId;references:Id" json:"-"` + Id int64 `json:"id" gorm:"primary_key" example:"789"` + Field string `json:"field" example:"rf_text"` + UpdatedValue *string `json:"updated_value" example:"New license text"` + OldValue *string `json:"old_value" example:"Old license text"` + AuditId int64 `json:"audit_id" example:"456"` + Audit Audit `gorm:"foreignKey:AuditId;references:Id" json:"-"` } // ChangeLogResponse represents the design of API response of change log