Support for bearer token authentication (#120)
* User manager can create and recognize auth tokens * Implement HTTP bearer auth * Use bearer auth in test-harness * Can receive auth token via query parameter (useful for media endpoints)
This commit is contained in:
parent
5e065c5e6a
commit
72c8ed9289
20 changed files with 737 additions and 131 deletions
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -448,6 +448,17 @@ dependencies = [
|
|||
"byte-tools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "branca"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05e85a179a6f9595d798a16a97ce64312beb20cfe30ebc40f94271a5fda89f3f"
|
||||
dependencies = [
|
||||
"base-x",
|
||||
"byteorder",
|
||||
"orion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-sys"
|
||||
version = "0.3.2"
|
||||
|
@ -1039,6 +1050,17 @@ dependencies = [
|
|||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8025cf36f917e6a52cce185b7c7177689b838b7ec138364e50cc2277a56cf4"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.11.1"
|
||||
|
@ -1644,6 +1666,18 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afbb993947f111397c2bc536944f8dac7f54a4e73383d478efe1990b56404b60"
|
||||
|
||||
[[package]]
|
||||
name = "orion"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1066cd107e316d26fe9f8657525afc01ed74b3288126ee7e58b5b141916aab09"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
"getrandom 0.2.0",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.1"
|
||||
|
@ -1776,6 +1810,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"ape",
|
||||
"base64 0.13.0",
|
||||
"branca",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"diesel",
|
||||
|
@ -1902,7 +1937,7 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.15",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
|
@ -1925,7 +1960,7 @@ version = "0.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2904,3 +2939,9 @@ dependencies = [
|
|||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36"
|
||||
|
|
|
@ -16,6 +16,7 @@ actix-web-httpauth = { version = "0.5.0" }
|
|||
anyhow = "1.0.35"
|
||||
ape = "0.3.0"
|
||||
base64 = "0.13"
|
||||
branca = "0.10.0"
|
||||
cookie = { version = "0.14", features = ["signed", "key-expansion"] }
|
||||
crossbeam-channel = "0.5"
|
||||
diesel_migrations = { version = "1.4", features = ["sqlite"] }
|
||||
|
|
|
@ -93,7 +93,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -123,7 +125,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -151,7 +155,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -180,7 +186,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -207,7 +215,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -235,7 +245,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -265,7 +277,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -295,7 +309,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -335,7 +351,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -363,7 +381,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_http_header": [],
|
||||
"admin_http_basic": [],
|
||||
"admin_http_bearer": [],
|
||||
"admin_query_parameter": [],
|
||||
"admin_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -390,7 +410,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -418,7 +440,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -436,14 +460,21 @@
|
|||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#components/schemas/AuthCredentials"
|
||||
"$ref": "#components/schemas/Credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation"
|
||||
"description": "Successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Authorization"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid credentials"
|
||||
|
@ -475,7 +506,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -515,7 +548,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -545,7 +580,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -585,7 +622,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -615,7 +654,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -645,7 +686,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -685,7 +728,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -722,7 +767,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -768,7 +815,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -798,7 +847,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -838,7 +889,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -876,7 +929,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -904,7 +959,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -934,7 +991,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -964,7 +1023,38 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/lastfm/link_token": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Last.fm"
|
||||
],
|
||||
"summary": "Obtain an authentication token to be used when linking a Polaris account to a Last.fm account. The token is only valid for 10 minutes.",
|
||||
"operationId": "getLastFMLinkToken",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LastFMLinkToken"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -982,6 +1072,15 @@
|
|||
},
|
||||
"operationId": "getLastFMLink",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "auth_token",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"description": "Polaris authentication token received from the `lastfm/link_token` endpoint",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
|
@ -1025,7 +1124,9 @@
|
|||
},
|
||||
"security": [
|
||||
{
|
||||
"auth_http_header": [],
|
||||
"auth_http_basic": [],
|
||||
"auth_http_bearer": [],
|
||||
"auth_query_parameter": [],
|
||||
"auth_cookie": []
|
||||
}
|
||||
]
|
||||
|
@ -1183,7 +1284,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"AuthCredentials": {
|
||||
"Credentials": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
|
@ -1194,6 +1295,28 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Authorization": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_admin": {
|
||||
"type": "bool"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LastFMLinkToken": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CollectionFile": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
@ -1320,26 +1443,49 @@
|
|||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"auth_http_header": {
|
||||
"auth_http_bearer": {
|
||||
"type": "http",
|
||||
"scheme": "basic"
|
||||
"scheme": "bearer",
|
||||
"description": "An authentication token obtained in the output of the `auth` endpoint"
|
||||
},
|
||||
"admin_http_bearer": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"description": "Identical to the auth_http_bearer scheme but only for users recognized as admin by the Polaris server"
|
||||
},
|
||||
"auth_query_parameter": {
|
||||
"type": "apikey",
|
||||
"in": "query",
|
||||
"name": "auth_token",
|
||||
"description": "An authentication token obtained in the output of the `auth` endpoint"
|
||||
},
|
||||
"admin_query_parameter": {
|
||||
"type": "apikey",
|
||||
"in": "query",
|
||||
"name": "auth_token",
|
||||
"description": "Identical to the auth_query_parameter scheme but only for users recognized as admin by the Polaris server"
|
||||
},
|
||||
"auth_http_basic": {
|
||||
"type": "http",
|
||||
"scheme": "basic",
|
||||
"description": "[deprecated]"
|
||||
},
|
||||
"admin_http_basic": {
|
||||
"type": "http",
|
||||
"scheme": "basic",
|
||||
"description": "[deprecated] Identical to the auth_http_basic scheme but only for users recognized as admin by the Polaris server"
|
||||
},
|
||||
"auth_cookie": {
|
||||
"type": "apikey",
|
||||
"in": "cookie",
|
||||
"name": "session",
|
||||
"description": "A session token obtained returned as a server cookie by making a request via the auth_http_header scheme."
|
||||
},
|
||||
"admin_http_header": {
|
||||
"type": "http",
|
||||
"scheme": "basic",
|
||||
"description": "Identical to the auth_http_header scheme but only for users recognized as admin by the Polaris server"
|
||||
"description": "[deprecated] A token obtained via the SET-COOKIE header in a response to a request via the auth_http_basic scheme, or a request to the `auth` endpoint."
|
||||
},
|
||||
"admin_cookie": {
|
||||
"type": "apikey",
|
||||
"in": "cookie",
|
||||
"name": "session",
|
||||
"description": "Identical to the auth_cookie scheme but only for users recognized as admin by the Polaris server"
|
||||
"description": "[deprecated] Identical to the auth_cookie scheme but only for users recognized as admin by the Polaris server"
|
||||
}
|
||||
},
|
||||
"links": {},
|
||||
|
|
|
@ -24,7 +24,8 @@ fn get_test_db(name: &str) -> DB {
|
|||
fn apply_saves_misc_settings() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let config_manager = Manager::new(
|
||||
|
@ -60,7 +61,8 @@ fn apply_saves_misc_settings() {
|
|||
fn apply_saves_mount_points() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let config_manager = Manager::new(
|
||||
|
@ -89,7 +91,8 @@ fn apply_saves_ddns_settings() {
|
|||
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let config_manager = Manager::new(
|
||||
|
@ -117,7 +120,8 @@ fn apply_saves_ddns_settings() {
|
|||
fn apply_can_toggle_admin() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let config_manager = Manager::new(
|
||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::*;
|
|||
use rustfm_scrobble::{Scrobble, Scrobbler};
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use user::AuthToken;
|
||||
|
||||
use crate::app::{index::Index, user};
|
||||
|
||||
|
@ -53,12 +54,19 @@ impl Manager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn link(&self, username: &str, token: &str) -> Result<()> {
|
||||
pub fn generate_link_token(&self, username: &str) -> Result<AuthToken> {
|
||||
self.user_manager
|
||||
.generate_lastfm_link_token(username)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn link(&self, username: &str, lastfm_token: &str) -> Result<()> {
|
||||
let mut scrobbler = Scrobbler::new(LASTFM_API_KEY.into(), LASTFM_API_SECRET.into());
|
||||
let auth_response = scrobbler.authenticate_with_token(token)?;
|
||||
let auth_response = scrobbler.authenticate_with_token(lastfm_token)?;
|
||||
|
||||
self.user_manager
|
||||
.lastfm_link(username, &auth_response.name, &auth_response.key)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn unlink(&self, username: &str) -> Result<()> {
|
||||
|
|
|
@ -6,7 +6,7 @@ mod manager;
|
|||
pub use error::*;
|
||||
pub use manager::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct AuthSecret {
|
||||
pub key: [u8; 32],
|
||||
}
|
||||
|
|
|
@ -6,6 +6,12 @@ pub enum Error {
|
|||
EmptyPassword,
|
||||
#[error("Username does not exist")]
|
||||
IncorrectUsername,
|
||||
#[error("Password does not match username")]
|
||||
IncorrectPassword,
|
||||
#[error("Invalid auth token")]
|
||||
InvalidAuthToken,
|
||||
#[error("Incorrect authorization scope")]
|
||||
IncorrectAuthorizationScope,
|
||||
#[error("Unspecified")]
|
||||
Unspecified,
|
||||
}
|
||||
|
|
|
@ -1,20 +1,24 @@
|
|||
use anyhow::anyhow;
|
||||
use diesel;
|
||||
use diesel::prelude::*;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use super::*;
|
||||
use crate::app::settings::AuthSecret;
|
||||
use crate::db::DB;
|
||||
|
||||
const HASH_ITERATIONS: u32 = 10000;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Manager {
|
||||
// TODO make this private and move preferences methods in this file
|
||||
pub db: DB,
|
||||
auth_secret: AuthSecret,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn new(db: DB) -> Self {
|
||||
Self { db }
|
||||
pub fn new(db: DB, auth_secret: AuthSecret) -> Self {
|
||||
Self { db, auth_secret }
|
||||
}
|
||||
|
||||
pub fn create(&self, new_user: &NewUser) -> Result<(), Error> {
|
||||
|
@ -67,7 +71,7 @@ impl Manager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn login(&self, username: &str, password: &str) -> Result<bool, Error> {
|
||||
pub fn login(&self, username: &str, password: &str) -> Result<AuthToken, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = self.db.connect()?;
|
||||
match users
|
||||
|
@ -78,11 +82,68 @@ impl Manager {
|
|||
Err(diesel::result::Error::NotFound) => Err(Error::IncorrectUsername),
|
||||
Ok(hash) => {
|
||||
let hash: String = hash;
|
||||
Ok(verify_password(&hash, password))
|
||||
if verify_password(&hash, password) {
|
||||
let authorization = Authorization {
|
||||
username: username.to_owned(),
|
||||
scope: AuthorizationScope::PolarisAuth,
|
||||
};
|
||||
self.generate_auth_token(&authorization)
|
||||
} else {
|
||||
Err(Error::IncorrectPassword)
|
||||
}
|
||||
}
|
||||
Err(_) => Err(Error::Unspecified),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authenticate(
|
||||
&self,
|
||||
auth_token: &AuthToken,
|
||||
scope: AuthorizationScope,
|
||||
) -> Result<Authorization, Error> {
|
||||
let authorization = self.decode_auth_token(auth_token, scope)?;
|
||||
if self.exists(&authorization.username)? {
|
||||
Ok(authorization)
|
||||
} else {
|
||||
Err(Error::IncorrectUsername)
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_auth_token(
|
||||
&self,
|
||||
auth_token: &AuthToken,
|
||||
scope: AuthorizationScope,
|
||||
) -> Result<Authorization, Error> {
|
||||
let AuthToken(data) = auth_token;
|
||||
let ttl = match scope {
|
||||
AuthorizationScope::PolarisAuth => 0, // permanent
|
||||
AuthorizationScope::LastFMLink => 10 * 60, // 10 minutes
|
||||
};
|
||||
let authorization = branca::decode(data, &self.auth_secret.key, ttl)
|
||||
.map_err(|_| Error::InvalidAuthToken)?;
|
||||
let authorization: Authorization =
|
||||
serde_json::from_slice(&authorization[..]).map_err(|_| Error::InvalidAuthToken)?;
|
||||
if authorization.scope != scope {
|
||||
return Err(Error::IncorrectAuthorizationScope);
|
||||
}
|
||||
Ok(authorization)
|
||||
}
|
||||
|
||||
fn generate_auth_token(&self, authorization: &Authorization) -> Result<AuthToken, Error> {
|
||||
let serialized_authorization =
|
||||
serde_json::to_string(&authorization).map_err(|_| Error::Unspecified)?;
|
||||
branca::encode(
|
||||
serialized_authorization.as_bytes(),
|
||||
&self.auth_secret.key,
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| Error::Unspecified)?
|
||||
.as_secs() as u32,
|
||||
)
|
||||
.map_err(|_| Error::Unspecified)
|
||||
.map(AuthToken)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> anyhow::Result<i64> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = self.db.connect()?;
|
||||
|
@ -110,13 +171,14 @@ impl Manager {
|
|||
Ok(results.len() > 0)
|
||||
}
|
||||
|
||||
pub fn is_admin(&self, username: &str) -> anyhow::Result<bool> {
|
||||
pub fn is_admin(&self, username: &str) -> Result<bool, Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = self.db.connect()?;
|
||||
let is_admin: i32 = users
|
||||
.filter(name.eq(username))
|
||||
.select(admin)
|
||||
.get_result(&connection)?;
|
||||
.get_result(&connection)
|
||||
.map_err(|_| Error::Unspecified)?;
|
||||
Ok(is_admin != 0)
|
||||
}
|
||||
|
||||
|
@ -125,7 +187,7 @@ impl Manager {
|
|||
username: &str,
|
||||
lastfm_login: &str,
|
||||
session_key: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), Error> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = self.db.connect()?;
|
||||
diesel::update(users.filter(name.eq(username)))
|
||||
|
@ -133,10 +195,18 @@ impl Manager {
|
|||
lastfm_username.eq(lastfm_login),
|
||||
lastfm_session_key.eq(session_key),
|
||||
))
|
||||
.execute(&connection)?;
|
||||
.execute(&connection)
|
||||
.map_err(|_| Error::Unspecified)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_lastfm_link_token(&self, username: &str) -> Result<AuthToken, Error> {
|
||||
self.generate_auth_token(&Authorization {
|
||||
username: username.to_owned(),
|
||||
scope: AuthorizationScope::LastFMLink,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_lastfm_session_key(&self, username: &str) -> anyhow::Result<String> {
|
||||
use crate::db::users::dsl::*;
|
||||
let connection = self.db.connect()?;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::db::users;
|
||||
|
||||
|
@ -32,3 +32,18 @@ pub struct NewUser {
|
|||
pub password: String,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthToken(pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub enum AuthorizationScope {
|
||||
PolarisAuth,
|
||||
LastFMLink,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Authorization {
|
||||
pub username: String,
|
||||
pub scope: AuthorizationScope,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use crate::app::settings;
|
||||
use crate::db::DB;
|
||||
use crate::test_name;
|
||||
|
||||
|
@ -19,7 +20,9 @@ pub fn get_test_db(name: &str) -> DB {
|
|||
#[test]
|
||||
fn create_delete_user_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let user_manager = Manager::new(db);
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
|
@ -37,7 +40,9 @@ fn create_delete_user_golden_path() {
|
|||
#[test]
|
||||
fn cannot_create_user_with_blank_username() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let user_manager = Manager::new(db);
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "".to_owned(),
|
||||
|
@ -54,7 +59,9 @@ fn cannot_create_user_with_blank_username() {
|
|||
#[test]
|
||||
fn cannot_create_user_with_blank_password() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let user_manager = Manager::new(db);
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
|
@ -71,7 +78,9 @@ fn cannot_create_user_with_blank_password() {
|
|||
#[test]
|
||||
fn cannot_create_duplicate_user() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let user_manager = Manager::new(db);
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let new_user = NewUser {
|
||||
name: "Walter".to_owned(),
|
||||
|
@ -86,7 +95,9 @@ fn cannot_create_duplicate_user() {
|
|||
#[test]
|
||||
fn can_read_write_preferences() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let user_manager = Manager::new(db);
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let new_preferences = Preferences {
|
||||
web_theme_base: Some("very-dark-theme".to_owned()),
|
||||
|
@ -108,3 +119,126 @@ fn can_read_write_preferences() {
|
|||
let read_preferences = user_manager.read_preferences("Walter").unwrap();
|
||||
assert_eq!(new_preferences, read_preferences);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_rejects_bad_password() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
assert_eq!(
|
||||
user_manager
|
||||
.login(username, "not the password")
|
||||
.unwrap_err(),
|
||||
Error::IncorrectPassword
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
assert!(user_manager.login(username, password).is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_rejects_bad_token() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
let fake_token = AuthToken("fake token".to_owned());
|
||||
assert!(user_manager
|
||||
.authenticate(&fake_token, AuthorizationScope::PolarisAuth)
|
||||
.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_golden_path() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
let token = user_manager.login(username, password).unwrap();
|
||||
let authorization = user_manager
|
||||
.authenticate(&token, AuthorizationScope::PolarisAuth)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
authorization,
|
||||
Authorization {
|
||||
username: username.to_owned(),
|
||||
scope: AuthorizationScope::PolarisAuth,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_validates_scope() {
|
||||
let db = get_test_db(&test_name!());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = Manager::new(db, auth_secret);
|
||||
|
||||
let username = "Walter";
|
||||
let password = "super_secret!";
|
||||
|
||||
let new_user = NewUser {
|
||||
name: username.to_owned(),
|
||||
password: password.to_owned(),
|
||||
admin: false,
|
||||
};
|
||||
|
||||
user_manager.create(&new_user).unwrap();
|
||||
let token = user_manager.generate_lastfm_link_token(username).unwrap();
|
||||
let authorization = user_manager.authenticate(&token, AuthorizationScope::PolarisAuth);
|
||||
assert_eq!(
|
||||
authorization.unwrap_err(),
|
||||
Error::IncorrectAuthorizationScope
|
||||
)
|
||||
}
|
||||
|
|
|
@ -102,7 +102,8 @@ pub fn get_test_db(name: &str) -> DB {
|
|||
|
||||
let db = DB::new(&db_path).unwrap();
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret().unwrap();
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let config_manager =
|
||||
|
|
|
@ -10,7 +10,7 @@ use actix_web::{
|
|||
web::{self, Data, Json, JsonConfig, ServiceConfig},
|
||||
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
||||
};
|
||||
use actix_web_httpauth::extractors::basic::BasicAuth;
|
||||
use actix_web_httpauth::extractors::{basic::BasicAuth, bearer::BearerAuth};
|
||||
use cookie::{self, *};
|
||||
use futures_util::future::{err, ok};
|
||||
use percent_encoding::percent_decode_str;
|
||||
|
@ -64,6 +64,7 @@ pub fn make_config() -> impl FnOnce(&mut ServiceConfig) + Clone {
|
|||
.service(delete_playlist)
|
||||
.service(lastfm_now_playing)
|
||||
.service(lastfm_scrobble)
|
||||
.service(lastfm_link_token)
|
||||
.service(lastfm_link)
|
||||
.service(lastfm_unlink);
|
||||
}
|
||||
|
@ -151,8 +152,10 @@ impl FromRequest for Cookies {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum AuthSource {
|
||||
AuthorizationHeader,
|
||||
AuthorizationBasic,
|
||||
AuthorizationBearer,
|
||||
Cookie,
|
||||
QueryParameter,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -173,7 +176,10 @@ impl FromRequest for Auth {
|
|||
};
|
||||
|
||||
let cookies_future = Cookies::from_request(request, payload);
|
||||
let http_auth_future = BasicAuth::from_request(request, payload);
|
||||
let basic_auth_future = BasicAuth::from_request(request, payload);
|
||||
let bearer_auth_future = BearerAuth::from_request(request, payload);
|
||||
let query_params_future =
|
||||
web::Query::<dto::AuthQueryParameters>::from_request(request, payload);
|
||||
|
||||
Box::pin(async move {
|
||||
// Auth via session cookie
|
||||
|
@ -192,20 +198,46 @@ impl FromRequest for Auth {
|
|||
}
|
||||
}
|
||||
|
||||
// Auth via HTTP header
|
||||
// Auth via bearer token in query parameter
|
||||
if let Ok(query) = query_params_future.await {
|
||||
let auth_token = user::AuthToken(query.auth_token.clone());
|
||||
let authorization = block(move || {
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
})
|
||||
.await?;
|
||||
return Ok(Auth {
|
||||
username: authorization.username.to_owned(),
|
||||
source: AuthSource::QueryParameter,
|
||||
});
|
||||
}
|
||||
|
||||
// Auth via bearer token in authorization header
|
||||
if let Ok(bearer_auth) = bearer_auth_future.await {
|
||||
let auth_token = user::AuthToken(bearer_auth.token().to_owned());
|
||||
let authorization = block(move || {
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::PolarisAuth)
|
||||
})
|
||||
.await?;
|
||||
return Ok(Auth {
|
||||
username: authorization.username.to_owned(),
|
||||
source: AuthSource::AuthorizationBearer,
|
||||
});
|
||||
}
|
||||
|
||||
// Auth via basic authorization header
|
||||
{
|
||||
let auth = http_auth_future.await?;
|
||||
let username = auth.user_id().to_string();
|
||||
let password = auth
|
||||
let basic_auth = basic_auth_future.await?;
|
||||
let username = basic_auth.user_id().to_string();
|
||||
let password = basic_auth
|
||||
.password()
|
||||
.map(|s| s.as_ref())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let auth_result = block(move || user_manager.login(&username, &password)).await?;
|
||||
if auth_result {
|
||||
let auth_result = block(move || user_manager.login(&username, &password)).await;
|
||||
if auth_result.is_ok() {
|
||||
Ok(Auth {
|
||||
username: auth.user_id().to_string(),
|
||||
source: AuthSource::AuthorizationHeader,
|
||||
username: basic_auth.user_id().to_string(),
|
||||
source: AuthSource::AuthorizationBasic,
|
||||
})
|
||||
} else {
|
||||
Err(ErrorUnauthorized(APIError::Unspecified))
|
||||
|
@ -280,8 +312,10 @@ pub fn http_auth_middleware<
|
|||
let mut response = response_future.await?;
|
||||
if let Ok(auth) = auth_future.await {
|
||||
let set_cookies = match auth.source {
|
||||
AuthSource::AuthorizationHeader => true,
|
||||
AuthSource::AuthorizationBasic => true,
|
||||
AuthSource::AuthorizationBearer => false,
|
||||
AuthSource::Cookie => false,
|
||||
AuthSource::QueryParameter => false,
|
||||
};
|
||||
if set_cookies {
|
||||
let cookies = cookies_future.await?;
|
||||
|
@ -547,20 +581,23 @@ async fn trigger_index(
|
|||
#[post("/auth")]
|
||||
async fn login(
|
||||
user_manager: Data<user::Manager>,
|
||||
credentials: Json<dto::AuthCredentials>,
|
||||
credentials: Json<dto::Credentials>,
|
||||
cookies: Cookies,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let username = credentials.username.clone();
|
||||
let is_admin = block(move || {
|
||||
if !user_manager.login(&credentials.username, &credentials.password)? {
|
||||
return Err(APIError::IncorrectCredentials);
|
||||
}
|
||||
user_manager
|
||||
.is_admin(&credentials.username)
|
||||
.map_err(|_| APIError::Unspecified)
|
||||
})
|
||||
.await?;
|
||||
let mut response = HttpResponse::Ok().finish();
|
||||
let (user::AuthToken(token), is_admin) =
|
||||
block(move || -> Result<(user::AuthToken, bool), APIError> {
|
||||
let auth_token = user_manager.login(&credentials.username, &credentials.password)?;
|
||||
let is_admin = user_manager.is_admin(&credentials.username)?;
|
||||
Ok((auth_token, is_admin))
|
||||
})
|
||||
.await?;
|
||||
let authorization = dto::Authorization {
|
||||
username: username.clone(),
|
||||
token,
|
||||
is_admin,
|
||||
};
|
||||
let mut response = HttpResponse::Ok().json(authorization);
|
||||
add_auth_cookies(&mut response, &cookies, &username, is_admin)
|
||||
.map_err(|_| APIError::Unspecified)?;
|
||||
Ok(response)
|
||||
|
@ -770,14 +807,29 @@ async fn lastfm_scrobble(
|
|||
Ok(HttpResponse::new(StatusCode::OK))
|
||||
}
|
||||
|
||||
#[get("/lastfm/link_token")]
|
||||
async fn lastfm_link_token(
|
||||
lastfm_manager: Data<lastfm::Manager>,
|
||||
auth: Auth,
|
||||
) -> Result<Json<dto::LastFMLinkToken>, APIError> {
|
||||
let user::AuthToken(value) =
|
||||
block(move || lastfm_manager.generate_link_token(&auth.username)).await?;
|
||||
Ok(Json(dto::LastFMLinkToken { value }))
|
||||
}
|
||||
|
||||
#[get("/lastfm/link")]
|
||||
async fn lastfm_link(
|
||||
lastfm_manager: Data<lastfm::Manager>,
|
||||
auth: Auth,
|
||||
user_manager: Data<user::Manager>,
|
||||
payload: web::Query<dto::LastFMLink>,
|
||||
) -> Result<HttpResponse, APIError> {
|
||||
let popup_content_string = block(move || {
|
||||
lastfm_manager.link(&auth.username, &payload.token)?;
|
||||
let auth_token = user::AuthToken(payload.auth_token.clone());
|
||||
let authorization =
|
||||
user_manager.authenticate(&auth_token, user::AuthorizationScope::LastFMLink)?;
|
||||
let lastfm_token = &payload.token;
|
||||
lastfm_manager.link(&authorization.username, lastfm_token)?;
|
||||
|
||||
// Percent decode
|
||||
let base64_content = percent_decode_str(&payload.content).decode_utf8_lossy();
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use actix_web::{
|
||||
client::ClientResponse,
|
||||
middleware::{Compress, Logger},
|
||||
rt::{System, SystemRunner},
|
||||
test,
|
||||
|
@ -7,36 +6,26 @@ use actix_web::{
|
|||
web::Bytes,
|
||||
App,
|
||||
};
|
||||
use cookie::Cookie;
|
||||
use http::{header, response::Builder, Method, Request, Response};
|
||||
use http::{response::Builder, Method, Request, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::service::actix::*;
|
||||
use crate::service::dto;
|
||||
use crate::service::test::TestService;
|
||||
|
||||
pub struct ActixTestService {
|
||||
system_runner: SystemRunner,
|
||||
cookies: HashMap<String, String>,
|
||||
authorization: Option<dto::Authorization>,
|
||||
server: TestServer,
|
||||
}
|
||||
|
||||
pub type ServiceType = ActixTestService;
|
||||
|
||||
impl ActixTestService {
|
||||
fn update_cookies<T>(&mut self, actix_response: &ClientResponse<T>) {
|
||||
let cookies = actix_response.headers().get_all(header::SET_COOKIE);
|
||||
for raw_cookie in cookies {
|
||||
let cookie = Cookie::parse(raw_cookie.to_str().unwrap()).unwrap();
|
||||
self.cookies
|
||||
.insert(cookie.name().to_owned(), cookie.value().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
fn process_internal<T: Serialize + Clone + 'static>(
|
||||
&mut self,
|
||||
request: &Request<T>,
|
||||
|
@ -57,22 +46,14 @@ impl ActixTestService {
|
|||
actix_request = actix_request.set_header(name, value.clone());
|
||||
}
|
||||
|
||||
actix_request = {
|
||||
let cookies_value = self
|
||||
.cookies
|
||||
.iter()
|
||||
.map(|(name, value)| format!("{}={}", name, value))
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
actix_request.set_header(header::COOKIE, cookies_value)
|
||||
};
|
||||
if let Some(ref authorization) = self.authorization {
|
||||
actix_request = actix_request.bearer_auth(&authorization.token);
|
||||
}
|
||||
|
||||
let mut actix_response = self
|
||||
.system_runner
|
||||
.block_on(async move { actix_request.send_json(&body).await.unwrap() });
|
||||
|
||||
self.update_cookies(&actix_response);
|
||||
|
||||
let mut response_builder = Response::builder().status(actix_response.status());
|
||||
let headers = response_builder.headers_mut().unwrap();
|
||||
for (name, value) in actix_response.headers().iter() {
|
||||
|
@ -122,7 +103,7 @@ impl TestService for ActixTestService {
|
|||
});
|
||||
|
||||
ActixTestService {
|
||||
cookies: HashMap::new(),
|
||||
authorization: None,
|
||||
system_runner,
|
||||
server,
|
||||
}
|
||||
|
@ -151,4 +132,8 @@ impl TestService for ActixTestService {
|
|||
let body = serde_json::from_slice(&body.unwrap()).unwrap();
|
||||
response_builder.body(body).unwrap()
|
||||
}
|
||||
|
||||
fn set_authorization(&mut self, authorization: Option<dto::Authorization>) {
|
||||
self.authorization = authorization;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,23 @@ pub struct InitialSetup {
|
|||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AuthCredentials {
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Authorization {
|
||||
pub username: String,
|
||||
pub token: String,
|
||||
pub is_admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct AuthQueryParameters {
|
||||
pub auth_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ThumbnailOptions {
|
||||
pub pad: Option<bool>,
|
||||
|
@ -42,8 +54,14 @@ pub struct SavePlaylistInput {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLink {
|
||||
pub token: String,
|
||||
pub content: String,
|
||||
pub auth_token: String, // user::AuthToken emitted by Polaris, valid for LastFMLink scope
|
||||
pub token: String, // LastFM token for use in scrobble calls
|
||||
pub content: String, // Payload to send back to client after successful link
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct LastFMLinkToken {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
|
|
@ -87,6 +87,9 @@ impl From<user::Error> for APIError {
|
|||
user::Error::EmptyUsername => APIError::EmptyUsername,
|
||||
user::Error::EmptyPassword => APIError::EmptyPassword,
|
||||
user::Error::IncorrectUsername => APIError::IncorrectCredentials,
|
||||
user::Error::IncorrectPassword => APIError::IncorrectCredentials,
|
||||
user::Error::InvalidAuthToken => APIError::IncorrectCredentials,
|
||||
user::Error::IncorrectAuthorizationScope => APIError::IncorrectCredentials,
|
||||
user::Error::Unspecified => APIError::Unspecified,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,8 +84,9 @@ impl ContextBuilder {
|
|||
|
||||
let vfs_manager = vfs::Manager::new(db.clone());
|
||||
let settings_manager = settings::Manager::new(db.clone());
|
||||
let auth_secret = settings_manager.get_auth_secret()?;
|
||||
let ddns_manager = ddns::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone());
|
||||
let user_manager = user::Manager::new(db.clone(), auth_secret);
|
||||
let index = Index::new(db.clone(), vfs_manager.clone(), settings_manager.clone());
|
||||
let config_manager = config::Manager::new(
|
||||
settings_manager.clone(),
|
||||
|
|
|
@ -77,9 +77,14 @@ fn test_login_golden_path() {
|
|||
service.complete_initial_setup();
|
||||
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch(&request);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let authorization = response.body();
|
||||
assert_eq!(authorization.username, TEST_USERNAME);
|
||||
assert_eq!(authorization.is_admin, false);
|
||||
assert!(!authorization.token.is_empty());
|
||||
|
||||
validate_added_cookies(&response);
|
||||
}
|
||||
|
||||
|
@ -97,7 +102,7 @@ fn test_requests_without_auth_header_do_not_set_cookies() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_http_header_rejects_bad_username() {
|
||||
fn test_authentication_via_basic_http_header_rejects_bad_username() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -110,7 +115,7 @@ fn test_authentication_via_http_header_rejects_bad_username() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_http_header_rejects_bad_password() {
|
||||
fn test_authentication_via_basic_http_header_rejects_bad_password() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -123,7 +128,7 @@ fn test_authentication_via_http_header_rejects_bad_password() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_http_header_golden_path() {
|
||||
fn test_authentication_via_basic_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
|
@ -136,3 +141,78 @@ fn test_authentication_via_http_header_golden_path() {
|
|||
|
||||
validate_added_cookies(&response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_bearer_http_header_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
let mut request = protocol::random();
|
||||
let bearer = headers::Authorization::bearer("garbage").unwrap();
|
||||
request.headers_mut().typed_insert(bearer);
|
||||
|
||||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_bearer_http_header_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
let authorization = {
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
response.into_body()
|
||||
};
|
||||
|
||||
service.logout();
|
||||
|
||||
let mut request = protocol::random();
|
||||
let bearer = headers::Authorization::bearer(&authorization.token).unwrap();
|
||||
request.headers_mut().typed_insert(bearer);
|
||||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
validate_no_cookies(&response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_query_param_rejects_bad_token() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
let mut request = protocol::random();
|
||||
*request.uri_mut() = (request.uri().to_string() + "?auth_token=garbage-token")
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authentication_via_query_param_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
|
||||
let authorization = {
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = service.fetch_json::<_, dto::Authorization>(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
response.into_body()
|
||||
};
|
||||
|
||||
service.logout();
|
||||
|
||||
let mut request = protocol::random();
|
||||
*request.uri_mut() = format!("{}?auth_token={}", request.uri(), authorization.token)
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
validate_no_cookies(&response);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use http::StatusCode;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::service::dto;
|
||||
use crate::service::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
||||
|
@ -37,3 +38,25 @@ fn test_lastfm_now_playing_ignores_unlinked_user() {
|
|||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lastfm_link_token_requires_auth() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
let request = protocol::lastfm_link_token();
|
||||
let response = service.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lastfm_link_token_golden_path() {
|
||||
let mut service = ServiceType::new(&test_name!());
|
||||
service.complete_initial_setup();
|
||||
service.login();
|
||||
|
||||
let request = protocol::lastfm_link_token();
|
||||
let response = service.fetch_json::<_, dto::LastFMLinkToken>(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let link_token = response.body();
|
||||
assert!(!link_token.value.is_empty());
|
||||
}
|
||||
|
||||
|
|
|
@ -62,18 +62,28 @@ pub trait TestService {
|
|||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
fn login_admin(&mut self) {
|
||||
let request = protocol::login(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
|
||||
let response = self.fetch(&request);
|
||||
fn login_internal(&mut self, username: &str, password: &str) {
|
||||
let request = protocol::login(username, password);
|
||||
let response = self.fetch_json::<_, dto::Authorization>(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let authorization = response.into_body();
|
||||
self.set_authorization(Some(authorization));
|
||||
}
|
||||
|
||||
fn login_admin(&mut self) {
|
||||
self.login_internal(TEST_USERNAME_ADMIN, TEST_PASSWORD_ADMIN);
|
||||
}
|
||||
|
||||
fn login(&mut self) {
|
||||
let request = protocol::login(TEST_USERNAME, TEST_PASSWORD);
|
||||
let response = self.fetch(&request);
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
self.login_internal(TEST_USERNAME, TEST_PASSWORD);
|
||||
}
|
||||
|
||||
fn logout(&mut self) {
|
||||
self.set_authorization(None);
|
||||
}
|
||||
|
||||
fn set_authorization(&mut self, authorization: Option<dto::Authorization>);
|
||||
|
||||
fn index(&mut self) {
|
||||
let request = protocol::trigger_index();
|
||||
let response = self.fetch(&request);
|
||||
|
|
|
@ -37,8 +37,8 @@ pub fn initial_setup() -> Request<()> {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn login(username: &str, password: &str) -> Request<dto::AuthCredentials> {
|
||||
let credentials = dto::AuthCredentials {
|
||||
pub fn login(username: &str, password: &str) -> Request<dto::Credentials> {
|
||||
let credentials = dto::Credentials {
|
||||
username: username.into(),
|
||||
password: password.into(),
|
||||
};
|
||||
|
@ -253,6 +253,14 @@ pub fn delete_playlist(name: &str) -> Request<()> {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn lastfm_link_token() -> Request<()> {
|
||||
Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri("/api/lastfm/link_token")
|
||||
.body(())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn lastfm_now_playing(path: &Path) -> Request<()> {
|
||||
let path = path.to_string_lossy();
|
||||
let endpoint = format!("/api/lastfm/now_playing/{}", url_encode(path.as_ref()));
|
||||
|
|
Loading…
Add table
Reference in a new issue