From 4f620b400e26cf42f7894d50b6bb0fc791c9e449 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 | 29 +- .github/workflows/api-swagger.yml | 4 +- .github/workflows/go.yml | 2 +- .github/workflows/golangci.yml | 2 +- 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 | 26 +- go.sum | 60 ++- 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 | 596 ++++++++++++++++++++++++--- pkg/db/db.go | 2 +- pkg/middleware/middleware.go | 194 +++++++-- pkg/models/types.go | 78 +++- pkg/utils/util.go | 5 +- 22 files changed, 1722 insertions(+), 200 deletions(-) diff --git a/.env.example b/.env.example index 07dd44b..b8ae9d9 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,31 @@ 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) +# If not set, there will be multiple verify attempts done with all the algorithms in the +# family of algorithms mentioned in the "kty" field till a match is found +# +# 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..3457a57 100644 --- a/.github/workflows/golangci.yml +++ b/.github/workflows/golangci.yml @@ -23,7 +23,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/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 1f188c6..e012f05 100644 --- a/go.mod +++ b/go.mod @@ -1,25 +1,32 @@ 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/joho/godotenv v1.5.1 + github.com/lestrrat-go/httprc/v3 v3.0.0-beta1 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 - golang.org/x/crypto v0.21.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.1 ) require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect - golang.org/x/sync v0.5.0 // 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.8.0 // indirect gorm.io/driver/mysql v1.4.7 // indirect ) @@ -40,7 +47,7 @@ require ( 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/goccy/go-json v0.10.3 // 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 @@ -50,6 +57,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.0-alpha1 github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -60,10 +68,10 @@ require ( 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 - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.16.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.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 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 741b9f1..cf840bd 100644 --- a/go.sum +++ b/go.sum @@ -16,9 +16,12 @@ github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3 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/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.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 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= @@ -34,6 +37,7 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh 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/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= @@ -42,13 +46,14 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4 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-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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/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/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -74,12 +79,23 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX 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/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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/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/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.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= @@ -87,7 +103,9 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ 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/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= 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= @@ -100,6 +118,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb 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= @@ -128,23 +148,24 @@ 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/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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.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= @@ -154,8 +175,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc 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= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -164,13 +185,13 @@ 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +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/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= @@ -178,6 +199,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= @@ -192,7 +214,9 @@ gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o 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/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= 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= 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..6a1f9a2 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -7,14 +7,25 @@ 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 +34,8 @@ import ( "github.com/fossology/LicenseDb/pkg/utils" ) +var Jwks *jwk.Cache + // CreateUser creates a new user // // @Summary Create new user @@ -31,14 +44,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 +64,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 +95,233 @@ 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: "Something went wrong, try again", + 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 to create a jwk.Cache from the oidc provider's URL\033[0m") + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong", + Error: "Something went wrong", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + 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: "Please check your credentials and try again", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + keyOptions := jws.WithKeySet(keyset, jws.WithInferAlgorithmFromKey(true)) + kid := "" + if err := parsedToken.Get("kid", &kid); 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 { + key.Set("alg", alg) + keyOptions = jws.WithKeySet(keyset) + } + } else if _, ok := key.Algorithm(); ok { + keyOptions = jws.WithKeySet(keyset) + } + } + } + + 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: "Please check your credentials and try again", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: Token verification \033[0m") + 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: "Please check your credentials and try again", + 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: "Please check your credentials and try again", + 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 +340,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 +590,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 +617,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 +632,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 +659,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 +681,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 +699,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 +739,6 @@ func Login(c *gin.Context) { } c.JSON(http.StatusInternalServerError, er) - c.Abort() return } @@ -258,14 +747,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 +799,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..d3d7ac0 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. @@ -43,60 +47,200 @@ func AuthenticationMiddleware() gin.HandlerFunc { 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)) if err != nil { er := models.LicenseError{ Status: http.StatusUnauthorized, Message: "Please check your credentials and try again", - Error: err.Error(), + Error: "wrong credentials were passed", 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") { + parsedToken, err := jwt.Parse([]byte(tokenString), jwt.WithKey(jwa.HS256(), []byte(os.Getenv("API_SECRET"))), jwt.WithValidate(true)) + if err != nil { + fmt.Print(err.Error()) + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + var user models.User + parsedToken.Get("user", &user) + + if err := db.DB.Where(models.User{Id: user.Id}).First(&user).Error; err != nil { + 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 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: "Something went wrong", + 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 to create a jwk.Cache from the oidc provider's URL\033[0m") + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Something went wrong", + Error: "Something went wrong", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusInternalServerError, er) + c.Abort() + return + } + + parsedToken, err := jwt.Parse([]byte(tokenString), 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: "Please check your credentials and try again", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + c.Abort() + return + } + + keyOptions := jws.WithKeySet(keyset, jws.WithInferAlgorithmFromKey(true)) + kid := "" + if err := parsedToken.Get("kid", &kid); 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 { + key.Set("alg", alg) + keyOptions = jws.WithKeySet(keyset) + } + } else if _, ok := key.Algorithm(); ok { + keyOptions = jws.WithKeySet(keyset) + } + } + } + + if _, err = jws.Verify([]byte(tokenString), keyOptions); err != nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "Please check your credentials and try again", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: Token verification \033[0m") + return + } + + var username string + if err = parsedToken.Get(os.Getenv("OIDC_USERNAME_KEY"), &username); err != nil { + er := models.LicenseError{ + Status: http.StatusUnauthorized, + Message: "Please check your credentials and try again", + Error: "Please check your credentials and try again", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: %s\033[0m", err.Error()) + return + } + + var user models.User + if err := db.DB.Where(models.User{Username: &username}).First(&user).Error; err != nil { + 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 { er := models.LicenseError{ Status: http.StatusUnauthorized, - Message: "Invalid token", - Error: "Invalid token", + Message: "Please check your credentials and try again", + Error: "Please check your credentials and try again", Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusUnauthorized, er) + log.Printf("\033[31mError: Issuer '%s' not supported or not configured in .env\033[0m", iss) 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 { er := models.LicenseError{ - Status: http.StatusUnauthorized, - Message: "User not found", - Error: err.Error(), + Status: http.StatusForbidden, + Message: "this resource requires elevated access rights", + Error: "this resource requires elevated access rights", 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..0dd8850 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -11,7 +11,6 @@ import ( "encoding/json" "errors" "fmt" - "html" "log" "net/http" "os" @@ -153,8 +152,6 @@ func HashPassword(user *models.User) error { } *user.Userpassword = string(hashedPassword) - user.Username = html.EscapeString(strings.TrimSpace(user.Username)) - return nil } @@ -370,7 +367,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 }