From 3f5dc62b0f21b1d95f19411034818aaf7216cf4a Mon Sep 17 00:00:00 2001 From: deo002 Date: Tue, 3 Dec 2024 13:40:32 +0530 Subject: [PATCH] feat(oidc_auth): Add backend support for OIDC Auth Signed-off-by: deo002 --- .env.example | 27 +- .github/workflows/api-swagger.yml | 4 +- .github/workflows/go.yml | 2 +- .github/workflows/golangci.yml | 10 +- Dockerfile | 2 +- cmd/laas/docs/docs.go | 305 +++++++++++++- cmd/laas/docs/swagger.json | 305 +++++++++++++- cmd/laas/docs/swagger.yaml | 212 +++++++++- cmd/laas/main.go | 22 + go.mod | 78 ++-- go.sum | 195 ++++----- pkg/api/api.go | 56 ++- pkg/api/api_test.go | 14 +- pkg/api/licenses.go | 2 +- pkg/api/obligationClassifications.go | 2 +- pkg/api/obligationTypes.go | 2 +- pkg/api/obligations.go | 2 +- pkg/auth/auth.go | 610 ++++++++++++++++++++++++--- pkg/db/db.go | 2 +- pkg/middleware/middleware.go | 243 +++++++++-- pkg/models/types.go | 78 +++- pkg/utils/util.go | 25 +- 22 files changed, 1889 insertions(+), 309 deletions(-) diff --git a/.env.example b/.env.example index 07dd44b..76df509 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,29 @@ 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 +READ_API_AUTHENTICATION_ENABLED=false + +PORT=8080 + +# OIDC Provider (To be set if OIDC Authentication support required) +# The URL for retrieving keys for Token Parsing +JWKS_URI=https://provider/keys + +# The field in ID Token that is to be used as username +OIDC_USERNAME_KEY=employee_id + +# The field in ID Token that is to be used as email +OIDC_EMAIL_KEY=mail + +# The issuer url +OIDC_ISSUER=https://provider + +# The field in ID Token that is used as display name +OIDC_DISPLAYNAME_KEY=display_name + +# Some OIDC providers do not provide the "alg" header in their key set(ex. AzureAD) +# This env variable, if set, will be used for signing while verifying the JWT signature +# (Make sure it's same as the signing algorithm used by the provider) +# +# For OIDC providers that provide the "alg" header in their key set, there is no need for this to be set +OIDC_SIGNING_ALG=RS256 \ No newline at end of file diff --git a/.github/workflows/api-swagger.yml b/.github/workflows/api-swagger.yml index a963b81..a52428d 100644 --- a/.github/workflows/api-swagger.yml +++ b/.github/workflows/api-swagger.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' check-latest: true cache: true @@ -54,7 +54,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' check-latest: true cache: true diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 03dc834..ba031c2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.20' + go-version: '1.22' check-latest: true cache: true diff --git a/.github/workflows/golangci.yml b/.github/workflows/golangci.yml index 0b05719..8d2318b 100644 --- a/.github/workflows/golangci.yml +++ b/.github/workflows/golangci.yml @@ -18,12 +18,12 @@ jobs: golangci-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.22' check-latest: true cache: true @@ -31,7 +31,7 @@ jobs: run: cp external_ref_fields.example.yaml external_ref_fields.yaml && go generate ./... - name: lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.55 + version: v1.62.2 diff --git a/Dockerfile b/Dockerfile index d7675ba..b281fe9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2024 Kaushlendra Pratap # SPDX-License-Identifier: GPL-2.0-only -FROM golang:1.20 AS build +FROM golang:1.22 AS build WORKDIR /LicenseDb diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index bfc9a7a..c1c185c 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -790,6 +790,12 @@ const docTemplate = `{ } } } + }, + "401": { + "description": "Incorrect username or password", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } } } } @@ -1859,6 +1865,12 @@ const docTemplate = `{ "summary": "Get users", "operationId": "GetAllUsers", "parameters": [ + { + "type": "boolean", + "description": "Active user only", + "name": "active", + "in": "query" + }, { "type": "integer", "description": "Page number", @@ -1912,7 +1924,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.UserInput" + "$ref": "#/definitions/models.UserCreate" } } ], @@ -1936,16 +1948,107 @@ const docTemplate = `{ } } } + }, + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Users can update their profile using this endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Users can update their profile using this endpoint", + "operationId": "UpdateProfile", + "parameters": [ + { + "description": "Profile fields to update", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ProfileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } } }, - "/users/{id}": { + "/users/oidc": { + "post": { + "description": "Create a new service user via oidc id token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create new user via oidc id token", + "operationId": "CreateOidcUser", + "parameters": [ + { + "description": "User to create", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.OidcUserCreate" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "User already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users/{username}": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get a single user by ID", + "description": "Get a single user by username", "consumes": [ "application/json" ], @@ -1959,9 +2062,9 @@ const docTemplate = `{ "operationId": "GetUser", "parameters": [ { - "type": "integer", - "description": "User ID", - "name": "id", + "type": "string", + "description": "Username", + "name": "username", "in": "path", "required": true } @@ -1986,6 +2089,102 @@ const docTemplate = `{ } } } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Deactivate an user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Deactivate user", + "operationId": "DeleteUser", + "parameters": [ + { + "type": "string", + "description": "Username of the user to be marked as inactive", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "No user with given username found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a service user, requires admin rights", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update user, requires admin rights", + "operationId": "UpdateUser", + "parameters": [ + { + "type": "string", + "description": "username of the user to be updated", + "name": "username", + "in": "path", + "required": true + }, + { + "description": "User to update", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "403": { + "description": "This resource requires elevated access rights", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } } } }, @@ -2756,6 +2955,14 @@ const docTemplate = `{ } } }, + "models.OidcUserCreate": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "models.PaginationMeta": { "type": "object", "properties": { @@ -2785,6 +2992,21 @@ const docTemplate = `{ } } }, + "models.ProfileUpdate": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "example": "fossy" + }, + "user_email": { + "type": "string" + }, + "user_password": { + "type": "string" + } + } + }, "models.SearchLicense": { "type": "object", "required": [ @@ -2829,18 +3051,22 @@ const docTemplate = `{ }, "models.User": { "type": "object", - "required": [ - "userlevel", - "username" - ], "properties": { + "display_name": { + "type": "string", + "example": "fossy" + }, "id": { "type": "integer", "example": 123 }, - "userlevel": { + "user_email": { + "type": "string", + "example": "fossy@org.com" + }, + "user_level": { "type": "string", - "example": "admin" + "example": "USER" }, "username": { "type": "string", @@ -2848,21 +3074,34 @@ const docTemplate = `{ } } }, - "models.UserInput": { + "models.UserCreate": { "type": "object", "required": [ - "password", - "userlevel", + "display_name", + "user_email", + "user_level", "username" ], "properties": { - "password": { + "display_name": { "type": "string", "example": "fossy" }, - "userlevel": { + "user_email": { "type": "string", - "example": "admin" + "example": "fossy@org.com" + }, + "user_level": { + "type": "string", + "enum": [ + "USER", + "ADMIN" + ], + "example": "ADMIN" + }, + "user_password": { + "type": "string", + "example": "fossy" }, "username": { "type": "string", @@ -2904,6 +3143,36 @@ const docTemplate = `{ "example": 200 } } + }, + "models.UserUpdate": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "display_name": { + "type": "string", + "example": "fossy" + }, + "user_email": { + "type": "string" + }, + "user_level": { + "type": "string", + "enum": [ + "USER", + "ADMIN" + ], + "example": "ADMIN" + }, + "user_password": { + "type": "string" + }, + "username": { + "type": "string", + "example": "fossy" + } + } } }, "securityDefinitions": { diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 34ddbe6..fdfea66 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -783,6 +783,12 @@ } } } + }, + "401": { + "description": "Incorrect username or password", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } } } } @@ -1852,6 +1858,12 @@ "summary": "Get users", "operationId": "GetAllUsers", "parameters": [ + { + "type": "boolean", + "description": "Active user only", + "name": "active", + "in": "query" + }, { "type": "integer", "description": "Page number", @@ -1905,7 +1917,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.UserInput" + "$ref": "#/definitions/models.UserCreate" } } ], @@ -1929,16 +1941,107 @@ } } } + }, + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Users can update their profile using this endpoint", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Users can update their profile using this endpoint", + "operationId": "UpdateProfile", + "parameters": [ + { + "description": "Profile fields to update", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ProfileUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } } }, - "/users/{id}": { + "/users/oidc": { + "post": { + "description": "Create a new service user via oidc id token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Create new user via oidc id token", + "operationId": "CreateOidcUser", + "parameters": [ + { + "description": "User to create", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.OidcUserCreate" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "409": { + "description": "User already exists", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + } + }, + "/users/{username}": { "get": { "security": [ { "ApiKeyAuth": [] } ], - "description": "Get a single user by ID", + "description": "Get a single user by username", "consumes": [ "application/json" ], @@ -1952,9 +2055,9 @@ "operationId": "GetUser", "parameters": [ { - "type": "integer", - "description": "User ID", - "name": "id", + "type": "string", + "description": "Username", + "name": "username", "in": "path", "required": true } @@ -1979,6 +2082,102 @@ } } } + }, + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Deactivate an user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Deactivate user", + "operationId": "DeleteUser", + "parameters": [ + { + "type": "string", + "description": "Username of the user to be marked as inactive", + "name": "username", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "No user with given username found", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } + }, + "patch": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "Update a service user, requires admin rights", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Users" + ], + "summary": "Update user, requires admin rights", + "operationId": "UpdateUser", + "parameters": [ + { + "type": "string", + "description": "username of the user to be updated", + "name": "username", + "in": "path", + "required": true + }, + { + "description": "User to update", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserUpdate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserResponse" + } + }, + "400": { + "description": "Invalid json body", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + }, + "403": { + "description": "This resource requires elevated access rights", + "schema": { + "$ref": "#/definitions/models.LicenseError" + } + } + } } } }, @@ -2749,6 +2948,14 @@ } } }, + "models.OidcUserCreate": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, "models.PaginationMeta": { "type": "object", "properties": { @@ -2778,6 +2985,21 @@ } } }, + "models.ProfileUpdate": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "example": "fossy" + }, + "user_email": { + "type": "string" + }, + "user_password": { + "type": "string" + } + } + }, "models.SearchLicense": { "type": "object", "required": [ @@ -2822,18 +3044,22 @@ }, "models.User": { "type": "object", - "required": [ - "userlevel", - "username" - ], "properties": { + "display_name": { + "type": "string", + "example": "fossy" + }, "id": { "type": "integer", "example": 123 }, - "userlevel": { + "user_email": { + "type": "string", + "example": "fossy@org.com" + }, + "user_level": { "type": "string", - "example": "admin" + "example": "USER" }, "username": { "type": "string", @@ -2841,21 +3067,34 @@ } } }, - "models.UserInput": { + "models.UserCreate": { "type": "object", "required": [ - "password", - "userlevel", + "display_name", + "user_email", + "user_level", "username" ], "properties": { - "password": { + "display_name": { "type": "string", "example": "fossy" }, - "userlevel": { + "user_email": { "type": "string", - "example": "admin" + "example": "fossy@org.com" + }, + "user_level": { + "type": "string", + "enum": [ + "USER", + "ADMIN" + ], + "example": "ADMIN" + }, + "user_password": { + "type": "string", + "example": "fossy" }, "username": { "type": "string", @@ -2897,6 +3136,36 @@ "example": 200 } } + }, + "models.UserUpdate": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "display_name": { + "type": "string", + "example": "fossy" + }, + "user_email": { + "type": "string" + }, + "user_level": { + "type": "string", + "enum": [ + "USER", + "ADMIN" + ], + "example": "ADMIN" + }, + "user_password": { + "type": "string" + }, + "username": { + "type": "string", + "example": "fossy" + } + } } }, "securityDefinitions": { diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 358b74b..acc45da 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -538,6 +538,11 @@ definitions: example: RISK type: string type: object + models.OidcUserCreate: + properties: + token: + type: string + type: object models.PaginationMeta: properties: limit: @@ -559,6 +564,16 @@ definitions: example: 20 type: integer type: object + models.ProfileUpdate: + properties: + display_name: + example: fossy + type: string + user_email: + type: string + user_password: + type: string + type: object models.SearchLicense: properties: field: @@ -590,33 +605,46 @@ definitions: type: object models.User: properties: + display_name: + example: fossy + type: string id: example: 123 type: integer - userlevel: - example: admin + user_email: + example: fossy@org.com + type: string + user_level: + example: USER type: string username: example: fossy type: string - required: - - userlevel - - username type: object - models.UserInput: + models.UserCreate: properties: - password: + display_name: example: fossy type: string - userlevel: - example: admin + user_email: + example: fossy@org.com + type: string + user_level: + enum: + - USER + - ADMIN + example: ADMIN + type: string + user_password: + example: fossy type: string username: example: fossy type: string required: - - password - - userlevel + - display_name + - user_email + - user_level - username type: object models.UserLogin: @@ -643,6 +671,27 @@ definitions: example: 200 type: integer type: object + models.UserUpdate: + properties: + active: + type: boolean + display_name: + example: fossy + type: string + user_email: + type: string + user_level: + enum: + - USER + - ADMIN + example: ADMIN + type: string + user_password: + type: string + username: + example: fossy + type: string + type: object info: contact: email: fossology@fossology.org @@ -1153,6 +1202,10 @@ paths: token: type: string type: object + "401": + description: Incorrect username or password + schema: + $ref: '#/definitions/models.LicenseError' summary: Login tags: - Users @@ -1838,6 +1891,10 @@ paths: description: Get all service users operationId: GetAllUsers parameters: + - description: Active user only + in: query + name: active + type: boolean - description: Page number in: query name: page @@ -1862,6 +1919,34 @@ paths: summary: Get users tags: - Users + patch: + consumes: + - application/json + description: Users can update their profile using this endpoint + operationId: UpdateProfile + parameters: + - description: Profile fields to update + in: body + name: user + required: true + schema: + $ref: '#/definitions/models.ProfileUpdate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + security: + - ApiKeyAuth: [] + summary: Users can update their profile using this endpoint + tags: + - Users post: consumes: - application/json @@ -1873,7 +1958,7 @@ paths: name: user required: true schema: - $ref: '#/definitions/models.UserInput' + $ref: '#/definitions/models.UserCreate' produces: - application/json responses: @@ -1894,18 +1979,43 @@ paths: summary: Create new user tags: - Users - /users/{id}: + /users/{username}: + delete: + consumes: + - application/json + description: Deactivate an user + operationId: DeleteUser + parameters: + - description: Username of the user to be marked as inactive + in: path + name: username + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: No user with given username found + schema: + $ref: '#/definitions/models.LicenseError' + security: + - ApiKeyAuth: [] + summary: Deactivate user + tags: + - Users get: consumes: - application/json - description: Get a single user by ID + description: Get a single user by username operationId: GetUser parameters: - - description: User ID + - description: Username in: path - name: id + name: username required: true - type: integer + type: string produces: - application/json responses: @@ -1926,6 +2036,74 @@ paths: summary: Get a user tags: - Users + patch: + consumes: + - application/json + description: Update a service user, requires admin rights + operationId: UpdateUser + parameters: + - description: username of the user to be updated + in: path + name: username + required: true + type: string + - description: User to update + in: body + name: user + required: true + schema: + $ref: '#/definitions/models.UserUpdate' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + "403": + description: This resource requires elevated access rights + schema: + $ref: '#/definitions/models.LicenseError' + security: + - ApiKeyAuth: [] + summary: Update user, requires admin rights + tags: + - Users + /users/oidc: + post: + consumes: + - application/json + description: Create a new service user via oidc id token + operationId: CreateOidcUser + parameters: + - description: User to create + in: body + name: user + required: true + schema: + $ref: '#/definitions/models.OidcUserCreate' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.UserResponse' + "400": + description: Invalid json body + schema: + $ref: '#/definitions/models.LicenseError' + "409": + description: User already exists + schema: + $ref: '#/definitions/models.LicenseError' + summary: Create new user via oidc id token + tags: + - Users securityDefinitions: ApiKeyAuth: description: Token from /login endpoint diff --git a/cmd/laas/main.go b/cmd/laas/main.go index 7080ed9..d2e7059 100644 --- a/cmd/laas/main.go +++ b/cmd/laas/main.go @@ -7,15 +7,20 @@ package main import ( + "context" "flag" "log" + "os" "github.com/joho/godotenv" + "github.com/lestrrat-go/httprc/v3" + "github.com/lestrrat-go/jwx/v3/jwk" "gorm.io/gorm/clause" _ "github.com/dave/jennifer/jen" _ "github.com/fossology/LicenseDb/cmd/laas/docs" "github.com/fossology/LicenseDb/pkg/api" + "github.com/fossology/LicenseDb/pkg/auth" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" "github.com/fossology/LicenseDb/pkg/utils" @@ -48,6 +53,23 @@ func main() { flag.Parse() + if os.Getenv("TOKEN_HOUR_LIFESPAN") == "" || os.Getenv("API_SECRET") == "" || os.Getenv("DEFAULT_ISSUER") == "" { + log.Fatal("Mandatory environment variables not configured") + } + + if os.Getenv("JWKS_URI") != "" { + cache, err := jwk.NewCache(context.Background(), httprc.NewClient()) + if err != nil { + log.Fatalf("Failed to create a jwk.Cache from the oidc provider's URL: %s", err) + } + + if err := cache.Register(context.Background(), os.Getenv("JWKS_URI")); err != nil { + log.Fatalf("Failed to create a jwk.Cache from the oidc provider's URL: %s", err) + } + + auth.Jwks = cache + } + db.Connect(dbhost, port, user, dbname, password) if err := db.DB.AutoMigrate(&models.LicenseDB{}); err != nil { diff --git a/go.mod b/go.mod index 56e139f..4346434 100644 --- a/go.mod +++ b/go.mod @@ -1,71 +1,79 @@ module github.com/fossology/LicenseDb -go 1.20 +go 1.22.6 + +toolchain go1.22.10 require ( - github.com/gin-gonic/gin v1.9.1 - github.com/golang-jwt/jwt/v4 v4.5.1 + github.com/gin-gonic/gin v1.10.0 github.com/joho/godotenv v1.5.1 + github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 + github.com/lestrrat-go/jwx/v3 v3.0.0-alpha1 github.com/stretchr/testify v1.9.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.16.2 + github.com/swaggo/swag v1.16.4 golang.org/x/crypto v0.31.0 - golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 - gorm.io/driver/postgres v1.5.2 - gorm.io/gorm v1.25.1 + golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 ) require ( - github.com/go-sql-driver/mysql v1.7.0 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic/loader v0.2.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect golang.org/x/sync v0.10.0 // indirect - gorm.io/driver/mysql v1.4.7 // indirect + gorm.io/driver/mysql v1.5.7 // indirect ) require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/dave/jennifer v1.7.0 + github.com/bytedance/sonic v1.12.6 // indirect + github.com/dave/jennifer v1.7.1 github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.7 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.6 // indirect - github.com/go-openapi/spec v0.20.4 // indirect - github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 - github.com/goccy/go-json v0.10.2 // indirect + github.com/go-playground/validator/v10 v10.23.0 + github.com/goccy/go-json v0.10.4 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.6 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.12.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/tools v0.28.0 // indirect + google.golang.org/protobuf v1.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/datatypes v1.2.0 + gorm.io/datatypes v1.2.5 ) diff --git a/go.sum b/go.sum index 84ffa3f..5a3639e 100644 --- a/go.sum +++ b/go.sum @@ -1,66 +1,71 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= -github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= +github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= +github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= -github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= -github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= -github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= -github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= -github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= -github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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= @@ -70,36 +75,46 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 h1:pzDjP9dSONCFQC/AE3mWUnHILGiYPiMKzQIS+weKJXA= +github.com/lestrrat-go/httprc/v3 v3.0.0-beta1/go.mod h1:wdsgouffPvWPEYh8t7PRH/PidR5sfVqt0na4Nhj60Ms= +github.com/lestrrat-go/jwx/v3 v3.0.0-alpha1 h1:IKsSdax3m7zsi4ooThn7YR74PMsx8fqcLcEeA6164nI= +github.com/lestrrat-go/jwx/v3 v3.0.0-alpha1/go.mod h1:JLWHVwLtN56LfSrlpyjhvKEdG00MTYOrmzLIJkrCeDw= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -109,38 +124,34 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= -github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= -github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A= +github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= +golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= -golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo= +golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= +golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -149,10 +160,8 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -163,7 +172,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= @@ -171,31 +179,28 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I= +gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= -gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= -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= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= +gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/pkg/api/api.go b/pkg/api/api.go index 36e3224..b0260cf 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -97,6 +97,10 @@ func Router() *gin.Engine { { apiCollection.GET("", GetAPICollection) } + oidc := unAuthorizedv1.Group("/users/oidc") + { + oidc.POST("", auth.CreateOidcUser) + } } authorizedv1 := r.Group("/api/v1") @@ -110,7 +114,7 @@ func Router() *gin.Engine { licenses.GET("/preview", GetAllLicensePreviews) licenses.POST("", CreateLicense) licenses.PATCH(":shortname", UpdateLicense) - licenses.POST("import", ImportLicenses) + licenses.POST("import", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), ImportLicenses) } search := authorizedv1.Group("/search") { @@ -118,9 +122,12 @@ func Router() *gin.Engine { } users := authorizedv1.Group("/users") { - users.GET("", auth.GetAllUser) - users.GET(":id", auth.GetUser) - users.POST("", auth.CreateUser) + users.GET("", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.GetAllUser) + users.GET(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.GetUser) + users.POST("", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.CreateUser) + users.PATCH("", auth.UpdateProfile) + users.PATCH(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.UpdateUser) + users.DELETE(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.DeleteUser) } obligations := authorizedv1.Group("/obligations") { @@ -130,15 +137,15 @@ func Router() *gin.Engine { obligations.GET(":topic/audits", GetObligationAudits) obligations.GET("export", ExportObligations) obligations.POST("", CreateObligation) - obligations.POST("import", ImportObligations) + obligations.POST("import", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), ImportObligations) obligations.PATCH(":topic", UpdateObligation) obligations.DELETE(":topic", DeleteObligation) - obligations.GET("/types", GetAllObligationType) - obligations.POST("/types", CreateObligationType) - obligations.DELETE("/types/:type", DeleteObligationType) - obligations.GET("/classifications", GetAllObligationClassification) - obligations.POST("/classifications", CreateObligationClassification) - obligations.DELETE("/classifications/:classification", DeleteObligationClassification) + obligations.GET("/types", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), GetAllObligationType) + obligations.POST("/types", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), CreateObligationType) + obligations.DELETE("/types/:type", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationType) + obligations.GET("/classifications", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), GetAllObligationClassification) + obligations.POST("/classifications", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), CreateObligationClassification) + obligations.DELETE("/classifications/:classification", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationClassification) } obMap := authorizedv1.Group("/obligation_maps") { @@ -201,6 +208,10 @@ func Router() *gin.Engine { { apiCollection.GET("", GetAPICollection) } + oidc := unAuthorizedv1.Group("/users/oidc") + { + oidc.POST("", auth.CreateOidcUser) + } } authorizedv1 := r.Group("/api/v1") @@ -214,22 +225,25 @@ func Router() *gin.Engine { } users := authorizedv1.Group("/users") { - users.GET("", auth.GetAllUser) - users.GET(":id", auth.GetUser) - users.POST("", auth.CreateUser) + users.GET("", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.GetAllUser) + users.GET(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.GetUser) + users.POST("", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.CreateUser) + users.PATCH(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.UpdateUser) + users.PATCH("", auth.UpdateProfile) + users.DELETE(":username", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), auth.DeleteUser) } obligations := authorizedv1.Group("/obligations") { obligations.POST("", CreateObligation) - obligations.POST("import", ImportObligations) + obligations.POST("import", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), ImportObligations) obligations.PATCH(":topic", UpdateObligation) obligations.DELETE(":topic", DeleteObligation) - obligations.GET("/types", GetAllObligationType) - obligations.POST("/types", CreateObligationType) - obligations.DELETE("/types/:type", DeleteObligationType) - obligations.GET("/classifications", GetAllObligationClassification) - obligations.POST("/classifications", CreateObligationClassification) - obligations.DELETE("/classifications/:classification", DeleteObligationClassification) + obligations.GET("/types", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), GetAllObligationType) + obligations.POST("/types", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), CreateObligationType) + obligations.DELETE("/types/:type", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationType) + obligations.GET("/classifications", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), GetAllObligationClassification) + obligations.POST("/classifications", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), CreateObligationClassification) + obligations.DELETE("/classifications/:classification", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationClassification) } obMap := authorizedv1.Group("/obligation_maps") { diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index ce4766a..e9c147d 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -205,12 +205,14 @@ func TestSearchInLicense2(t *testing.T) { func TestGetUser(t *testing.T) { password := "fossy" + username := "fossy" + userlevel := "ADMIN" expectUser := models.User{ - Username: "fossy", + Username: &username, Userpassword: &password, - Userlevel: "admin", + Userlevel: &userlevel, } - w := makeRequest("GET", "/api/user/1", nil, false) + w := makeRequest("GET", "/api/user/fossy", nil, false) assert.Equal(t, http.StatusOK, w.Code) var res models.UserResponse @@ -225,10 +227,12 @@ func TestGetUser(t *testing.T) { func TestCreateUser(t *testing.T) { password := "abc123" + username := "fossy" + userlevel := "ADMIN" user := models.User{ - Username: "general_user", + Username: &username, Userpassword: &password, - Userlevel: "participant", + Userlevel: &userlevel, } w := makeRequest("POST", "/api/user", user, true) assert.Equal(t, http.StatusOK, w.Code) diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index 8260528..15abbe3 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -718,7 +718,7 @@ func addChangelogsForLicenseUpdate(tx *gorm.DB, username string, if len(changes) != 0 { var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + if err := tx.Where(models.User{Username: &username}).First(&user).Error; err != nil { return err } diff --git a/pkg/api/obligationClassifications.go b/pkg/api/obligationClassifications.go index 689eaeb..9a71000 100644 --- a/pkg/api/obligationClassifications.go +++ b/pkg/api/obligationClassifications.go @@ -256,7 +256,7 @@ func toggleObligationClassificationActiveStatus(c *gin.Context, tx *gorm.DB, obC username := c.GetString("username") var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + if err := tx.Where(models.User{Username: &username}).First(&user).Error; err != nil { return errors.New("unable to change 'active' status of obligation classification") } diff --git a/pkg/api/obligationTypes.go b/pkg/api/obligationTypes.go index df90612..b6e6a1d 100644 --- a/pkg/api/obligationTypes.go +++ b/pkg/api/obligationTypes.go @@ -256,7 +256,7 @@ func toggleObligationTypeActiveStatus(c *gin.Context, tx *gorm.DB, obType *model username := c.GetString("username") var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + if err := tx.Where(models.User{Username: &username}).First(&user).Error; err != nil { return errors.New("unable to change 'active' status of obligation type") } diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index f55017d..99ded14 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -618,7 +618,7 @@ func ExportObligations(c *gin.Context) { func addChangelogsForObligationUpdate(tx *gorm.DB, username string, newObligation, oldObligation *models.Obligation) error { var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + if err := tx.Where(models.User{Username: &username}).First(&user).Error; err != nil { return err } var changes []models.ChangeLog diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 0ca831d..e879ba4 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -1,20 +1,32 @@ // SPDX-FileCopyrightText: 2023 Kavya Shukla // SPDX-FileCopyrightText: 2023 Siemens AG // SPDX-FileContributor: Gaurav Mishra +// SPDX-FileCopyrightText: 2024 Dearsh Oberoi // // SPDX-License-Identifier: GPL-2.0-only package auth import ( + "context" + "errors" "fmt" + "html" + "log" "net/http" "os" "strconv" + "strings" "time" - "github.com/golang-jwt/jwt/v4" + "github.com/go-playground/validator/v10" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jwk" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "gorm.io/gorm/clause" "github.com/gin-gonic/gin" @@ -23,6 +35,8 @@ import ( "github.com/fossology/LicenseDb/pkg/utils" ) +var Jwks *jwk.Cache + // CreateUser creates a new user // // @Summary Create new user @@ -31,14 +45,14 @@ import ( // @Tags Users // @Accept json // @Produce json -// @Param user body models.UserInput true "User to create" +// @Param user body models.UserCreate true "User to create" // @Success 201 {object} models.UserResponse // @Failure 400 {object} models.LicenseError "Invalid json body" // @Failure 409 {object} models.LicenseError "User already exists" // @Security ApiKeyAuth // @Router /users [post] func CreateUser(c *gin.Context) { - var input models.UserInput + var input models.UserCreate if err := c.ShouldBindJSON(&input); err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, @@ -51,12 +65,22 @@ func CreateUser(c *gin.Context) { return } - user := models.User{ - Username: input.Username, - Userlevel: input.Userlevel, - Userpassword: input.Userpassword, + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&input); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "can not create user with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return } + user := models.User(input) + *user.Username = html.EscapeString(strings.TrimSpace(*user.Username)) + *user.DisplayName = html.EscapeString(strings.TrimSpace(*user.DisplayName)) err := utils.HashPassword(&user) if err != nil { er := models.LicenseError{ @@ -72,20 +96,246 @@ func CreateUser(c *gin.Context) { result := db.DB.Where(models.User{Username: user.Username}).FirstOrCreate(&user) if result.Error != nil { + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + er := models.LicenseError{ + Status: http.StatusConflict, + Message: "Failed to create the new user", + Error: "User with this email id already exists", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusConflict, er) + } else { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to create the new user", + Error: result.Error.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + } + return + } else if result.RowsAffected == 0 { + errMessage := fmt.Sprintf("Error: User with username '%s' already exists", *user.Username) + if !*user.Active { + errMessage = fmt.Sprintf("Error: User with username '%s' already exists, but is deactivated", *user.Username) + } + er := models.LicenseError{ + Status: http.StatusConflict, + Message: "can not create user", + Error: errMessage, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusConflict, er) + return + } + + res := models.UserResponse{ + Data: []models.User{user}, + Status: http.StatusCreated, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, + } + + c.JSON(http.StatusCreated, res) +} + +// CreateOidcUser creates a new user via oidc id token +// +// @Summary Create new user via oidc id token +// @Description Create a new service user via oidc id token +// @Id CreateOidcUser +// @Tags Users +// @Accept json +// @Produce json +// @Param user body models.OidcUserCreate true "User to create" +// @Success 201 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Failure 409 {object} models.LicenseError "User already exists" +// @Router /users/oidc [post] +func CreateOidcUser(c *gin.Context) { + if os.Getenv("OIDC_ISSUER") == "" || os.Getenv("OIDC_EMAIL_KEY") == "" || os.Getenv("OIDC_DISPLAYNAME_KEY") == "" || + os.Getenv("OIDC_USERNAME_KEY") == "" || Jwks == nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong, try again", + Error: "internal server error", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + log.Print("\033[31mError: OIDC environment variables not configured properly\033[0m") + return + } + + var tokenBody models.OidcUserCreate + if err := c.ShouldBindJSON(&tokenBody); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + keyset, err := Jwks.Lookup(context.Background(), os.Getenv("JWKS_URI")) + if err != nil { + log.Print("\033[31mError: Failed jwk.Cache lookup from the oidc provider's URL\033[0m") + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong", + Error: "internal server error", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + keyOptions := jws.WithKeySet(keyset) + keyError := true + if kid, err := utils.GetKid(tokenBody.Token); err == nil { + if key, ok := keyset.LookupKeyID(kid); ok { + if os.Getenv("OIDC_SIGNING_ALG") != "" { + if alg, ok := jwa.LookupSignatureAlgorithm(os.Getenv("OIDC_SIGNING_ALG")); ok { + if err = key.Set("alg", alg); err == nil { + keyError = false + } + } + } else if _, ok := key.Algorithm(); ok { + keyError = false + } + } + } + + if keyError { + log.Printf("\033[31mError: Token verification failed due to invalid alg header key field \033[0m") + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token verification failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + return + } + + if _, err = jws.Verify([]byte(tokenBody.Token), keyOptions); err != nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token verification failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: Token verification failed \033[0m") + return + } + + parsedToken, err := jwt.Parse([]byte(tokenBody.Token), jwt.WithValidate(true), jwt.WithVerify(false)) + if err != nil { + fmt.Println(err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token parsing failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + return + } + + iss, _ := parsedToken.Issuer() + if iss != os.Getenv("OIDC_ISSUER") { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "internal server error", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: Issuer '%s' not supported\033[0m", iss) + return + } + + var email, username, displayname, errMessage string + if err = parsedToken.Get(os.Getenv("OIDC_EMAIL_KEY"), &email); err != nil { + errMessage = err.Error() + } + if err = parsedToken.Get(os.Getenv("OIDC_USERNAME_KEY"), &username); err != nil { + errMessage = err.Error() + } + if err = parsedToken.Get(os.Getenv("OIDC_DISPLAYNAME_KEY"), &displayname); err != nil { + errMessage = err.Error() + } + if errMessage != "" { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "incompatible token format", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: %s\033[0m", errMessage) + return + } + level := "USER" + + user := models.User{ + Username: &username, + UserEmail: &email, + Userlevel: &level, + DisplayName: &displayname, + } + + result := db.DB. + Where(&models.User{Username: user.Username}). + FirstOrCreate(&user) + if result.Error != nil { + errMessage := "Something went wrong. Try again." + if errors.Is(result.Error, gorm.ErrDuplicatedKey) { + errMessage = "User with same display name or email exists" + er := models.LicenseError{ + Status: http.StatusConflict, + Message: "Failed to create user", + Error: errMessage, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusConflict, er) + return + } er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to create the new user", - Error: result.Error.Error(), + Error: errMessage, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusInternalServerError, er) return - } else if result.RowsAffected == 0 { + } + + if result.RowsAffected == 0 { + errMessage := fmt.Sprintf("Error: User with username '%s' already exists", *user.Username) + if !*user.Active { + errMessage = fmt.Sprintf("Error: User with username '%s' already exists, but is deactivated", *user.Username) + } er := models.LicenseError{ Status: http.StatusConflict, - Message: "can not create user with same username", - Error: fmt.Sprintf("Error: User with username '%s' already exists", user.Username), + Message: "Failed to create user", + Error: errMessage, Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } @@ -104,6 +354,248 @@ func CreateUser(c *gin.Context) { c.JSON(http.StatusCreated, res) } +// UpdateUser updates a user, requires admin rights +// +// @Summary Update user, requires admin rights +// @Description Update a service user, requires admin rights +// @Id UpdateUser +// @Tags Users +// @Accept json +// @Produce json +// @Param username path string true "username of the user to be updated" +// @Param user body models.UserUpdate true "User to update" +// @Success 200 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Failure 403 {object} models.LicenseError "This resource requires elevated access rights" +// @Security ApiKeyAuth +// @Router /users/{username} [patch] +func UpdateUser(c *gin.Context) { + var user models.User + username := c.Param("username") + + if err := db.DB.Where(models.User{Username: &username}).First(&user).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + + var input models.UserUpdate + if err := c.ShouldBindJSON(&input); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&input); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "can not update user with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + updatedUser := models.User(input) + if updatedUser.Username != nil { + *updatedUser.Username = html.EscapeString(strings.TrimSpace(*updatedUser.Username)) + } + if updatedUser.DisplayName != nil { + *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) + } + if updatedUser.Userpassword != nil { + err := utils.HashPassword(&updatedUser) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "password hashing failed", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + } + + updatedUser.Id = user.Id + if err := db.DB.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + res := models.UserResponse{ + Data: []models.User{updatedUser}, + Status: http.StatusOK, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, + } + c.JSON(http.StatusOK, res) +} + +// UpdateProfile updates one's user profile +// +// @Summary Users can update their profile using this endpoint +// @Description Users can update their profile using this endpoint +// @Id UpdateProfile +// @Tags Users +// @Accept json +// @Produce json +// @Param user body models.ProfileUpdate true "Profile fields to update" +// @Success 200 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid json body" +// @Security ApiKeyAuth +// @Router /users [patch] +func UpdateProfile(c *gin.Context) { + var user models.User + username := c.GetString("username") + + if err := db.DB.Where(models.User{Username: &username}).First(&user).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + + var input models.ProfileUpdate + if err := c.ShouldBindJSON(&input); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + if err := validate.Struct(&input); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "can not update profile with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + updatedUser := models.User(input) + if updatedUser.DisplayName != nil { + *updatedUser.DisplayName = html.EscapeString(strings.TrimSpace(*updatedUser.DisplayName)) + } + if updatedUser.Userpassword != nil { + err := utils.HashPassword(&updatedUser) + if err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "password hashing failed", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + } + + updatedUser.Id = user.Id + if err := db.DB.Clauses(clause.Returning{}).Updates(&updatedUser).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + return + } + + res := models.UserResponse{ + Data: []models.User{updatedUser}, + Status: http.StatusOK, + Meta: &models.PaginationMeta{ + ResourceCount: 1, + }, + } + c.JSON(http.StatusOK, res) +} + +// DeleteUser marks an existing user record as inactive +// +// @Summary Deactivate user +// @Description Deactivate an user +// @Id DeleteUser +// @Tags Users +// @Accept json +// @Produce json +// @Param username path string true "Username of the user to be marked as inactive" +// @Success 204 +// @Failure 404 {object} models.LicenseError "No user with given username found" +// @Security ApiKeyAuth +// @Router /users/{username} [delete] +func DeleteUser(c *gin.Context) { + var user models.User + username := c.Param("username") + active := true + if err := db.DB.Where(models.User{Username: &username, Active: &active}).First(&user).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "no user with such username exists", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + *user.Active = false + if err := db.DB.Updates(&user).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "failed to delete user", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return + } + c.Status(http.StatusNoContent) +} + // GetAllUser retrieves a list of all users from the database. // // @Summary Get users @@ -112,19 +604,23 @@ func CreateUser(c *gin.Context) { // @Tags Users // @Accept json // @Produce json -// @Param page query int false "Page number" -// @Param limit query int false "Number of records per page" +// @Param active query bool false "Active user only" +// @Param page query int false "Page number" +// @Param limit query int false "Number of records per page" // @Success 200 {object} models.UserResponse // @Failure 404 {object} models.LicenseError "Users not found" // @Security ApiKeyAuth // @Router /users [get] func GetAllUser(c *gin.Context) { - var users []models.User + active, err := strconv.ParseBool(c.Query("active")) + if err != nil { + active = false + } + var users []models.User query := db.DB.Model(&models.User{}) _ = utils.PreparePaginateResponse(c, query, &models.UserResponse{}) - - if err := query.Find(&users).Error; err != nil { + if err := query.Where(&models.User{Active: &active}).Find(&users).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: "Users not found", @@ -135,9 +631,7 @@ func GetAllUser(c *gin.Context) { c.JSON(http.StatusNotFound, er) return } - for i := 0; i < len(users); i++ { - users[i].Userpassword = nil - } + res := models.UserResponse{ Data: users, Status: http.StatusOK, @@ -152,29 +646,26 @@ func GetAllUser(c *gin.Context) { // GetUser retrieves a user by their user ID from the database. // // @Summary Get a user -// @Description Get a single user by ID +// @Description Get a single user by username // @Id GetUser // @Tags Users // @Accept json // @Produce json -// @Param id path int true "User ID" -// @Success 200 {object} models.UserResponse -// @Failure 400 {object} models.LicenseError "Invalid user id" -// @Failure 404 {object} models.LicenseError "User not found" +// @Param username path string true "Username" +// @Success 200 {object} models.UserResponse +// @Failure 400 {object} models.LicenseError "Invalid user id" +// @Failure 404 {object} models.LicenseError "User not found" // @Security ApiKeyAuth -// @Router /users/{id} [get] +// @Router /users/{username} [get] func GetUser(c *gin.Context) { var user models.User - id := c.Param("id") - parsedId, err := utils.ParseIdToInt(c, id, "user") - if err != nil { - return - } + username := c.Param("username") - if err := db.DB.Where(models.User{Id: parsedId}).First(&user).Error; err != nil { + active := true + if err := db.DB.Where(models.User{Username: &username, Active: &active}).First(&user).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, - Message: "no user with such user id exists", + Message: "no user with such username exists", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), @@ -182,7 +673,7 @@ func GetUser(c *gin.Context) { c.JSON(http.StatusNotFound, er) return } - user.Userpassword = nil + res := models.UserResponse{ Data: []models.User{user}, Status: http.StatusOK, @@ -204,6 +695,7 @@ func GetUser(c *gin.Context) { // @Produce json // @Param user body models.UserLogin true "Login credentials" // @Success 200 {object} object{token=string} "JWT token" +// @Failure 401 {object} models.LicenseError "Incorrect username or password" // @Router /login [post] func Login(c *gin.Context) { var input models.UserLogin @@ -221,20 +713,32 @@ func Login(c *gin.Context) { username := input.Username password := input.Userpassword - + active := true var user models.User - result := db.DB.Where(models.User{Username: username}).First(&user) + result := db.DB.Where(models.User{Username: &username, Active: &active}).First(&user) if result.Error != nil { er := models.LicenseError{ Status: http.StatusUnauthorized, - Message: "User name not found", - Error: "", + Message: "Incorrect username or password", + Error: "Incorrect username or password", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + + c.JSON(http.StatusUnauthorized, er) + return + } + + if user.Userpassword == nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Incorrect username or password", + Error: "Incorrect username or password", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusUnauthorized, er) - c.Abort() return } @@ -249,7 +753,6 @@ func Login(c *gin.Context) { } c.JSON(http.StatusInternalServerError, er) - c.Abort() return } @@ -258,14 +761,13 @@ func Login(c *gin.Context) { if err != nil { er := models.LicenseError{ Status: http.StatusUnauthorized, - Message: "Incorrect password", - Error: err.Error(), + Message: "Incorrect username or password", + Error: "Incorrect username or password", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusUnauthorized, er) - c.Abort() return } @@ -311,11 +813,23 @@ func generateToken(user models.User) (string, error) { return "", err } - claims := jwt.MapClaims{} - claims["user"] = user - claims["nbf"] = time.Now().Unix() - claims["exp"] = time.Now().Add(time.Hour * time.Duration(tokenLifespan)).Unix() - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tok, err := jwt.NewBuilder(). + Issuer(os.Getenv("DEFAULT_ISSUER")). + IssuedAt(time.Now()). + NotBefore(time.Now()). + Expiration(time.Now().Add(time.Hour*time.Duration(tokenLifespan))). + Claim("user", user). + Build() + if err != nil { + fmt.Printf("failed to build token: %s\n", err) + return "", err + } + + signed, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256(), []byte(os.Getenv("API_SECRET")))) + if err != nil { + fmt.Printf("failed to sign token: %s\n", err) + return "", err + } - return token.SignedString([]byte(os.Getenv("API_SECRET"))) + return string(signed), nil } diff --git a/pkg/db/db.go b/pkg/db/db.go index 892ac69..d2ac959 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -18,7 +18,7 @@ var DB *gorm.DB func Connect(dbhost, port, user, dbname, password *string) { dburi := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s", *dbhost, *port, *user, *dbname, *password) - gormConfig := &gorm.Config{} + gormConfig := &gorm.Config{TranslateError: true} database, err := gorm.Open(postgres.Open(dburi), gormConfig) if err != nil { log.Fatalf("Failed to connect to database: %v", err) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 5943d58..22cac2a 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -8,6 +8,7 @@ package middleware import ( "bytes" + "context" "encoding/json" "fmt" "log" @@ -17,11 +18,14 @@ import ( "strconv" "time" + "github.com/fossology/LicenseDb/pkg/auth" "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v4" + "github.com/lestrrat-go/jwx/v3/jwa" + "github.com/lestrrat-go/jwx/v3/jws" + "github.com/lestrrat-go/jwx/v3/jwt" ) // AuthenticationMiddleware is a middleware function for user authentication. @@ -37,66 +41,251 @@ func AuthenticationMiddleware() gin.HandlerFunc { Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusUnauthorized, er) c.Abort() return } - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(os.Getenv("API_SECRET")), nil - }) - + unverfiedParsedToken, err := jwt.Parse([]byte(tokenString), jwt.WithVerify(false), jwt.WithValidate(true)) if err != nil { er := models.LicenseError{ Status: http.StatusUnauthorized, Message: "Please check your credentials and try again", - Error: err.Error(), + Error: "token parsing failed", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusUnauthorized, er) c.Abort() return } - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { + iss, _ := unverfiedParsedToken.Issuer() + if iss == os.Getenv("DEFAULT_ISSUER") { + _, err := jws.Verify([]byte(tokenString), jws.WithKey(jwa.HS256(), []byte(os.Getenv("API_SECRET")))) + if err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token verification failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + var userData map[string]interface{} + if err = unverfiedParsedToken.Get("user", &userData); err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "incompatible token format", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + userDataBytes, err := json.Marshal(userData) + if err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "failed to marshal user data", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + // Unmarshal the JSON bytes into the models.User struct + var user models.User + err = json.Unmarshal(userDataBytes, &user) + if err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "incompatible token format", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + if err := db.DB.Where(models.User{Id: user.Id}).First(&user).Error; err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "User not found. Please check your credentials.", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + c.Set("username", *user.Username) + c.Set("role", *user.Userlevel) + } else if iss == os.Getenv("OIDC_ISSUER") { + if auth.Jwks == nil || os.Getenv("OIDC_USERNAME_KEY") == "" { + log.Print("\033[31mError: OIDC environment variables not configured properly\033[0m") + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong", + Error: "internal server error", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + c.Abort() + return + } + + keyset, err := auth.Jwks.Lookup(context.Background(), os.Getenv("JWKS_URI")) + if err != nil { + log.Print("\033[31mError: Failed jwk.Cache lookup from the oidc provider's URL\033[0m") + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong", + Error: "internal server error", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + c.Abort() + return + } + + keyOptions := jws.WithKeySet(keyset) + keyError := true + if kid, err := utils.GetKid(tokenString); err == nil { + if key, ok := keyset.LookupKeyID(kid); ok { + if os.Getenv("OIDC_SIGNING_ALG") != "" { + if alg, ok := jwa.LookupSignatureAlgorithm(os.Getenv("OIDC_SIGNING_ALG")); ok { + if err = key.Set("alg", alg); err == nil { + keyError = false + } + } + } else if _, ok := key.Algorithm(); ok { + keyError = false + } + } + } + + if keyError { + log.Printf("\033[31mError: Token verification failed due to invalid alg header key field \033[0m") + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token verification failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + if _, err = jws.Verify([]byte(tokenString), keyOptions); err != nil { + log.Printf("\033[31mError: Token verification failed \033[0m") + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "token verification failed", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + var username string + if err = unverfiedParsedToken.Get(os.Getenv("OIDC_USERNAME_KEY"), &username); err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "incompatible token format", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + var user models.User + if err := db.DB.Where(models.User{Username: &username}).First(&user).Error; err != nil { + log.Printf("\033[31mError: %s\033[0m", err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "User not found", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + c.Set("username", *user.Username) + c.Set("role", *user.Userlevel) + } else { + log.Printf("\033[31mError: Issuer '%s' not supported or not configured in .env\033[0m", iss) er := models.LicenseError{ Status: http.StatusUnauthorized, - Message: "Invalid token", - Error: "Invalid token", + Message: "Please check your credentials and try again", + Error: "internal server error", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusUnauthorized, er) c.Abort() return } + c.Next() + } +} - userId := int64(claims["user"].(map[string]interface{})["id"].(float64)) - - var user models.User - if err := db.DB.Where(models.User{Id: userId}).First(&user).Error; err != nil { +// RoleBasedAccessMiddleware is a middleware function for giving role based access to apis. +func RoleBasedAccessMiddleware(roles []string) gin.HandlerFunc { + return func(c *gin.Context) { + role := c.GetString("role") + found := false + for _, r := range roles { + if role == r { + found = true + break + } + } + if !found { + log.Print("\033[31mError: access denied due to insufficient role permissions\033[0m") er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "User not found", - Error: err.Error(), + Status: http.StatusForbidden, + Message: "You do not have the necessary permissions to access this resource", + Error: "access denied due to insufficient role permissions", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - - c.JSON(http.StatusUnauthorized, er) + c.JSON(http.StatusForbidden, er) c.Abort() return } - - c.Set("username", user.Username) c.Next() } } diff --git a/pkg/models/types.go b/pkg/models/types.go index a15a3b8..8c3a777 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -212,15 +212,81 @@ type LicenseError struct { // User struct is representation of user information. type User struct { Id int64 `json:"id" gorm:"primary_key" example:"123"` - Username string `json:"username" gorm:"unique;not null" binding:"required" example:"fossy"` - Userlevel string `json:"userlevel" binding:"required" example:"admin"` + Username *string `json:"username" gorm:"unique;not null" example:"fossy"` + DisplayName *string `json:"display_name" gorm:"unique;not null" example:"fossy"` + UserEmail *string `json:"user_email" gorm:"unique;not null" example:"fossy@org.com"` + Userlevel *string `json:"user_level" gorm:"not null" example:"USER"` Userpassword *string `json:"-"` + Active *bool `json:"-" gorm:"not null;default:true"` } -type UserInput struct { - Username string `json:"username" gorm:"unique;not null" binding:"required" example:"fossy"` - Userlevel string `json:"userlevel" binding:"required" example:"admin"` - Userpassword *string `json:"password,omitempty" binding:"required" example:"fossy"` +func (u *User) BeforeCreate(tx *gorm.DB) (err error) { + if u.Username != nil && *u.Username == "" { + return errors.New("username cannot be an empty string") + } + if u.DisplayName != nil && *u.DisplayName == "" { + return errors.New("display_name cannot be an empty string") + } + if u.UserEmail != nil && *u.UserEmail == "" { + return errors.New("user email cannot be an empty string") + } + if u.Userlevel != nil && *u.Userlevel == "" { + return errors.New("user level cannot be an empty string") + } + return +} + +func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { + if u.Username != nil && *u.Username == "" { + return errors.New("username cannot be an empty string") + } + if u.DisplayName != nil && *u.DisplayName == "" { + return errors.New("display_name cannot be an empty string") + } + if u.UserEmail != nil && *u.UserEmail == "" { + return errors.New("user email cannot be an empty string") + } + if u.Userlevel != nil && *u.Userlevel == "" { + return errors.New("user level cannot be an empty string") + } + if u.Userpassword != nil && *u.Userpassword == "" { + return errors.New("user password cannot be an empty string") + } + return +} + +type UserCreate struct { + Id int64 `json:"-"` + Username *string `json:"username" validate:"required" example:"fossy"` + DisplayName *string `json:"display_name" validate:"required" example:"fossy"` + UserEmail *string `json:"user_email" validate:"required,email" example:"fossy@org.com"` + Userlevel *string `json:"user_level" validate:"required,oneof=USER ADMIN" example:"ADMIN"` + Userpassword *string `json:"user_password" example:"fossy"` + Active *bool `json:"-"` +} + +type UserUpdate struct { + Id int64 `json:"-"` + Username *string `json:"username" example:"fossy"` + DisplayName *string `json:"display_name" example:"fossy"` + UserEmail *string `json:"user_email" validate:"omitempty,email"` + Userlevel *string `json:"user_level" validate:"omitempty,oneof=USER ADMIN" example:"ADMIN"` + Userpassword *string `json:"user_password"` + Active *bool `json:"active"` +} + +type ProfileUpdate struct { + Id int64 `json:"-"` + Username *string `json:"-"` + DisplayName *string `json:"display_name" example:"fossy"` + UserEmail *string `json:"user_email" validate:"omitempty,email"` + Userlevel *string `json:"-"` + Userpassword *string `json:"user_password"` + Active *bool `json:"-"` +} + +type OidcUserCreate struct { + Token string `json:"token"` } type UserLogin struct { diff --git a/pkg/utils/util.go b/pkg/utils/util.go index 737b776..7cbf2b3 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -8,10 +8,10 @@ package utils import ( + "encoding/base64" "encoding/json" "errors" "fmt" - "html" "log" "net/http" "os" @@ -153,8 +153,6 @@ func HashPassword(user *models.User) error { } *user.Userpassword = string(hashedPassword) - user.Username = html.EscapeString(strings.TrimSpace(user.Username)) - return nil } @@ -370,7 +368,7 @@ func createObligationMapChangelog(tx *gorm.DB, username string, } var user models.User - if err := tx.Where(models.User{Username: username}).First(&user).Error; err != nil { + if err := tx.Where(models.User{Username: &username}).First(&user).Error; err != nil { return err } @@ -475,3 +473,22 @@ func GetAuditEntity(c *gin.Context, audit *models.Audit) error { } return nil } + +// https://github.com/lestrrat-go/jwx/discussions/547 +// Get kid field value from JWS Header +func GetKid(token string) (string, error) { + type JWSHeader struct { + KeyID string `json:"kid"` + } + + parts := strings.Split(token, ".") + + decodedBytes, err := base64.StdEncoding.DecodeString(parts[0]) + if err != nil { + return "", err + } + + var header JWSHeader + err = json.Unmarshal(decodedBytes, &header) + return header.KeyID, err +}