From 82b0c3256d58ea965c9379c76e6c4a56eab8df57 Mon Sep 17 00:00:00 2001 From: deo002 Date: Thu, 1 Aug 2024 14:18:41 +0530 Subject: [PATCH] feat(auth): Enable/disable authentication for READ apis via env variable Signed-off-by: deo002 --- .env.example | 1 + cmd/laas/docs/docs.go | 131 ++++++++++++++++- cmd/laas/docs/swagger.json | 131 ++++++++++++++++- cmd/laas/docs/swagger.yaml | 83 ++++++++++- entrypoint.sh | 2 +- pkg/api/api.go | 278 ++++++++++++++++++++++++++++--------- pkg/api/audit.go | 8 +- pkg/api/licenses.go | 6 +- pkg/api/obligationmap.go | 2 + pkg/api/obligations.go | 8 +- pkg/models/types.go | 32 +++++ 11 files changed, 595 insertions(+), 87 deletions(-) diff --git a/.env.example b/.env.example index 602e613..07dd44b 100644 --- a/.env.example +++ b/.env.example @@ -5,3 +5,4 @@ TOKEN_HOUR_LIFESPAN=24 # Secret key to sign tokens (openssl rand -hex 32) API_SECRET=some-random-string +READ_API_AUTHENTICATION_ENABLED=false \ No newline at end of file diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 2d0f22c..7723a2e 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -23,11 +23,42 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/apiCollection": { + "get": { + "description": "Returns the apis which require authentication and which do not", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API Collection" + ], + "summary": "Returns the apis which require authentication and which do not", + "operationId": "getAPICollection", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.APICollectionResponse" + } + }, + "500": { + "description": "Unable to parse swagger docs", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/audits": { "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get all audit records from the server", @@ -76,7 +107,8 @@ const docTemplate = `{ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get a specific audit records by ID", @@ -126,7 +158,8 @@ const docTemplate = `{ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get changelogs of an audit record", @@ -182,7 +215,8 @@ const docTemplate = `{ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get a specific changelog of an audit record by its ID", @@ -267,6 +301,12 @@ const docTemplate = `{ }, "/licenses": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Filter licenses based on different parameters", "consumes": [ "application/json" @@ -452,7 +492,8 @@ const docTemplate = `{ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Export all licenses as a json file", @@ -550,6 +591,12 @@ const docTemplate = `{ }, "/licenses/preview": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get shortnames of all active licenses from the service", "consumes": [ "application/json" @@ -595,6 +642,12 @@ const docTemplate = `{ }, "/licenses/{shortname}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get a single license by its shortname", "consumes": [ "application/json" @@ -743,6 +796,12 @@ const docTemplate = `{ }, "/obligation_maps/license/{license}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get obligation maps for a given license shortname", "consumes": [ "application/json" @@ -782,6 +841,12 @@ const docTemplate = `{ }, "/obligation_maps/topic/{topic}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get obligation maps for a given obligation topic", "consumes": [ "application/json" @@ -943,6 +1008,12 @@ const docTemplate = `{ }, "/obligations": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get all active obligations from the service", "consumes": [ "application/json" @@ -1061,6 +1132,12 @@ const docTemplate = `{ }, "/obligations/export": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Export all obligations as a json file", "produces": [ "application/json" @@ -1156,6 +1233,12 @@ const docTemplate = `{ }, "/obligations/preview": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get topic and type of all active obligations from the service", "consumes": [ "application/json" @@ -1189,6 +1272,12 @@ const docTemplate = `{ }, "/obligations/{topic}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get an active based on given topic", "consumes": [ "application/json" @@ -1332,7 +1421,8 @@ const docTemplate = `{ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Fetches audits corresponding to an obligation", @@ -1392,6 +1482,12 @@ const docTemplate = `{ }, "/search": { "post": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Search licenses on different filters and algorithms", "consumes": [ "application/json" @@ -1591,6 +1687,29 @@ const docTemplate = `{ "datatypes.JSONType-models_LicenseDBSchemaExtension": { "type": "object" }, + "models.APICollection": { + "type": "object", + "properties": { + "authenticated": { + "type": "object" + }, + "unAuthenticated": { + "type": "object" + } + } + }, + "models.APICollectionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.APICollection" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "models.Audit": { "type": "object", "properties": { diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 8ebc341..37d588b 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -16,11 +16,42 @@ }, "basePath": "/api/v1", "paths": { + "/apiCollection": { + "get": { + "description": "Returns the apis which require authentication and which do not", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "API Collection" + ], + "summary": "Returns the apis which require authentication and which do not", + "operationId": "getAPICollection", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.APICollectionResponse" + } + }, + "500": { + "description": "Unable to parse swagger docs", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, "/audits": { "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get all audit records from the server", @@ -69,7 +100,8 @@ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get a specific audit records by ID", @@ -119,7 +151,8 @@ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get changelogs of an audit record", @@ -175,7 +208,8 @@ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Get a specific changelog of an audit record by its ID", @@ -260,6 +294,12 @@ }, "/licenses": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Filter licenses based on different parameters", "consumes": [ "application/json" @@ -445,7 +485,8 @@ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Export all licenses as a json file", @@ -543,6 +584,12 @@ }, "/licenses/preview": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get shortnames of all active licenses from the service", "consumes": [ "application/json" @@ -588,6 +635,12 @@ }, "/licenses/{shortname}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get a single license by its shortname", "consumes": [ "application/json" @@ -736,6 +789,12 @@ }, "/obligation_maps/license/{license}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get obligation maps for a given license shortname", "consumes": [ "application/json" @@ -775,6 +834,12 @@ }, "/obligation_maps/topic/{topic}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get obligation maps for a given obligation topic", "consumes": [ "application/json" @@ -936,6 +1001,12 @@ }, "/obligations": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get all active obligations from the service", "consumes": [ "application/json" @@ -1054,6 +1125,12 @@ }, "/obligations/export": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Export all obligations as a json file", "produces": [ "application/json" @@ -1149,6 +1226,12 @@ }, "/obligations/preview": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get topic and type of all active obligations from the service", "consumes": [ "application/json" @@ -1182,6 +1265,12 @@ }, "/obligations/{topic}": { "get": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Get an active based on given topic", "consumes": [ "application/json" @@ -1325,7 +1414,8 @@ "get": { "security": [ { - "ApiKeyAuth": [] + "ApiKeyAuth": [], + "{}": [] } ], "description": "Fetches audits corresponding to an obligation", @@ -1385,6 +1475,12 @@ }, "/search": { "post": { + "security": [ + { + "ApiKeyAuth": [], + "{}": [] + } + ], "description": "Search licenses on different filters and algorithms", "consumes": [ "application/json" @@ -1584,6 +1680,29 @@ "datatypes.JSONType-models_LicenseDBSchemaExtension": { "type": "object" }, + "models.APICollection": { + "type": "object", + "properties": { + "authenticated": { + "type": "object" + }, + "unAuthenticated": { + "type": "object" + } + } + }, + "models.APICollectionResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/models.APICollection" + }, + "status": { + "type": "integer", + "example": 200 + } + } + }, "models.Audit": { "type": "object", "properties": { diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 5ec108e..6c81df5 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -2,6 +2,21 @@ basePath: /api/v1 definitions: datatypes.JSONType-models_LicenseDBSchemaExtension: type: object + models.APICollection: + properties: + authenticated: + type: object + unAuthenticated: + type: object + type: object + models.APICollectionResponse: + properties: + data: + $ref: '#/definitions/models.APICollection' + status: + example: 200 + type: integer + type: object models.Audit: properties: entity: @@ -764,6 +779,26 @@ info: title: laas (License as a Service) API version: 0.0.9 paths: + /apiCollection: + get: + consumes: + - application/json + description: Returns the apis which require authentication and which do not + operationId: getAPICollection + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.APICollectionResponse' + "500": + description: Unable to parse swagger docs + schema: + $ref: '#/definitions/models.LicenseError' + summary: Returns the apis which require authentication and which do not + tags: + - API Collection /audits: get: consumes: @@ -791,7 +826,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Get audit records tags: - Audits @@ -823,7 +859,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Get an audit record tags: - Audits @@ -859,7 +896,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Get changelogs tags: - Audits @@ -896,7 +934,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Get a changelog tags: - Audits @@ -1003,6 +1042,9 @@ paths: description: Invalid value schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Filter licenses tags: - Licenses @@ -1065,6 +1107,9 @@ paths: description: License with shortname not found schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get a license by shortname tags: - Licenses @@ -1131,7 +1176,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Export all licenses as a json file tags: - Licenses @@ -1201,6 +1247,9 @@ paths: description: Unable to fetch licenses schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get shortnames of all active licenses tags: - Licenses @@ -1253,6 +1302,9 @@ paths: description: No license with given shortname found or no map for schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get maps for a license tags: - Obligations @@ -1279,6 +1331,9 @@ paths: description: No obligation with given topic found or no map for schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get maps for an obligation tags: - Obligations @@ -1402,6 +1457,9 @@ paths: description: No obligations in DB schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get all active obligations tags: - Obligations @@ -1489,6 +1547,9 @@ paths: description: No obligation with given topic found schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get an obligation tags: - Obligations @@ -1569,7 +1630,8 @@ paths: schema: $ref: '#/definitions/models.LicenseError' security: - - ApiKeyAuth: [] + - '{}': [] + ApiKeyAuth: [] summary: Fetches audits corresponding to an obligation tags: - Obligations @@ -1590,6 +1652,9 @@ paths: description: Failed to fetch obligations schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Export all obligations as a json file tags: - Obligations @@ -1651,6 +1716,9 @@ paths: description: OK schema: $ref: '#/definitions/models.ObligationPreviewResponse' + security: + - '{}': [] + ApiKeyAuth: [] summary: Get topic and types of all active obligations tags: - Obligations @@ -1682,6 +1750,9 @@ paths: description: Search algorithm doesn't exist schema: $ref: '#/definitions/models.LicenseError' + security: + - '{}': [] + ApiKeyAuth: [] summary: Search licenses tags: - Licenses diff --git a/entrypoint.sh b/entrypoint.sh index f647b3c..2425002 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -12,7 +12,7 @@ db_password="${DB_PASSWORD:-fossy}" populate_db="${POPULATE_DB:-true}" data_file="/app/licenseRef.json" -printf "TOKEN_HOUR_LIFESPAN=24\nAPI_SECRET=%s\n" $(openssl rand -hex 32) > /app/.env +printf "READ_API_AUTHENTICATION_ENABLED=false\nTOKEN_HOUR_LIFESPAN=24\nAPI_SECRET=%s\n" $(openssl rand -hex 32) > /app/.env /app/laas -host=$db_host -port=$db_port -user=$db_user -dbname=$db_name \ -password=$db_password -datafile="$data_file" -populatedb=$populate_db diff --git a/pkg/api/api.go b/pkg/api/api.go index 0c19164..3e25dbc 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -7,14 +7,18 @@ package api import ( + "encoding/json" "fmt" "net/http" "os" + "regexp" + "strconv" "time" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" + "golang.org/x/exp/maps" "github.com/fossology/LicenseDb/cmd/laas/docs" "github.com/fossology/LicenseDb/pkg/auth" @@ -43,7 +47,10 @@ import ( // @name Authorization // @description Token from /login endpoint -const DEFAULT_PORT = "8080" +const ( + DEFAULT_PORT = "8080" + DEFAULT_READ_API_AUTHENTICATION_ENABLED = false +) func Router() *gin.Engine { @@ -53,6 +60,16 @@ func Router() *gin.Engine { } docs.SwaggerInfo.Host = fmt.Sprintf("localhost:%s", port) + // workaround to add optional authentication: https://github.com/swaggo/swag/issues/1226 + oldSecurityScheme := regexp.MustCompile(`({\s*"ApiKeyAuth":\s*\[\]),\s*"{}":\s*\[\](\s*})`) + docs.SwaggerInfo.SwaggerTemplate = oldSecurityScheme.ReplaceAllString(docs.SwaggerInfo.SwaggerTemplate, "$1$2, {}") + + // Convert the environment variable to a boolean + authEnabled, err := strconv.ParseBool(os.Getenv("READ_API_AUTHENTICATION_ENABLED")) + if err != nil { + authEnabled = DEFAULT_READ_API_AUTHENTICATION_ENABLED + } + // r is a default instance of gin engine r := gin.Default() @@ -65,75 +82,148 @@ func Router() *gin.Engine { // Pagination middleware r.Use(middleware.PaginationMiddleware()) - unAuthorizedv1 := r.Group("/api/v1") - { - licenses := unAuthorizedv1.Group("/licenses") - { - licenses.GET("", FilterLicense) - licenses.GET(":shortname", GetLicense) - licenses.GET("export", ExportLicenses) - licenses.GET("/preview", GetAllLicensePreviews) - } - search := unAuthorizedv1.Group("/search") - { - search.POST("", SearchInLicense) - } - obligations := unAuthorizedv1.Group("/obligations") - { - obligations.GET("", GetAllObligation) - obligations.GET("/preview", GetAllObligationPreviews) - obligations.GET(":topic", GetObligation) - obligations.GET(":topic/audits", GetObligationAudits) - obligations.GET("export", ExportObligations) - } - obMap := unAuthorizedv1.Group("/obligation_maps") - { - obMap.GET("topic/:topic", GetObligationMapByTopic) - obMap.GET("license/:license", GetObligationMapByLicense) - } - health := unAuthorizedv1.Group("/health") - { - health.GET("", GetHealth) - } - login := unAuthorizedv1.Group("/login") + if authEnabled { + unAuthorizedv1 := r.Group("/api/v1") { - login.POST("", auth.Login) + health := unAuthorizedv1.Group("/health") + { + health.GET("", GetHealth) + } + login := unAuthorizedv1.Group("/login") + { + login.POST("", auth.Login) + } + apiCollection := unAuthorizedv1.Group("/apiCollection") + { + apiCollection.GET("", GetAPICollection) + } } - } - authorizedv1 := r.Group("/api/v1") - authorizedv1.Use(middleware.AuthenticationMiddleware()) - { - licenses := authorizedv1.Group("/licenses") - { - licenses.POST("", CreateLicense) - licenses.PATCH(":shortname", UpdateLicense) - licenses.POST("import", ImportLicenses) - } - users := authorizedv1.Group("/users") + authorizedv1 := r.Group("/api/v1") + authorizedv1.Use(middleware.AuthenticationMiddleware()) { - users.GET("", auth.GetAllUser) - users.GET(":id", auth.GetUser) - users.POST("", auth.CreateUser) + licenses := authorizedv1.Group("/licenses") + { + licenses.GET("", FilterLicense) + licenses.GET(":shortname", GetLicense) + licenses.GET("export", ExportLicenses) + licenses.GET("/preview", GetAllLicensePreviews) + licenses.POST("", CreateLicense) + licenses.PATCH(":shortname", UpdateLicense) + licenses.POST("import", ImportLicenses) + } + search := authorizedv1.Group("/search") + { + search.POST("", SearchInLicense) + } + users := authorizedv1.Group("/users") + { + users.GET("", auth.GetAllUser) + users.GET(":id", auth.GetUser) + users.POST("", auth.CreateUser) + } + obligations := authorizedv1.Group("/obligations") + { + obligations.GET("", GetAllObligation) + obligations.GET("/preview", GetAllObligationPreviews) + obligations.GET(":topic", GetObligation) + obligations.GET(":topic/audits", GetObligationAudits) + obligations.GET("export", ExportObligations) + obligations.POST("", CreateObligation) + obligations.POST("import", ImportObligations) + obligations.PATCH(":topic", UpdateObligation) + obligations.DELETE(":topic", DeleteObligation) + } + obMap := authorizedv1.Group("/obligation_maps") + { + obMap.GET("topic/:topic", GetObligationMapByTopic) + obMap.GET("license/:license", GetObligationMapByLicense) + obMap.PATCH("topic/:topic/license", PatchObligationMap) + obMap.PUT("topic/:topic/license", UpdateLicenseInObligationMap) + } + audit := authorizedv1.Group("/audits") + { + audit.GET("", GetAllAudit) + audit.GET(":audit_id", GetAudit) + audit.GET(":audit_id/changes", GetChangeLogs) + audit.GET(":audit_id/changes/:id", GetChangeLogbyId) + } } - audit := authorizedv1.Group("/audits") + } else { + unAuthorizedv1 := r.Group("/api/v1") { - audit.GET("", GetAllAudit) - audit.GET(":audit_id", GetAudit) - audit.GET(":audit_id/changes", GetChangeLogs) - audit.GET(":audit_id/changes/:id", GetChangeLogbyId) + licenses := unAuthorizedv1.Group("/licenses") + { + licenses.GET("", FilterLicense) + licenses.GET(":shortname", GetLicense) + licenses.GET("export", ExportLicenses) + licenses.GET("/preview", GetAllLicensePreviews) + } + search := unAuthorizedv1.Group("/search") + { + search.POST("", SearchInLicense) + } + obligations := unAuthorizedv1.Group("/obligations") + { + obligations.GET("", GetAllObligation) + obligations.GET("/preview", GetAllObligationPreviews) + obligations.GET(":topic", GetObligation) + obligations.GET(":topic/audits", GetObligationAudits) + obligations.GET("export", ExportObligations) + } + obMap := unAuthorizedv1.Group("/obligation_maps") + { + obMap.GET("topic/:topic", GetObligationMapByTopic) + obMap.GET("license/:license", GetObligationMapByLicense) + } + audit := unAuthorizedv1.Group("/audits") + { + audit.GET("", GetAllAudit) + audit.GET(":audit_id", GetAudit) + audit.GET(":audit_id/changes", GetChangeLogs) + audit.GET(":audit_id/changes/:id", GetChangeLogbyId) + } + health := unAuthorizedv1.Group("/health") + { + health.GET("", GetHealth) + } + login := unAuthorizedv1.Group("/login") + { + login.POST("", auth.Login) + } + apiCollection := unAuthorizedv1.Group("/apiCollection") + { + apiCollection.GET("", GetAPICollection) + } } - obligations := authorizedv1.Group("/obligations") - { - obligations.POST("", CreateObligation) - obligations.POST("import", ImportObligations) - obligations.PATCH(":topic", UpdateObligation) - obligations.DELETE(":topic", DeleteObligation) - } - obMap := authorizedv1.Group("/obligation_maps") + + authorizedv1 := r.Group("/api/v1") + authorizedv1.Use(middleware.AuthenticationMiddleware()) { - obMap.PATCH("topic/:topic/license", PatchObligationMap) - obMap.PUT("topic/:topic/license", UpdateLicenseInObligationMap) + licenses := authorizedv1.Group("/licenses") + { + licenses.POST("", CreateLicense) + licenses.PATCH(":shortname", UpdateLicense) + licenses.POST("import", ImportLicenses) + } + users := authorizedv1.Group("/users") + { + users.GET("", auth.GetAllUser) + users.GET(":id", auth.GetUser) + users.POST("", auth.CreateUser) + } + obligations := authorizedv1.Group("/obligations") + { + obligations.POST("", CreateObligation) + obligations.POST("import", ImportObligations) + obligations.PATCH(":topic", UpdateObligation) + obligations.DELETE(":topic", DeleteObligation) + } + obMap := authorizedv1.Group("/obligation_maps") + { + obMap.PATCH("topic/:topic/license", PatchObligationMap) + obMap.PUT("topic/:topic/license", UpdateLicenseInObligationMap) + } } } @@ -195,3 +285,67 @@ func GetHealth(c *gin.Context) { } c.JSON(http.StatusOK, er) } + +// The GetAPICollection function returns the apis which require authentication and which do not +// +// @Summary Returns the apis which require authentication and which do not +// @Description Returns the apis which require authentication and which do not +// @Id getAPICollection +// @Tags API Collection +// @Accept json +// @Produce json +// @Success 200 {object} models.APICollectionResponse +// @Failure 500 {object} models.LicenseError "Unable to parse swagger docs" +// @Router /apiCollection [get] +func GetAPICollection(c *gin.Context) { + var swaggerDocAPISecurityScheme models.SwaggerDocAPISecurityScheme + + if err := json.Unmarshal([]byte(docs.SwaggerInfo.ReadDoc()), &swaggerDocAPISecurityScheme); err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Unable to parse swagger docs", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + authEnabled, err := strconv.ParseBool(os.Getenv("READ_API_AUTHENTICATION_ENABLED")) + if err != nil { + authEnabled = DEFAULT_READ_API_AUTHENTICATION_ENABLED + } + + var unAuthenticatedApis models.LinksCollection + var authenticatedApis models.LinksCollection + unAuthenticatedApis.Links = make(map[string]models.Api) + authenticatedApis.Links = make(map[string]models.Api) + + for _, path := range maps.Keys(swaggerDocAPISecurityScheme.Paths) { + for _, method := range maps.Keys(swaggerDocAPISecurityScheme.Paths[path]) { + if len(swaggerDocAPISecurityScheme.Paths[path][method].Security) == 0 || + (len(swaggerDocAPISecurityScheme.Paths[path][method].Security) == 2 && !authEnabled) { + unAuthenticatedApis.Links[swaggerDocAPISecurityScheme.Paths[path][method].OperationId] = models.Api{ + Href: fmt.Sprintf("%s%s", swaggerDocAPISecurityScheme.BasePath, path), + RequestMethod: method, + } + } else { + authenticatedApis.Links[swaggerDocAPISecurityScheme.Paths[path][method].OperationId] = models.Api{ + Href: fmt.Sprintf("%s%s", swaggerDocAPISecurityScheme.BasePath, path), + RequestMethod: method, + } + } + } + } + + apiCollectionResponse := &models.APICollectionResponse{ + Status: http.StatusOK, + Data: models.APICollection{ + Authenticated: authenticatedApis, + UnAuthenticated: unAuthenticatedApis, + }, + } + + c.JSON(http.StatusOK, apiCollectionResponse) +} diff --git a/pkg/api/audit.go b/pkg/api/audit.go index b81419a..16ca34a 100644 --- a/pkg/api/audit.go +++ b/pkg/api/audit.go @@ -28,7 +28,7 @@ import ( // @Param limit query int false "Number of records per page" // @Success 200 {object} models.AuditResponse "Audit records" // @Failure 404 {object} models.LicenseError "Not changelogs in DB" -// @Security ApiKeyAuth +// @Security ApiKeyAuth || {} // @Router /audits [get] func GetAllAudit(c *gin.Context) { var audits []models.Audit @@ -77,7 +77,7 @@ func GetAllAudit(c *gin.Context) { // @Success 200 {object} models.AuditResponse // @Failure 400 {object} models.LicenseError "Invalid audit ID" // @Failure 404 {object} models.LicenseError "No audit entry with given ID" -// @Security ApiKeyAuth +// @Security ApiKeyAuth || {} // @Router /audits/{audit_id} [get] func GetAudit(c *gin.Context) { var audit models.Audit @@ -127,7 +127,7 @@ func GetAudit(c *gin.Context) { // @Failure 400 {object} models.LicenseError "Invalid audit ID" // @Failure 404 {object} models.LicenseError "No audit entry with given ID" // @Failure 500 {object} models.LicenseError "unable to find changes" -// @Security ApiKeyAuth +// @Security ApiKeyAuth || {} // @Router /audits/{audit_id}/changes [get] func GetChangeLogs(c *gin.Context) { var changelog []models.ChangeLog @@ -186,7 +186,7 @@ func GetChangeLogs(c *gin.Context) { // @Success 200 {object} models.ChangeLogResponse // @Failure 400 {object} models.LicenseError "Invalid ID" // @Failure 404 {object} models.LicenseError "No changelog with given ID found" -// @Security ApiKeyAuth +// @Security ApiKeyAuth || {} // @Router /audits/{audit_id}/changes/{id} [get] func GetChangeLogbyId(c *gin.Context) { var changelog models.ChangeLog diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index db047f7..fff27f6 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -49,6 +49,7 @@ import ( // @Param order_by query string false "Asc or desc ordering" Enums(asc, desc) default(asc) // @Success 200 {object} models.LicenseResponse "Filtered licenses" // @Failure 400 {object} models.LicenseError "Invalid value" +// @Security ApiKeyAuth || {} // @Router /licenses [get] func FilterLicense(c *gin.Context) { SpdxId := c.Query("spdxid") @@ -212,6 +213,7 @@ func FilterLicense(c *gin.Context) { // @Param shortname path string true "Shortname of the license" // @Success 200 {object} models.LicenseResponse // @Failure 404 {object} models.LicenseError "License with shortname not found" +// @Security ApiKeyAuth || {} // @Router /licenses/{shortname} [get] func GetLicense(c *gin.Context) { var license models.LicenseDB @@ -791,6 +793,7 @@ func addChangelogsForLicenseUpdate(tx *gorm.DB, username string, // @Success 200 {object} models.LicenseResponse "Licenses matched" // @Failure 400 {object} models.LicenseError "Invalid request" // @Failure 404 {object} models.LicenseError "Search algorithm doesn't exist" +// @Security ApiKeyAuth || {} // @Router /search [post] func SearchInLicense(c *gin.Context) { var input models.SearchLicense @@ -1120,7 +1123,7 @@ func InsertOrUpdateLicenseOnImport(tx *gorm.DB, newLicenseMap map[string]interfa // @Produce json // @Success 200 {array} models.ExportLicenseDB // @Failure 500 {object} models.LicenseError "Failed to fetch Licenses" -// @Security ApiKeyAuth +// @Security ApiKeyAuth || {} // @Router /licenses/export [get] func ExportLicenses(c *gin.Context) { @@ -1161,6 +1164,7 @@ func ExportLicenses(c *gin.Context) { // @Success 200 {object} models.LicensePreviewResponse // @Failure 400 {object} models.LicenseError "Invalid active value" // @Failure 500 {object} models.LicenseError "Unable to fetch licenses" +// @Security ApiKeyAuth || {} // @Router /licenses/preview [get] func GetAllLicensePreviews(c *gin.Context) { var licenses []models.LicenseDB diff --git a/pkg/api/obligationmap.go b/pkg/api/obligationmap.go index 7d777a1..ced78fb 100644 --- a/pkg/api/obligationmap.go +++ b/pkg/api/obligationmap.go @@ -33,6 +33,7 @@ import ( // @Success 200 {object} models.ObligationMapResponse // @Failure 404 {object} models.LicenseError "No obligation with given topic found or no map for // obligation exists" +// @Security ApiKeyAuth || {} // @Router /obligation_maps/topic/{topic} [get] func GetObligationMapByTopic(c *gin.Context) { var obligation models.Obligation @@ -110,6 +111,7 @@ func GetObligationMapByTopic(c *gin.Context) { // @Success 200 {object} models.ObligationMapResponse // @Failure 404 {object} models.LicenseError "No license with given shortname found or no map for // license exists" +// @Security ApiKeyAuth || {} // @Router /obligation_maps/license/{license} [get] func GetObligationMapByLicense(c *gin.Context) { var license models.LicenseDB diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index d440adb..7c37986 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -40,6 +40,7 @@ import ( // @Param order_by query string false "Asc or desc ordering" Enums(asc, desc) default(asc) // @Success 200 {object} models.ObligationResponse // @Failure 404 {object} models.LicenseError "No obligations in DB" +// @Security ApiKeyAuth || {} // @Router /obligations [get] func GetAllObligation(c *gin.Context) { var obligations []models.Obligation @@ -107,6 +108,7 @@ func GetAllObligation(c *gin.Context) { // @Param topic path string true "Topic of the obligation" // @Success 200 {object} models.ObligationResponse // @Failure 404 {object} models.LicenseError "No obligation with given topic found" +// @Security ApiKeyAuth || {} // @Router /obligations/{topic} [get] func GetObligation(c *gin.Context) { var obligation models.Obligation @@ -438,7 +440,9 @@ func DeleteObligation(c *gin.Context) { // @Success 200 {object} models.AuditResponse // @Failure 404 {object} models.LicenseError "No obligation with given topic found" // @Failure 500 {object} models.LicenseError "unable to find audits with such obligation topic" -// @Security ApiKeyAuth +// +// @Security ApiKeyAuth || {} +// // @Router /obligations/{topic}/audits [get] func GetObligationAudits(c *gin.Context) { var obligation models.Obligation @@ -642,6 +646,7 @@ func ImportObligations(c *gin.Context) { // @Produce json // @Success 200 {array} models.ObligationJSONFileFormat // @Failure 500 {object} models.LicenseError "Failed to fetch obligations" +// @Security ApiKeyAuth || {} // @Router /obligations/export [get] func ExportObligations(c *gin.Context) { var obligations []models.Obligation @@ -803,6 +808,7 @@ func addChangelogsForObligationUpdate(tx *gorm.DB, username string, // @Produce json // @Param active query bool true "Active obligation only" // @Success 200 {object} models.ObligationPreviewResponse +// @Security ApiKeyAuth || {} // @Router /obligations/preview [get] func GetAllObligationPreviews(c *gin.Context) { var obligations []models.Obligation diff --git a/pkg/models/types.go b/pkg/models/types.go index f2cd5cc..d13f473 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -451,3 +451,35 @@ type ImportObligationsResponse struct { Status int `json:"status" example:"200"` Data []interface{} `json:"data"` // can be of type models.LicenseError or models.ObligationImportStatus } + +// Api contains the information about an endpoint +type Api struct { + Href string `json:"href" example:"/api/v1/licenses"` + RequestMethod string `json:"request_method" enums:"POST,PATCH,DELETE,PUT,GET" example:"POST"` +} + +// LinksCollection is a collection of links +type LinksCollection struct { + Links map[string]Api `json:"_links" swaggertype:"object,string" example:"licenses:{}"` +} + +// APICollection is the object that lists which apis require authentication and which do not +type APICollection struct { + Authenticated LinksCollection `json:"authenticated" swaggertype:"object"` + UnAuthenticated LinksCollection `json:"unAuthenticated" swaggertype:"object"` +} + +// APICollectionResponse represents the response format for api collection data. +type APICollectionResponse struct { + Status int `json:"status" example:"200"` + Data APICollection `json:"data"` +} + +// SwaggerDocAPISecurityScheme is the json schema describing info about various apis +type SwaggerDocAPISecurityScheme struct { + BasePath string `json:"basePath" example:"/api/v1"` + Paths map[string]map[string]struct { + Security []map[string]interface{} `json:"security" swaggertype:"array,object"` + OperationId string `json:"operationId" example:"GetLicense"` + } `json:"paths"` +}