mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-04-29 02:22:28 +00:00
Adds LDAP auth support
This commit is contained in:
parent
8434a2d1fa
commit
a277a5d167
@ -10,6 +10,24 @@
|
|||||||
"$ref": "file://./paths/get.json"
|
"$ref": "file://./paths/get.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/auth": {
|
||||||
|
"get": {
|
||||||
|
"$ref": "file://./paths/auth/get.json"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/refresh": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/refresh/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/sse": {
|
||||||
|
"post": {
|
||||||
|
"$ref": "file://./paths/auth/sse/post.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/certificates": {
|
"/certificates": {
|
||||||
"get": {
|
"get": {
|
||||||
"$ref": "file://./paths/certificates/get.json"
|
"$ref": "file://./paths/certificates/get.json"
|
||||||
@ -155,19 +173,6 @@
|
|||||||
"$ref": "file://./paths/streams/streamID/delete.json"
|
"$ref": "file://./paths/streams/streamID/delete.json"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/tokens": {
|
|
||||||
"get": {
|
|
||||||
"$ref": "file://./paths/tokens/get.json"
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"$ref": "file://./paths/tokens/post.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/tokens/sse": {
|
|
||||||
"post": {
|
|
||||||
"$ref": "file://./paths/tokens/sse/post.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/upstreams": {
|
"/upstreams": {
|
||||||
"get": {
|
"get": {
|
||||||
"$ref": "file://./paths/upstreams/get.json"
|
"$ref": "file://./paths/upstreams/get.json"
|
||||||
@ -219,6 +224,9 @@
|
|||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"AuthConfigObject": {
|
||||||
|
"$ref": "file://./components/AuthConfigObject.json"
|
||||||
|
},
|
||||||
"CertificateAuthorityList": {
|
"CertificateAuthorityList": {
|
||||||
"$ref": "file://./components/CertificateAuthorityList.json"
|
"$ref": "file://./components/CertificateAuthorityList.json"
|
||||||
},
|
},
|
||||||
|
13
backend/embed/api_docs/components/AuthConfigObject.json
Normal file
13
backend/embed/api_docs/components/AuthConfigObject.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"description": "AuthConfigObject",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"local",
|
||||||
|
"ldap",
|
||||||
|
"oidc"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,6 @@
|
|||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"name",
|
"name",
|
||||||
"nickname",
|
|
||||||
"email",
|
"email",
|
||||||
"is_disabled"
|
"is_disabled"
|
||||||
],
|
],
|
||||||
@ -29,12 +28,7 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 2,
|
"minLength": 2,
|
||||||
"maxLength": 100
|
"maxLength": 50
|
||||||
},
|
|
||||||
"nickname": {
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 2,
|
|
||||||
"maxLength": 100
|
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
28
backend/embed/api_docs/paths/auth/get.json
Normal file
28
backend/embed/api_docs/paths/auth/get.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"operationId": "getAuthConfig",
|
||||||
|
"summary": "Returns auth configuration",
|
||||||
|
"tags": ["Auth"],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "200 response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["result"],
|
||||||
|
"properties": {
|
||||||
|
"result": {
|
||||||
|
"$ref": "#/components/schemas/AuthConfigObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"default": {
|
||||||
|
"value": "todo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "requestToken",
|
"operationId": "requestToken",
|
||||||
"summary": "Request a new access token from credentials",
|
"summary": "Request a new access token from credentials",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"description": "Credentials Payload",
|
"description": "Credentials Payload",
|
||||||
"required": true,
|
"required": true,
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "refreshToken",
|
"operationId": "refreshToken",
|
||||||
"summary": "Refresh your access token",
|
"summary": "Refresh your access token",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "200 response",
|
"description": "200 response",
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"operationId": "requestSSEToken",
|
"operationId": "requestSSEToken",
|
||||||
"summary": "Request a new SSE token",
|
"summary": "Request a new SSE token",
|
||||||
"tags": ["Tokens"],
|
"tags": ["Auth"],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "200 response",
|
"description": "200 response",
|
@ -28,7 +28,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"description": "The sorting of the list",
|
"description": "The sorting of the list",
|
||||||
"example": "name,nickname.desc,email.asc"
|
"example": "name,email.asc"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@ -57,10 +57,6 @@
|
|||||||
"field": "name",
|
"field": "name",
|
||||||
"direction": "ASC"
|
"direction": "ASC"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"field": "nickname",
|
|
||||||
"direction": "DESC"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"field": "email",
|
"field": "email",
|
||||||
"direction": "ASC"
|
"direction": "ASC"
|
||||||
@ -70,7 +66,6 @@
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010090000,
|
"created_at": 1578010090000,
|
||||||
"updated_at": 1578010095000,
|
"updated_at": 1578010095000,
|
||||||
@ -81,7 +76,6 @@
|
|||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"nickname": "John",
|
|
||||||
"email": "johdoe@example.com",
|
"email": "johdoe@example.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010105000,
|
"updated_at": 1578010105000,
|
||||||
@ -95,7 +89,6 @@
|
|||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"name": "Jane Doe",
|
"name": "Jane Doe",
|
||||||
"nickname": "Jane",
|
|
||||||
"email": "janedoe@example.com",
|
"email": "janedoe@example.com",
|
||||||
"created_at": 1578010110000,
|
"created_at": 1578010110000,
|
||||||
"updated_at": 1578010115000,
|
"updated_at": 1578010115000,
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010100000,
|
"updated_at": 1578010100000,
|
||||||
|
@ -43,7 +43,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010105000,
|
"updated_at": 1578010105000,
|
||||||
|
@ -52,7 +52,6 @@
|
|||||||
"result": {
|
"result": {
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Jamie Curnow",
|
"name": "Jamie Curnow",
|
||||||
"nickname": "James",
|
|
||||||
"email": "jc@jc21.com",
|
"email": "jc@jc21.com",
|
||||||
"created_at": 1578010100000,
|
"created_at": 1578010100000,
|
||||||
"updated_at": 1578010110000,
|
"updated_at": 1578010110000,
|
||||||
|
@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user`
|
|||||||
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
`updated_at` BIGINT NOT NULL DEFAULT 0,
|
||||||
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
`name` VARCHAR(50) NOT NULL,
|
`name` VARCHAR(50) NOT NULL,
|
||||||
`nickname` VARCHAR(50) NOT NULL,
|
|
||||||
`email` VARCHAR(255) NOT NULL,
|
`email` VARCHAR(255) NOT NULL,
|
||||||
`is_system` BOOLEAN NOT NULL DEFAULT FALSE,
|
`is_system` BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE
|
`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth`
|
|||||||
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
`user_id` INT NOT NULL,
|
`user_id` INT NOT NULL,
|
||||||
`type` VARCHAR(50) NOT NULL,
|
`type` VARCHAR(50) NOT NULL,
|
||||||
|
`identity` VARCHAR(255) NOT NULL,
|
||||||
`secret` VARCHAR(255) NOT NULL,
|
`secret` VARCHAR(255) NOT NULL,
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE,
|
||||||
UNIQUE (`user_id`, `type`)
|
UNIQUE (`user_id`, `type`)
|
||||||
|
@ -37,6 +37,27 @@ INSERT INTO `setting` (
|
|||||||
"default-site",
|
"default-site",
|
||||||
"What to show users who hit your Nginx server by default",
|
"What to show users who hit your Nginx server by default",
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"auth-methods",
|
||||||
|
"Which methods are enabled for authentication",
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"oidc-auth",
|
||||||
|
"Configuration for OIDC authentication",
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000),
|
||||||
|
"ldap-auth",
|
||||||
|
"Configuration for LDAP authentication",
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
|
@ -15,7 +15,6 @@ CREATE TABLE "user" (
|
|||||||
"updated_at" BIGINT NOT NULL DEFAULT 0,
|
"updated_at" BIGINT NOT NULL DEFAULT 0,
|
||||||
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
"name" VARCHAR(50) NOT NULL,
|
"name" VARCHAR(50) NOT NULL,
|
||||||
"nickname" VARCHAR(50) NOT NULL,
|
|
||||||
"email" VARCHAR(255) NOT NULL,
|
"email" VARCHAR(255) NOT NULL,
|
||||||
"is_system" BOOLEAN NOT NULL DEFAULT FALSE,
|
"is_system" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE
|
"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
@ -39,6 +38,7 @@ CREATE TABLE "auth" (
|
|||||||
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism
|
||||||
"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
|
"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
|
||||||
"type" VARCHAR(50) NOT NULL,
|
"type" VARCHAR(50) NOT NULL,
|
||||||
|
"identity" VARCHAR(255) NOT NULL,
|
||||||
"secret" VARCHAR(255) NOT NULL,
|
"secret" VARCHAR(255) NOT NULL,
|
||||||
UNIQUE ("user_id", "type")
|
UNIQUE ("user_id", "type")
|
||||||
);
|
);
|
||||||
|
@ -37,6 +37,27 @@ INSERT INTO "setting" (
|
|||||||
'default-site',
|
'default-site',
|
||||||
'What to show users who hit your Nginx server by default',
|
'What to show users who hit your Nginx server by default',
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'auth-methods',
|
||||||
|
'Which methods are enabled for authentication',
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'oidc-auth',
|
||||||
|
'Configuration for OIDC authentication',
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000,
|
||||||
|
'ldap-auth',
|
||||||
|
'Configuration for LDAP authentication',
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
|
@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user`
|
|||||||
`updated_at` INTEGER NOT NULL DEFAULT 0,
|
`updated_at` INTEGER NOT NULL DEFAULT 0,
|
||||||
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
||||||
`name` TEXT NOT NULL,
|
`name` TEXT NOT NULL,
|
||||||
`nickname` TEXT NOT NULL,
|
|
||||||
`email` TEXT NOT NULL,
|
`email` TEXT NOT NULL,
|
||||||
`is_system` INTEGER NOT NULL DEFAULT 0,
|
`is_system` INTEGER NOT NULL DEFAULT 0,
|
||||||
`is_disabled` INTEGER NOT NULL DEFAULT 0
|
`is_disabled` INTEGER NOT NULL DEFAULT 0
|
||||||
@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth`
|
|||||||
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
`is_deleted` INTEGER NOT NULL DEFAULT 0,
|
||||||
`user_id` INTEGER NOT NULL,
|
`user_id` INTEGER NOT NULL,
|
||||||
`type` TEXT NOT NULL,
|
`type` TEXT NOT NULL,
|
||||||
|
`identity` TEXT NOT NULL,
|
||||||
`secret` TEXT NOT NULL,
|
`secret` TEXT NOT NULL,
|
||||||
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE,
|
||||||
UNIQUE (`user_id`, `type`)
|
UNIQUE (`user_id`, `type`)
|
||||||
|
@ -36,6 +36,27 @@ INSERT INTO `setting` (
|
|||||||
"default-site",
|
"default-site",
|
||||||
"What to show users who hit your Nginx server by default",
|
"What to show users who hit your Nginx server by default",
|
||||||
'"welcome"' -- remember this is json
|
'"welcome"' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"auth-methods",
|
||||||
|
"Which methods are enabled for authentication",
|
||||||
|
'["local"]' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"oidc-auth",
|
||||||
|
"Configuration for OIDC authentication",
|
||||||
|
'{}' -- remember this is json
|
||||||
|
),
|
||||||
|
(
|
||||||
|
unixepoch() * 1000,
|
||||||
|
unixepoch() * 1000,
|
||||||
|
"ldap-auth",
|
||||||
|
"Configuration for LDAP authentication",
|
||||||
|
'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Default Certificate Authorities
|
-- Default Certificate Authorities
|
||||||
|
@ -32,14 +32,17 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.8 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
||||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
@ -26,12 +29,16 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
|||||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
|
||||||
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
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/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
@ -47,6 +54,12 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
|
|||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||||
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
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/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||||
@ -61,6 +74,12 @@ github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec h1:KKntwkZlM2w/88QiDyA
|
|||||||
github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
|
github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s=
|
||||||
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
|
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k=
|
||||||
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
|
github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80=
|
||||||
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
|
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||||
|
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||||
|
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||||
|
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||||
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
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/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
@ -136,40 +155,90 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr
|
|||||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
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/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
|
github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk=
|
||||||
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
|
github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI=
|
||||||
github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc=
|
github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
|
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w=
|
||||||
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
|
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4=
|
||||||
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38=
|
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38=
|
||||||
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||||
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
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/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-20200114155413-6afb5195e5aa/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.25.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=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
|
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||||
|
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.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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.18.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
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 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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
@ -3,97 +3,244 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
h "npm/internal/api/http"
|
||||||
|
"npm/internal/errors"
|
||||||
|
"npm/internal/logger"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
c "npm/internal/api/context"
|
||||||
h "npm/internal/api/http"
|
|
||||||
"npm/internal/entity/auth"
|
"npm/internal/entity/auth"
|
||||||
|
"npm/internal/entity/setting"
|
||||||
"npm/internal/entity/user"
|
"npm/internal/entity/user"
|
||||||
"npm/internal/errors"
|
njwt "npm/internal/jwt"
|
||||||
"npm/internal/logger"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type setAuthModel struct {
|
// tokenPayload is the structure we expect from a incoming login request
|
||||||
// The json tags are required, as the change password form decodes into this object
|
type tokenPayload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
Identity string `json:"identity"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
CurrentSecret string `json:"current_secret"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAuth sets a auth method. This can be used for "me" and `2` for example
|
// GetAuthConfig is anonymous and returns the types of authentication
|
||||||
// Route: POST /users/:userID/auth
|
// enabled for this site
|
||||||
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
func GetAuthConfig() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
val, err := setting.GetAuthMethods()
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, nil)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewToken Also known as a Login, requesting a new token with credentials
|
||||||
|
// Route: POST /auth
|
||||||
|
func NewToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Read the bytes from the body
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
var newAuth setAuthModel
|
var payload tokenPayload
|
||||||
err := json.Unmarshal(bodyBytes, &newAuth)
|
err := json.Unmarshal(bodyBytes, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, isSelf, userIDErr := getUserIDFromRequest(r)
|
// Check that this auth type is enabled
|
||||||
if userIDErr != nil {
|
if authMethods, err := setting.GetAuthMethods(); err == gorm.ErrRecordNotFound {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
h.ResultResponseJSON(w, r, http.StatusOK, nil)
|
||||||
return
|
return
|
||||||
}
|
} else if err != nil {
|
||||||
|
|
||||||
// Load user
|
|
||||||
thisUser, thisUserErr := user.GetByID(userID)
|
|
||||||
if thisUserErr == gorm.ErrRecordNotFound {
|
|
||||||
h.NotFound(w, r)
|
|
||||||
return
|
|
||||||
} else if thisUserErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if thisUser.IsSystem {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing auth for user
|
|
||||||
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
|
|
||||||
if userAuthErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSelf {
|
|
||||||
// confirm that the current_secret given is valid for the one stored in the database
|
|
||||||
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
|
|
||||||
if validateErr != nil {
|
|
||||||
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
|
|
||||||
// Sleep for 1 second to prevent brute force password guessing
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newAuth.Type == auth.TypePassword {
|
|
||||||
err := userAuth.SetPassword(newAuth.Secret)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("SetPasswordError", err)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
}
|
return
|
||||||
}
|
} else if !slices.Contains(authMethods, payload.Type) {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidAuthType.Error(), nil)
|
||||||
if err = userAuth.Save(); err != nil {
|
|
||||||
logger.Error("AuthSaveError", err)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userAuth.Secret = ""
|
switch payload.Type {
|
||||||
|
case "ldap":
|
||||||
// todo: add to audit-log
|
newTokenLDAP(w, r, payload)
|
||||||
|
case "oidc":
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
|
newTokenOIDC(w, r, payload)
|
||||||
|
case "local":
|
||||||
|
newTokenLocal(w, r, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenLocal(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
|
||||||
|
// Find user by email
|
||||||
|
userObj, userErr := user.GetByEmail(payload.Identity)
|
||||||
|
if userErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), userErr.Error())
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Auth
|
||||||
|
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
||||||
|
if authErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Auth
|
||||||
|
validateErr := authObj.ValidateSecret(payload.Secret)
|
||||||
|
if validateErr != nil {
|
||||||
|
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
|
||||||
|
// Sleep for 1 second to prevent brute force password guessing
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenLDAP(w http.ResponseWriter, r *http.Request, payload tokenPayload) {
|
||||||
|
// Get LDAP settings
|
||||||
|
ldapSettings, err := setting.GetLDAPSettings()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LDAP settings not found", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets try to authenticate with LDAP
|
||||||
|
ldapUser, err := auth.LDAPAuthenticate(payload.Identity, payload.Secret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LDAP Auth Error", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Auth by identity
|
||||||
|
authObj, authErr := auth.GetByIdenityType(ldapUser.Username, payload.Type)
|
||||||
|
if authErr == gorm.ErrRecordNotFound {
|
||||||
|
// Auth is not found for this identity. We can create it
|
||||||
|
if !ldapSettings.AutoCreateUser {
|
||||||
|
// LDAP Login was successful, but user does not have an auth record
|
||||||
|
// and auto create is disabled. Showing account disabled error
|
||||||
|
// for the time being
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to find user by email
|
||||||
|
foundUser, err := user.GetByEmail(ldapUser.Email)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// User not found, create user
|
||||||
|
foundUser, err = user.CreateFromLDAPUser(ldapUser)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("user.CreateFromLDAPUser", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Created user from LDAP: %s, %s", ldapUser.Username, foundUser.Email)
|
||||||
|
} else if err != nil {
|
||||||
|
logger.Error("user.GetByEmail", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create auth record and attach to this user
|
||||||
|
authObj = auth.Model{
|
||||||
|
UserID: foundUser.ID,
|
||||||
|
Type: auth.TypeLDAP,
|
||||||
|
Identity: ldapUser.Username,
|
||||||
|
}
|
||||||
|
if err := authObj.Save(); err != nil {
|
||||||
|
logger.Error("auth.Save", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("Created LDAP auth for user: %s, %s", ldapUser.Username, foundUser.Email)
|
||||||
|
} else if authErr != nil {
|
||||||
|
logger.Error("auth.GetByIdenityType", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, authErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userObj, userErr := user.GetByID(authObj.UserID)
|
||||||
|
if userErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenOIDC(w http.ResponseWriter, r *http.Request, _ tokenPayload) {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "NOT YET SUPPORTED", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken an existing token by given them a new one with the same claims
|
||||||
|
// Route: POST /auth/refresh
|
||||||
|
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// TODO: Use your own methods to verify an existing user is
|
||||||
|
// able to refresh their token and then give them a new one
|
||||||
|
userObj, _ := user.GetByEmail("jc@jc21.com")
|
||||||
|
if response, err := njwt.Generate(&userObj, false); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSSEToken will generate and return a very short lived token for
|
||||||
|
// use by the /sse/* endpoint. It requires an app token to generate this
|
||||||
|
// Route: POST /auth/sse
|
||||||
|
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := r.Context().Value(c.UserIDCtxKey).(uint)
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
userObj, userErr := user.GetByID(userID)
|
||||||
|
if userErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if userObj.IsDisabled {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response, err := njwt.Generate(&userObj, true); err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
||||||
|
} else {
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,12 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the setting already exists
|
||||||
|
if _, err := setting.GetByName(newSetting.Name); err == nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Setting with name '%s' already exists", newSetting.Name), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err = newSetting.Save(); err != nil {
|
if err = newSetting.Save(); err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
|
||||||
return
|
return
|
||||||
@ -75,6 +81,7 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
|
|
||||||
// UpdateSetting updates a setting
|
// UpdateSetting updates a setting
|
||||||
// Route: PUT /settings/{name}
|
// Route: PUT /settings/{name}
|
||||||
|
// TODO: Add validation for the setting value, for system settings they should be validated against the setting name and type
|
||||||
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
settingName := chi.URLParam(r, "name")
|
settingName := chi.URLParam(r, "name")
|
||||||
@ -85,13 +92,12 @@ func UpdateSetting() func(http.ResponseWriter, *http.Request) {
|
|||||||
h.NotFound(w, r)
|
h.NotFound(w, r)
|
||||||
case nil:
|
case nil:
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
err := json.Unmarshal(bodyBytes, &setting)
|
if err := json.Unmarshal(bodyBytes, &setting); err != nil {
|
||||||
if err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = setting.Save(); err != nil {
|
if err := setting.Save(); err != nil {
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,116 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
h "npm/internal/api/http"
|
|
||||||
"npm/internal/errors"
|
|
||||||
"npm/internal/logger"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
|
||||||
"npm/internal/entity/auth"
|
|
||||||
"npm/internal/entity/user"
|
|
||||||
njwt "npm/internal/jwt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tokenPayload is the structure we expect from a incoming login request
|
|
||||||
type tokenPayload struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Identity string `json:"identity"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewToken Also known as a Login, requesting a new token with credentials
|
|
||||||
// Route: POST /tokens
|
|
||||||
func NewToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Read the bytes from the body
|
|
||||||
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
|
||||||
|
|
||||||
var payload tokenPayload
|
|
||||||
err := json.Unmarshal(bodyBytes, &payload)
|
|
||||||
if err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find user
|
|
||||||
userObj, userErr := user.GetByEmail(payload.Identity)
|
|
||||||
if userErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userObj.IsDisabled {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Auth
|
|
||||||
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
|
|
||||||
if authErr != nil {
|
|
||||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Auth
|
|
||||||
validateErr := authObj.ValidateSecret(payload.Secret)
|
|
||||||
if validateErr != nil {
|
|
||||||
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
|
|
||||||
// Sleep for 1 second to prevent brute force password guessing
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response, err := njwt.Generate(&userObj, false); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshToken an existing token by given them a new one with the same claims
|
|
||||||
// Route: GET /tokens
|
|
||||||
func RefreshToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// TODO: Use your own methods to verify an existing user is
|
|
||||||
// able to refresh their token and then give them a new one
|
|
||||||
userObj, _ := user.GetByEmail("jc@jc21.com")
|
|
||||||
if response, err := njwt.Generate(&userObj, false); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSSEToken will generate and return a very short lived token for
|
|
||||||
// use by the /sse/* endpoint. It requires an app token to generate this
|
|
||||||
// Route: POST /tokens/sse
|
|
||||||
func NewSSEToken() func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
userID := r.Context().Value(c.UserIDCtxKey).(uint)
|
|
||||||
|
|
||||||
// Find user
|
|
||||||
userObj, userErr := user.GetByID(userID)
|
|
||||||
if userErr != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userObj.IsDisabled {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if response, err := njwt.Generate(&userObj, true); err != nil {
|
|
||||||
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
|
|
||||||
} else {
|
|
||||||
h.ResultResponseJSON(w, r, http.StatusOK, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
c "npm/internal/api/context"
|
c "npm/internal/api/context"
|
||||||
h "npm/internal/api/http"
|
h "npm/internal/api/http"
|
||||||
@ -17,6 +18,13 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type setAuthModel struct {
|
||||||
|
// The json tags are required, as the change password form decodes into this object
|
||||||
|
Type string `json:"type"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
CurrentSecret string `json:"current_secret"`
|
||||||
|
}
|
||||||
|
|
||||||
// GetUsers returns all users
|
// GetUsers returns all users
|
||||||
// Route: GET /users
|
// Route: GET /users
|
||||||
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
func GetUsers() func(http.ResponseWriter, *http.Request) {
|
||||||
@ -188,7 +196,7 @@ func CreateUser() func(http.ResponseWriter, *http.Request) {
|
|||||||
// newUser has been saved, now save their auth
|
// newUser has been saved, now save their auth
|
||||||
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
|
||||||
newUser.Auth.UserID = newUser.ID
|
newUser.Auth.UserID = newUser.ID
|
||||||
if newUser.Auth.Type == auth.TypePassword {
|
if newUser.Auth.Type == auth.TypeLocal {
|
||||||
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("SetPasswordError", err)
|
logger.Error("SetPasswordError", err)
|
||||||
@ -247,3 +255,79 @@ func getUserIDFromRequest(r *http.Request) (uint, bool, error) {
|
|||||||
}
|
}
|
||||||
return userID, self, nil
|
return userID, self, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAuth sets a auth method. This can be used for "me" and `2` for example
|
||||||
|
// Route: POST /users/:userID/auth
|
||||||
|
func SetAuth() func(http.ResponseWriter, *http.Request) {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
|
||||||
|
|
||||||
|
var newAuth setAuthModel
|
||||||
|
err := json.Unmarshal(bodyBytes, &newAuth)
|
||||||
|
if err != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, isSelf, userIDErr := getUserIDFromRequest(r)
|
||||||
|
if userIDErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load user
|
||||||
|
thisUser, thisUserErr := user.GetByID(userID)
|
||||||
|
if thisUserErr == gorm.ErrRecordNotFound {
|
||||||
|
h.NotFound(w, r)
|
||||||
|
return
|
||||||
|
} else if thisUserErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if thisUser.IsSystem {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing auth for user
|
||||||
|
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
|
||||||
|
if userAuthErr != nil {
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSelf {
|
||||||
|
// confirm that the current_secret given is valid for the one stored in the database
|
||||||
|
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
|
||||||
|
if validateErr != nil {
|
||||||
|
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
|
||||||
|
// Sleep for 1 second to prevent brute force password guessing
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newAuth.Type == auth.TypeLocal {
|
||||||
|
err := userAuth.SetPassword(newAuth.Secret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("SetPasswordError", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = userAuth.Save(); err != nil {
|
||||||
|
logger.Error("AuthSaveError", err)
|
||||||
|
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userAuth.Secret = ""
|
||||||
|
|
||||||
|
// todo: add to audit-log
|
||||||
|
|
||||||
|
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,9 +4,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"npm/internal/entity/user"
|
"npm/internal/entity/user"
|
||||||
"npm/internal/model"
|
"npm/internal/model"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/qri-io/jsonschema"
|
"github.com/qri-io/jsonschema"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -36,9 +37,8 @@ func TestResultResponseJSON(t *testing.T) {
|
|||||||
ModelBase: model.ModelBase{ID: 10},
|
ModelBase: model.ModelBase{ID: 10},
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Nickname: "Jonny",
|
|
||||||
},
|
},
|
||||||
want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"nickname\":\"Jonny\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}",
|
want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error response",
|
name: "error response",
|
||||||
|
@ -74,12 +74,13 @@ func applyRoutes(r chi.Router) chi.Router {
|
|||||||
r.With(middleware.EnforceSetup(), middleware.Enforce()).
|
r.With(middleware.EnforceSetup(), middleware.Enforce()).
|
||||||
Get("/config", handler.Config())
|
Get("/config", handler.Config())
|
||||||
|
|
||||||
// Tokens
|
// Auth
|
||||||
r.With(middleware.EnforceSetup()).Route("/tokens", func(r chi.Router) {
|
r.With(middleware.EnforceSetup()).Route("/auth", func(r chi.Router) {
|
||||||
|
r.Get("/", handler.GetAuthConfig())
|
||||||
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
|
||||||
Post("/", handler.NewToken())
|
Post("/", handler.NewToken())
|
||||||
r.With(middleware.Enforce()).
|
r.With(middleware.Enforce()).
|
||||||
Get("/", handler.RefreshToken())
|
Post("/refresh", handler.RefreshToken())
|
||||||
r.With(middleware.Enforce()).
|
r.With(middleware.Enforce()).
|
||||||
Post("/sse", handler.NewSSEToken())
|
Post("/sse", handler.NewSSEToken())
|
||||||
})
|
})
|
||||||
|
@ -64,6 +64,9 @@ const anyType = `
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ func CreateUser() string {
|
|||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"nickname": %s,
|
|
||||||
"email": %s,
|
"email": %s,
|
||||||
"is_disabled": {
|
"is_disabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@ -30,7 +29,7 @@ func CreateUser() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^local$"
|
||||||
},
|
},
|
||||||
"secret": %s
|
"secret": %s
|
||||||
}
|
}
|
||||||
@ -38,5 +37,5 @@ func CreateUser() string {
|
|||||||
"capabilities": %s
|
"capabilities": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
|
`, stringMinMax(2, 50), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ func GetToken() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"enum": ["local", "ldap", "oidc"]
|
||||||
},
|
},
|
||||||
"identity": %s,
|
"identity": %s,
|
||||||
"secret": %s
|
"secret": %s
|
||||||
|
@ -3,6 +3,7 @@ package schema
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// SetAuth is the schema for incoming data validation
|
// SetAuth is the schema for incoming data validation
|
||||||
|
// Only local auth is supported for setting a password
|
||||||
func SetAuth() string {
|
func SetAuth() string {
|
||||||
return fmt.Sprintf(`
|
return fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
@ -15,7 +16,7 @@ func SetAuth() string {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"pattern": "^password$"
|
"pattern": "^local$"
|
||||||
},
|
},
|
||||||
"secret": %s,
|
"secret": %s,
|
||||||
"current_secret": %s
|
"current_secret": %s
|
||||||
|
@ -11,7 +11,6 @@ func UpdateUser() string {
|
|||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": %s,
|
"name": %s,
|
||||||
"nickname": %s,
|
|
||||||
"email": %s,
|
"email": %s,
|
||||||
"is_disabled": {
|
"is_disabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@ -19,5 +18,5 @@ func UpdateUser() string {
|
|||||||
"capabilities": %s
|
"capabilities": %s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties())
|
`, stringMinMax(2, 50), stringMinMax(5, 150), capabilties())
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ func (s *testsuite) SetupTest() {
|
|||||||
}).AddRow(
|
}).AddRow(
|
||||||
10,
|
10,
|
||||||
100,
|
100,
|
||||||
TypePassword,
|
TypeLocal,
|
||||||
"abc123",
|
"abc123",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -54,7 +54,7 @@ func TestExampleTestSuite(t *testing.T) {
|
|||||||
func assertModel(t *testing.T, m Model) {
|
func assertModel(t *testing.T, m Model) {
|
||||||
assert.Equal(t, uint(10), m.ID)
|
assert.Equal(t, uint(10), m.ID)
|
||||||
assert.Equal(t, uint(100), m.UserID)
|
assert.Equal(t, uint(100), m.UserID)
|
||||||
assert.Equal(t, TypePassword, m.Type)
|
assert.Equal(t, TypeLocal, m.Type)
|
||||||
assert.Equal(t, "abc123", m.Secret)
|
assert.Equal(t, "abc123", m.Secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,10 +83,10 @@ func (s *testsuite) TestGetByUserIDType() {
|
|||||||
|
|
||||||
s.mock.
|
s.mock.
|
||||||
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)).
|
ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)).
|
||||||
WithArgs(100, TypePassword, 0, 1).
|
WithArgs(100, TypeLocal, 0, 1).
|
||||||
WillReturnRows(s.singleRow)
|
WillReturnRows(s.singleRow)
|
||||||
|
|
||||||
m, err := GetByUserIDType(100, TypePassword)
|
m, err := GetByUserIDType(100, TypeLocal)
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
require.NoError(s.T(), s.mock.ExpectationsWereMet())
|
require.NoError(s.T(), s.mock.ExpectationsWereMet())
|
||||||
assertModel(s.T(), m)
|
assertModel(s.T(), m)
|
||||||
@ -103,7 +103,7 @@ func (s *testsuite) TestSave() {
|
|||||||
sqlmock.AnyArg(),
|
sqlmock.AnyArg(),
|
||||||
0,
|
0,
|
||||||
100,
|
100,
|
||||||
TypePassword,
|
TypeLocal,
|
||||||
"abc123",
|
"abc123",
|
||||||
).
|
).
|
||||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11"))
|
||||||
@ -112,7 +112,7 @@ func (s *testsuite) TestSave() {
|
|||||||
// New model
|
// New model
|
||||||
m := Model{
|
m := Model{
|
||||||
UserID: 100,
|
UserID: 100,
|
||||||
Type: TypePassword,
|
Type: TypeLocal,
|
||||||
Secret: "abc123",
|
Secret: "abc123",
|
||||||
}
|
}
|
||||||
err := m.Save()
|
err := m.Save()
|
||||||
@ -127,7 +127,7 @@ func (s *testsuite) TestSetPassword() {
|
|||||||
m := Model{UserID: 100}
|
m := Model{UserID: 100}
|
||||||
err := m.SetPassword("abc123")
|
err := m.SetPassword("abc123")
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
assert.Equal(s.T(), TypePassword, m.Type)
|
assert.Equal(s.T(), TypeLocal, m.Type)
|
||||||
assert.Greater(s.T(), len(m.Secret), 15)
|
assert.Greater(s.T(), len(m.Secret), 15)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -143,10 +143,10 @@ func (s *testsuite) TestValidateSecret() {
|
|||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
err = m.ValidateSecret("this is not the password")
|
err = m.ValidateSecret("this is not the password")
|
||||||
assert.NotNil(s.T(), err)
|
assert.NotNil(s.T(), err)
|
||||||
assert.Equal(s.T(), "Invalid Password", err.Error())
|
assert.Equal(s.T(), "Invalid Credentials", err.Error())
|
||||||
|
|
||||||
m.Type = "not a valid type"
|
m.Type = "not a valid type"
|
||||||
err = m.ValidateSecret("abc123")
|
err = m.ValidateSecret("abc123")
|
||||||
assert.NotNil(s.T(), err)
|
assert.NotNil(s.T(), err)
|
||||||
assert.Equal(s.T(), "Could not validate Secret, auth type is not a Password", err.Error())
|
assert.Equal(s.T(), "Could not validate Secret, auth type is not Local", err.Error())
|
||||||
}
|
}
|
||||||
|
96
backend/internal/entity/auth/ldap.go
Normal file
96
backend/internal/entity/auth/ldap.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"npm/internal/entity/setting"
|
||||||
|
"npm/internal/logger"
|
||||||
|
|
||||||
|
ldap3 "github.com/go-ldap/ldap/v3"
|
||||||
|
"github.com/rotisserie/eris"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LDAPUser is the LDAP User
|
||||||
|
type LDAPUser struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAPAuthenticate will use ldap to authenticate with user/pass
|
||||||
|
func LDAPAuthenticate(identity, password string) (*LDAPUser, error) {
|
||||||
|
ldapSettings, err := setting.GetLDAPSettings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dn := strings.Replace(ldapSettings.UserDN, "{{USERNAME}}", identity, 1)
|
||||||
|
conn, err := ldapConnect(ldapSettings.Host, dn, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// nolint: errcheck, gosec
|
||||||
|
defer conn.Close()
|
||||||
|
return ldapSearchUser(conn, ldapSettings, identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt ldap connection
|
||||||
|
func ldapConnect(host, dn, password string) (*ldap3.Conn, error) {
|
||||||
|
var conn *ldap3.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if conn, err = ldap3.DialURL(fmt.Sprintf("ldap://%s", host)); err != nil {
|
||||||
|
logger.Error("LdapError", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LDAP Logging in with: %s", dn)
|
||||||
|
if err := conn.Bind(dn, password); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "Invalid Credentials") {
|
||||||
|
logger.Error("LDAPAuthError", err)
|
||||||
|
}
|
||||||
|
// nolint: gosec, errcheck
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("LDAP Login Successful")
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ldapSearchUser(l *ldap3.Conn, ldapSettings setting.LDAPSettings, username string) (*LDAPUser, error) {
|
||||||
|
// Search for the given username
|
||||||
|
searchRequest := ldap3.NewSearchRequest(
|
||||||
|
ldapSettings.BaseDN,
|
||||||
|
ldap3.ScopeWholeSubtree,
|
||||||
|
ldap3.NeverDerefAliases,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
strings.Replace(ldapSettings.SelfFilter, "{{USERNAME}}", username, 1),
|
||||||
|
nil, // []string{"name"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
sr, err := l.Search(searchRequest)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("LdapError", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sr.Entries) < 1 {
|
||||||
|
return nil, eris.New("No user found in LDAP search")
|
||||||
|
} else if len(sr.Entries) > 1 {
|
||||||
|
j, _ := json.Marshal(sr)
|
||||||
|
logger.Debug("LDAP Search Results: %s", j)
|
||||||
|
return nil, eris.Errorf("Too many LDAP results returned in LDAP search: %d", len(sr.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LDAPUser{
|
||||||
|
Username: strings.ToLower(username),
|
||||||
|
Name: sr.Entries[0].GetAttributeValue(ldapSettings.NameProperty),
|
||||||
|
Email: strings.ToLower(sr.Entries[0].GetAttributeValue(ldapSettings.EmailProperty)),
|
||||||
|
}, nil
|
||||||
|
}
|
@ -11,7 +11,7 @@ func GetByID(id int) (Model, error) {
|
|||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByUserIDType finds a user by email
|
// GetByUserIDType finds a user by id and type
|
||||||
func GetByUserIDType(userID uint, authType string) (Model, error) {
|
func GetByUserIDType(userID uint, authType string) (Model, error) {
|
||||||
var auth Model
|
var auth Model
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
@ -21,3 +21,14 @@ func GetByUserIDType(userID uint, authType string) (Model, error) {
|
|||||||
First(&auth)
|
First(&auth)
|
||||||
return auth, result.Error
|
return auth, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByUserIDType finds a user by id and type
|
||||||
|
func GetByIdenityType(identity string, authType string) (Model, error) {
|
||||||
|
var auth Model
|
||||||
|
db := database.GetDB()
|
||||||
|
result := db.
|
||||||
|
Where("identity = ?", identity).
|
||||||
|
Where("type = ?", authType).
|
||||||
|
First(&auth)
|
||||||
|
return auth, result.Error
|
||||||
|
}
|
||||||
|
@ -8,16 +8,19 @@ import (
|
|||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Auth types
|
||||||
const (
|
const (
|
||||||
// TypePassword is the Password Type
|
TypeLocal = "local"
|
||||||
TypePassword = "password"
|
TypeLDAP = "ldap"
|
||||||
|
TypeOIDC = "oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Model is the model
|
// Model is the model
|
||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.ModelBase
|
||||||
UserID uint `json:"user_id" gorm:"column:user_id"`
|
UserID uint `json:"user_id" gorm:"column:user_id"`
|
||||||
Type string `json:"type" gorm:"column:type;default:password"`
|
Type string `json:"type" gorm:"column:type;default:local"`
|
||||||
|
Identity string `json:"identity,omitempty" gorm:"column:identity"`
|
||||||
Secret string `json:"secret,omitempty" gorm:"column:secret"`
|
Secret string `json:"secret,omitempty" gorm:"column:secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +51,7 @@ func (m *Model) SetPassword(password string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Type = TypePassword
|
m.Type = TypeLocal
|
||||||
m.Secret = string(hash)
|
m.Secret = string(hash)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -56,13 +59,13 @@ func (m *Model) SetPassword(password string) error {
|
|||||||
|
|
||||||
// ValidateSecret will check if a given secret matches the encrypted secret
|
// ValidateSecret will check if a given secret matches the encrypted secret
|
||||||
func (m *Model) ValidateSecret(secret string) error {
|
func (m *Model) ValidateSecret(secret string) error {
|
||||||
if m.Type != TypePassword {
|
if m.Type != TypeLocal {
|
||||||
return eris.New("Could not validate Secret, auth type is not a Password")
|
return eris.New("Could not validate Secret, auth type is not Local")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
|
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return eris.New("Invalid Password")
|
return eris.New("Invalid Credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
20
backend/internal/entity/setting/auth_methods.go
Normal file
20
backend/internal/entity/setting/auth_methods.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAuthMethods returns the authentication methods enabled for this site
|
||||||
|
func GetAuthMethods() ([]string, error) {
|
||||||
|
var l []string
|
||||||
|
var m Model
|
||||||
|
if err := m.LoadByName("auth-methods"); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
32
backend/internal/entity/setting/ldap.go
Normal file
32
backend/internal/entity/setting/ldap.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LDAPSettings are the settings for LDAP that come from
|
||||||
|
// the `ldap-auth` setting value
|
||||||
|
type LDAPSettings struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
BaseDN string `json:"base_dn"`
|
||||||
|
UserDN string `json:"user_dn"`
|
||||||
|
EmailProperty string `json:"email_property"`
|
||||||
|
NameProperty string `json:"name_property"`
|
||||||
|
SelfFilter string `json:"self_filter"`
|
||||||
|
AutoCreateUser bool `json:"auto_create_user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLDAPSettings will return the LDAP settings
|
||||||
|
func GetLDAPSettings() (LDAPSettings, error) {
|
||||||
|
var l LDAPSettings
|
||||||
|
var m Model
|
||||||
|
if err := m.LoadByName("ldap-auth"); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil {
|
||||||
|
return l, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
@ -41,14 +41,12 @@ func (s *testsuite) SetupTest() {
|
|||||||
s.singleRow = sqlmock.NewRows([]string{
|
s.singleRow = sqlmock.NewRows([]string{
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"nickname",
|
|
||||||
"email",
|
"email",
|
||||||
"is_disabled",
|
"is_disabled",
|
||||||
"is_system",
|
"is_system",
|
||||||
}).AddRow(
|
}).AddRow(
|
||||||
10,
|
10,
|
||||||
"John Doe",
|
"John Doe",
|
||||||
"Jonny",
|
|
||||||
"jon@example.com",
|
"jon@example.com",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -74,14 +72,12 @@ func (s *testsuite) SetupTest() {
|
|||||||
s.listRows = sqlmock.NewRows([]string{
|
s.listRows = sqlmock.NewRows([]string{
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"nickname",
|
|
||||||
"email",
|
"email",
|
||||||
"is_disabled",
|
"is_disabled",
|
||||||
"is_system",
|
"is_system",
|
||||||
}).AddRow(
|
}).AddRow(
|
||||||
10,
|
10,
|
||||||
"John Doe",
|
"John Doe",
|
||||||
"Jonny",
|
|
||||||
"jon@example.com",
|
"jon@example.com",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -104,7 +100,6 @@ func TestExampleTestSuite(t *testing.T) {
|
|||||||
func assertModel(t *testing.T, m Model) {
|
func assertModel(t *testing.T, m Model) {
|
||||||
assert.Equal(t, uint(10), m.ID)
|
assert.Equal(t, uint(10), m.ID)
|
||||||
assert.Equal(t, "John Doe", m.Name)
|
assert.Equal(t, "John Doe", m.Name)
|
||||||
assert.Equal(t, "Jonny", m.Nickname)
|
|
||||||
assert.Equal(t, "jon@example.com", m.Email)
|
assert.Equal(t, "jon@example.com", m.Email)
|
||||||
assert.Equal(t, false, m.IsDisabled)
|
assert.Equal(t, false, m.IsDisabled)
|
||||||
assert.Equal(t, false, m.IsSystem)
|
assert.Equal(t, false, m.IsSystem)
|
||||||
@ -182,7 +177,7 @@ func (s *testsuite) TestSave() {
|
|||||||
WillReturnRows(s.singleRow)
|
WillReturnRows(s.singleRow)
|
||||||
|
|
||||||
s.mock.ExpectBegin()
|
s.mock.ExpectBegin()
|
||||||
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","nickname","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)).
|
s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)).
|
||||||
WithArgs(
|
WithArgs(
|
||||||
sqlmock.AnyArg(),
|
sqlmock.AnyArg(),
|
||||||
sqlmock.AnyArg(),
|
sqlmock.AnyArg(),
|
||||||
@ -199,7 +194,6 @@ func (s *testsuite) TestSave() {
|
|||||||
// New model, as system
|
// New model, as system
|
||||||
m := Model{
|
m := Model{
|
||||||
Name: "John Doe",
|
Name: "John Doe",
|
||||||
Nickname: "Jonny",
|
|
||||||
Email: "JON@example.com", // mixed case on purpose
|
Email: "JON@example.com", // mixed case on purpose
|
||||||
IsSystem: true,
|
IsSystem: true,
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,10 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"npm/internal/database"
|
"npm/internal/database"
|
||||||
"npm/internal/entity"
|
"npm/internal/entity"
|
||||||
|
"npm/internal/entity/auth"
|
||||||
"npm/internal/logger"
|
"npm/internal/logger"
|
||||||
"npm/internal/model"
|
"npm/internal/model"
|
||||||
)
|
)
|
||||||
@ -104,3 +106,14 @@ func GetCapabilities(userID uint) ([]string, error) {
|
|||||||
}
|
}
|
||||||
return capabilities, nil
|
return capabilities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateFromLDAPUser will create a user from an LDAP user object
|
||||||
|
func CreateFromLDAPUser(ldapUser *auth.LDAPUser) (Model, error) {
|
||||||
|
user := Model{
|
||||||
|
Email: ldapUser.Email,
|
||||||
|
Name: ldapUser.Name,
|
||||||
|
}
|
||||||
|
err := user.Save()
|
||||||
|
user.generateGravatar()
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
@ -18,7 +18,6 @@ import (
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
model.ModelBase
|
model.ModelBase
|
||||||
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:name" filter:"name,string"`
|
||||||
Nickname string `json:"nickname" gorm:"column:nickname" filter:"nickname,string"`
|
|
||||||
Email string `json:"email" gorm:"column:email" filter:"email,email"`
|
Email string `json:"email" gorm:"column:email" filter:"email,email"`
|
||||||
IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"`
|
IsDisabled bool `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"`
|
||||||
IsSystem bool `json:"is_system,omitempty" gorm:"column:is_system" filter:"is_system,boolean"`
|
IsSystem bool `json:"is_system,omitempty" gorm:"column:is_system" filter:"is_system,boolean"`
|
||||||
|
@ -10,6 +10,7 @@ var (
|
|||||||
ErrDatabaseUnavailable = eris.New("database-unavailable")
|
ErrDatabaseUnavailable = eris.New("database-unavailable")
|
||||||
ErrDuplicateEmailUser = eris.New("email-already-exists")
|
ErrDuplicateEmailUser = eris.New("email-already-exists")
|
||||||
ErrInvalidLogin = eris.New("invalid-login-credentials")
|
ErrInvalidLogin = eris.New("invalid-login-credentials")
|
||||||
|
ErrInvalidAuthType = eris.New("invalid-auth-type")
|
||||||
ErrUserDisabled = eris.New("user-disabled")
|
ErrUserDisabled = eris.New("user-disabled")
|
||||||
ErrSystemUserReadonly = eris.New("cannot-save-system-users")
|
ErrSystemUserReadonly = eris.New("cannot-save-system-users")
|
||||||
ErrValidationFailed = eris.New("request-failed-validation")
|
ErrValidationFailed = eris.New("request-failed-validation")
|
||||||
|
@ -7,7 +7,7 @@ type Filter struct {
|
|||||||
Value []string `json:"value"`
|
Value []string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterMapValue ...
|
// FilterMapValue is the structure of a filter map value
|
||||||
type FilterMapValue struct {
|
type FilterMapValue struct {
|
||||||
Type string
|
Type string
|
||||||
Field string
|
Field string
|
||||||
|
@ -15,7 +15,7 @@ type Sort struct {
|
|||||||
Direction string `json:"direction"`
|
Direction string `json:"direction"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSort ...
|
// GetSort is the sort array
|
||||||
func (p *PageInfo) GetSort(def Sort) []Sort {
|
func (p *PageInfo) GetSort(def Sort) []Sort {
|
||||||
if p.Sort == nil {
|
if p.Sort == nil {
|
||||||
return []Sort{def}
|
return []Sort{def}
|
||||||
|
@ -18,7 +18,7 @@ func TestPageInfoGetSort(t *testing.T) {
|
|||||||
Direction: "asc",
|
Direction: "asc",
|
||||||
}
|
}
|
||||||
defined := Sort{
|
defined := Sort{
|
||||||
Field: "nickname",
|
Field: "email",
|
||||||
Direction: "desc",
|
Direction: "desc",
|
||||||
}
|
}
|
||||||
// default
|
// default
|
||||||
|
@ -18,7 +18,6 @@ func TestGetFilterSchema(t *testing.T) {
|
|||||||
ID uint `json:"id" gorm:"column:user_id" filter:"id,number"`
|
ID uint `json:"id" gorm:"column:user_id" filter:"id,number"`
|
||||||
Created time.Time `json:"created" gorm:"column:user_created_date" filter:"created,date"`
|
Created time.Time `json:"created" gorm:"column:user_created_date" filter:"created,date"`
|
||||||
Name string `json:"name" gorm:"column:user_name" filter:"name,string"`
|
Name string `json:"name" gorm:"column:user_name" filter:"name,string"`
|
||||||
NickName string `json:"nickname" gorm:"column:user_nickname" filter:"nickname"`
|
|
||||||
IsDisabled string `json:"is_disabled" gorm:"column:user_is_disabled" filter:"is_disabled,bool"`
|
IsDisabled string `json:"is_disabled" gorm:"column:user_is_disabled" filter:"is_disabled,bool"`
|
||||||
Permissions string `json:"permissions" gorm:"column:user_permissions" filter:"permissions,regex"`
|
Permissions string `json:"permissions" gorm:"column:user_permissions" filter:"permissions,regex"`
|
||||||
History string `json:"history" gorm:"column:user_history" filter:"history,regex,(id|name)"`
|
History string `json:"history" gorm:"column:user_history" filter:"history,regex,(id|name)"`
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:ci": "yarn lint --watchAll=false",
|
"lint:ci": "yarn lint --watchAll=false",
|
||||||
|
@ -6,7 +6,7 @@ export async function getSSEToken(
|
|||||||
): Promise<TokenResponse> {
|
): Promise<TokenResponse> {
|
||||||
const { result } = await api.post(
|
const { result } = await api.post(
|
||||||
{
|
{
|
||||||
url: "/tokens/sse",
|
url: "/auth/sse",
|
||||||
},
|
},
|
||||||
abortController,
|
abortController,
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,7 @@ export async function getToken(
|
|||||||
): Promise<TokenResponse> {
|
): Promise<TokenResponse> {
|
||||||
const { result } = await api.post(
|
const { result } = await api.post(
|
||||||
{
|
{
|
||||||
url: "/tokens",
|
url: "/auth",
|
||||||
data: payload,
|
data: payload,
|
||||||
},
|
},
|
||||||
abortController,
|
abortController,
|
||||||
|
@ -4,9 +4,9 @@ import { TokenResponse } from "./responseTypes";
|
|||||||
export async function refreshToken(
|
export async function refreshToken(
|
||||||
abortController?: AbortController,
|
abortController?: AbortController,
|
||||||
): Promise<TokenResponse> {
|
): Promise<TokenResponse> {
|
||||||
const { result } = await api.get(
|
const { result } = await api.post(
|
||||||
{
|
{
|
||||||
url: "/tokens",
|
url: "/auth/refresh",
|
||||||
},
|
},
|
||||||
abortController,
|
abortController,
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ReactNode, createContext, useContext, useState } from "react";
|
import { createContext, ReactNode, useContext, useState } from "react";
|
||||||
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useIntervalWhen } from "rooks";
|
import { useIntervalWhen } from "rooks";
|
||||||
@ -9,7 +9,7 @@ import AuthStore from "src/modules/AuthStore";
|
|||||||
// Context
|
// Context
|
||||||
export interface AuthContextType {
|
export interface AuthContextType {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (type: string, username: string, password: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
token?: string;
|
token?: string;
|
||||||
}
|
}
|
||||||
@ -36,8 +36,7 @@ function AuthProvider({
|
|||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async (identity: string, secret: string) => {
|
const login = async (type: string, identity: string, secret: string) => {
|
||||||
const type = "password";
|
|
||||||
const response = await getToken({ payload: { type, identity, secret } });
|
const response = await getToken({ payload: { type, identity, secret } });
|
||||||
handleTokenUpdate(response);
|
handleTokenUpdate(response);
|
||||||
};
|
};
|
||||||
|
@ -5,16 +5,16 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
Stack,
|
Stack,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
|
|
||||||
import { setAuth } from "src/api/npm";
|
import { setAuth } from "src/api/npm";
|
||||||
import { PrettyButton } from "src/components";
|
import { PrettyButton } from "src/components";
|
||||||
@ -43,7 +43,7 @@ function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setAuth("me", {
|
await setAuth("me", {
|
||||||
type: "password",
|
type: "local",
|
||||||
secret: payload.password,
|
secret: payload.password,
|
||||||
currentSecret: payload.current,
|
currentSecret: payload.current,
|
||||||
});
|
});
|
||||||
|
@ -5,16 +5,16 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
Stack,
|
Stack,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
|
|
||||||
import { setAuth } from "src/api/npm";
|
import { setAuth } from "src/api/npm";
|
||||||
import { PrettyButton } from "src/components";
|
import { PrettyButton } from "src/components";
|
||||||
@ -44,7 +44,7 @@ function SetPasswordModal({ userId, isOpen, onClose }: SetPasswordModalProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await setAuth(userId, {
|
await setAuth(userId, {
|
||||||
type: "password",
|
type: "local",
|
||||||
secret: payload.password,
|
secret: payload.password,
|
||||||
});
|
});
|
||||||
onClose();
|
onClose();
|
||||||
|
@ -7,22 +7,22 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
|
||||||
ModalContent,
|
|
||||||
ModalHeader,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalBody,
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
Stack,
|
Stack,
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
|
|
||||||
import { createUser } from "src/api/npm";
|
import { createUser } from "src/api/npm";
|
||||||
import {
|
import {
|
||||||
@ -56,7 +56,7 @@ function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) {
|
|||||||
...{
|
...{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
auth: {
|
auth: {
|
||||||
type: "password",
|
type: "local",
|
||||||
secret: values.password,
|
secret: values.password,
|
||||||
},
|
},
|
||||||
capabilities,
|
capabilities,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Center,
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
Box,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@ -12,7 +12,7 @@ import {
|
|||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
|
|
||||||
import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components";
|
import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components";
|
||||||
import { useAuthState } from "src/context";
|
import { useAuthState } from "src/context";
|
||||||
@ -40,7 +40,7 @@ function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await login(values.email, values.password);
|
await login("local", values.email, values.password);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
showErr(err.message);
|
showErr(err.message);
|
||||||
|
@ -7,14 +7,14 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Stack,
|
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Input,
|
||||||
|
Stack,
|
||||||
useColorModeValue,
|
useColorModeValue,
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Field, Form, Formik } from "formik";
|
||||||
|
|
||||||
import { createUser } from "src/api/npm";
|
import { createUser } from "src/api/npm";
|
||||||
// import logo from "src/assets/logo-256.png";
|
// import logo from "src/assets/logo-256.png";
|
||||||
@ -42,7 +42,7 @@ function Setup() {
|
|||||||
...{
|
...{
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
auth: {
|
auth: {
|
||||||
type: "password",
|
type: "local",
|
||||||
secret: values.password,
|
secret: values.password,
|
||||||
},
|
},
|
||||||
capabilities: ["full-admin"],
|
capabilities: ["full-admin"],
|
||||||
@ -65,7 +65,7 @@ function Setup() {
|
|||||||
const response = await createUser(payload);
|
const response = await createUser(payload);
|
||||||
if (response && typeof response.id !== "undefined" && response.id) {
|
if (response && typeof response.id !== "undefined" && response.id) {
|
||||||
try {
|
try {
|
||||||
await login(response.email, password);
|
await login("local", response.email, password);
|
||||||
// Trigger a Health change
|
// Trigger a Health change
|
||||||
await queryClient.refetchQueries({ queryKey: ["health"] });
|
await queryClient.refetchQueries({ queryKey: ["health"] });
|
||||||
// window.location.reload();
|
// window.location.reload();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user