From 280cc60be9933c8b27b7c88d2c1908f9ff8fed3f Mon Sep 17 00:00:00 2001 From: Daniel Sockwell Date: Thu, 4 Jul 2019 09:33:50 -0400 Subject: [PATCH 1/3] Add hard-coded "sec-websocket-protocol" response header --- Cargo.lock | 2 ++ src/main.rs | 13 ++++++++++--- src/timeline.rs | 18 +++++++++--------- src/user.rs | 42 +++++++++++++++++++++++++++++++++++------- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd041a9..767381e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,3 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. [[package]] name = "aho-corasick" version = "0.7.3" diff --git a/src/main.rs b/src/main.rs index 44f00dd..900ea39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use receiver::Receiver; use std::env; use std::net::SocketAddr; use stream::StreamManager; -use user::{Scope, User}; +use user::{Method, Scope, User}; use warp::path; use warp::Filter as WarpFilter; @@ -96,7 +96,7 @@ fn main() { //let redis_updates_ws = StreamManager::new(Receiver::new()); let websocket = path!("api" / "v1" / "streaming") - .and(Scope::Public.get_access_token()) + .and(Scope::Public.get_access_token(Method::WS)) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .and(query::Media::to_filter()) @@ -140,7 +140,14 @@ fn main() { Ok(ws.on_upgrade(move |socket| ws::send_replies(socket, stream))) }, - ); + ) + .map(|reply| { + warp::reply::with_header( + reply, + "sec-websocket-protocol", + "LhbVOxKckgqyMg3nDLaEu5vgqY6Yzc9Pk1w8_yKQwS8", + ) + }); let address: SocketAddr = env::var("SERVER_ADDR") .unwrap_or("127.0.0.1:4000".to_owned()) diff --git a/src/timeline.rs b/src/timeline.rs index 541ac2b..4cdf012 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -1,6 +1,6 @@ //! Filters for all the endpoints accessible for Server Sent Event updates use crate::query; -use crate::user::{Scope, User}; +use crate::user::{Method, Scope, User}; use warp::filters::BoxedFilter; use warp::{path, Filter}; @@ -14,7 +14,7 @@ type TimelineUser = ((String, User),); pub fn user() -> BoxedFilter { path!("api" / "v1" / "streaming" / "user") .and(path::end()) - .and(Scope::Private.get_access_token()) + .and(Scope::Private.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (user.id.to_string(), user)) .boxed() @@ -30,7 +30,7 @@ pub fn user() -> BoxedFilter { pub fn user_notifications() -> BoxedFilter { path!("api" / "v1" / "streaming" / "user" / "notification") .and(path::end()) - .and(Scope::Private.get_access_token()) + .and(Scope::Private.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (user.id.to_string(), user.with_notification_filter())) .boxed() @@ -43,7 +43,7 @@ pub fn user_notifications() -> BoxedFilter { pub fn public() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public") .and(path::end()) - .and(Scope::Public.get_access_token()) + .and(Scope::Public.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Public)) .map(|user: User| ("public".to_owned(), user.with_language_filter())) .boxed() @@ -56,7 +56,7 @@ pub fn public() -> BoxedFilter { pub fn public_media() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public") .and(path::end()) - .and(Scope::Public.get_access_token()) + .and(Scope::Public.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .map(|user: User, q: query::Media| match q.only_media.as_ref() { @@ -73,7 +73,7 @@ pub fn public_media() -> BoxedFilter { pub fn public_local() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public" / "local") .and(path::end()) - .and(Scope::Public.get_access_token()) + .and(Scope::Public.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Public)) .map(|user: User| ("public:local".to_owned(), user.with_language_filter())) .boxed() @@ -85,7 +85,7 @@ pub fn public_local() -> BoxedFilter { /// **public**. Filter: `Language` pub fn public_local_media() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public" / "local") - .and(Scope::Public.get_access_token()) + .and(Scope::Public.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .and(path::end()) @@ -103,7 +103,7 @@ pub fn public_local_media() -> BoxedFilter { pub fn direct() -> BoxedFilter { path!("api" / "v1" / "streaming" / "direct") .and(path::end()) - .and(Scope::Private.get_access_token()) + .and(Scope::Private.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (format!("direct:{}", user.id), user.with_no_filter())) .boxed() @@ -139,7 +139,7 @@ pub fn hashtag_local() -> BoxedFilter { /// **private**. Filter: `None` pub fn list() -> BoxedFilter { path!("api" / "v1" / "streaming" / "list") - .and(Scope::Private.get_access_token()) + .and(Scope::Private.get_access_token(Method::HttpPush)) .and_then(|token| User::from_access_token(token, Scope::Private)) .and(warp::query()) .and_then(|user: User, q: query::List| (user.authorized_for_list(q.list), Ok(user))) diff --git a/src/user.rs b/src/user.rs index 5a5cbb2..3923aa2 100644 --- a/src/user.rs +++ b/src/user.rs @@ -27,6 +27,7 @@ pub enum Filter { #[derive(Clone, Debug, PartialEq)] pub struct User { pub id: i64, + pub access_token: String, pub langs: Option>, pub logged_in: bool, pub filter: Filter, @@ -49,6 +50,7 @@ LIMIT 1", &[&token], ) .expect("Hard-coded query will return Some([0 or more rows])"); + dbg!(&result); if !result.is_empty() { let only_row = result.get(0); let id: i64 = only_row.get(1); @@ -56,6 +58,7 @@ LIMIT 1", info!("Granting logged-in access"); Ok(User { id, + access_token: token, langs, logged_in: true, filter: Filter::None, @@ -64,6 +67,7 @@ LIMIT 1", info!("Granting public access to non-authenticated client"); Ok(User { id: -1, + access_token: token, langs: None, logged_in: false, filter: Filter::None, @@ -116,6 +120,7 @@ LIMIT 1", pub fn public() -> Self { User { id: -1, + access_token: String::new(), langs: None, logged_in: false, filter: Filter::None, @@ -128,18 +133,41 @@ pub enum Scope { Public, Private, } +pub enum Method { + WS, + HttpPush, +} impl Scope { - pub fn get_access_token(self) -> warp::filters::BoxedFilter<(String,)> { - let token_from_header = warp::header::header::("authorization") - .map(|auth: String| auth.split(' ').nth(1).unwrap_or("invalid").to_string()); - let token_from_query = warp::query().map(|q: query::Auth| q.access_token); + pub fn get_access_token(self, method: Method) -> warp::filters::BoxedFilter<(String,)> { + let token_from_header_http_push = + warp::header::header::("authorization").map(|auth: String| { + dbg!(auth.split(' ').nth(1).unwrap_or("invalid").to_string()); + auth.split(' ').nth(1).unwrap_or("invalid").to_string() + }); + let token_from_header_ws = + warp::header::header::("Sec-WebSocket-Protocol").map(|auth: String| { + dbg!(&auth); + auth + }); + let token_from_query = warp::query().map(|q: query::Auth| { + dbg!(&q.access_token); + q.access_token + }); let public = warp::any().map(|| "no access token".to_string()); - match self { + match (self, method) { // if they're trying to access a private scope without an access token, reject the request - Scope::Private => any_of!(token_from_query, token_from_header).boxed(), + (Scope::Private, Method::HttpPush) => { + any_of!(token_from_query, token_from_header_http_push).boxed() + } + (Scope::Private, Method::WS) => any_of!(token_from_query, token_from_header_ws).boxed(), // if they're trying to access a public scope without an access token, proceed - Scope::Public => any_of!(token_from_query, token_from_header, public).boxed(), + (Scope::Public, Method::HttpPush) => { + any_of!(token_from_query, token_from_header_http_push, public).boxed() + } + (Scope::Public, Method::WS) => { + any_of!(token_from_query, token_from_header_ws, public).boxed() + } } } } From f8a82caa2dddec5518e7afb76f560e4faf6da94e Mon Sep 17 00:00:00 2001 From: Daniel Sockwell Date: Thu, 4 Jul 2019 10:57:15 -0400 Subject: [PATCH 2/3] Support passing access tokens via Sec-WebSocket-Protocol header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the access token needed to be passed via the query string; with this commit, the token can be passed *either* through the query string or the Sec-WebSocket-Protocol header. This was done to correspond to the changes made to the streaming.js version in [Improve streaming server security](https://github.com/tootsuite/mastodon/pull/10818). However, I am not sure that it *does* increase security; as explained at , there is generally no security advantage to passing sensitive information via websocket headers instead of the query string—the entire connection is encrypted and is not stored in the browser history, so the typical reasons to keep sensitive info out of the query string don't apply. I would welcome any corrections on this/reasons this change improves security. --- src/main.rs | 18 ++++++++---------- src/stream.rs | 25 ++++++++++++++----------- src/timeline.rs | 18 +++++++++--------- src/user.rs | 45 +++++++++++++++------------------------------ 4 files changed, 46 insertions(+), 60 deletions(-) diff --git a/src/main.rs b/src/main.rs index 900ea39..98eb190 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use receiver::Receiver; use std::env; use std::net::SocketAddr; use stream::StreamManager; -use user::{Method, Scope, User}; +use user::{Scope, User}; use warp::path; use warp::Filter as WarpFilter; @@ -96,7 +96,7 @@ fn main() { //let redis_updates_ws = StreamManager::new(Receiver::new()); let websocket = path!("api" / "v1" / "streaming") - .and(Scope::Public.get_access_token(Method::WS)) + .and(Scope::Public.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .and(query::Media::to_filter()) @@ -136,18 +136,16 @@ fn main() { // Other endpoints don't exist: _ => return Err(warp::reject::custom("Error: Nonexistent WebSocket query")), }; + let token = user.access_token.clone(); let stream = redis_updates_ws.configure_copy(&timeline, user); - Ok(ws.on_upgrade(move |socket| ws::send_replies(socket, stream))) + Ok(( + ws.on_upgrade(move |socket| ws::send_replies(socket, stream)), + token, + )) }, ) - .map(|reply| { - warp::reply::with_header( - reply, - "sec-websocket-protocol", - "LhbVOxKckgqyMg3nDLaEu5vgqY6Yzc9Pk1w8_yKQwS8", - ) - }); + .map(|(reply, token)| warp::reply::with_header(reply, "sec-websocket-protocol", token)); let address: SocketAddr = env::var("SERVER_ADDR") .unwrap_or("127.0.0.1:4000".to_owned()) diff --git a/src/stream.rs b/src/stream.rs index 069328b..928f97d 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -51,7 +51,10 @@ impl Stream for StreamManager { type Error = Error; fn poll(&mut self) -> Poll, Self::Error> { - let mut receiver = self.receiver.lock().expect("No other thread panic"); + let mut receiver = self + .receiver + .lock() + .expect("StreamManager: No other thread panic"); receiver.update(self.id, &self.target_timeline.clone()); match receiver.poll() { Ok(Async::Ready(Some(value))) => { @@ -61,19 +64,19 @@ impl Stream for StreamManager { .expect("Previously set current user"); let user_langs = user.langs.clone(); - let copy = value.clone(); - let event = copy["event"].as_str().expect("Redis string"); - let copy = value.clone(); - let payload = copy["payload"].to_string(); - let copy = value.clone(); - let toot_lang = copy["payload"]["language"] - .as_str() - .expect("redis str") - .to_string(); + let event = value["event"].as_str().expect("Redis string"); + let payload = value["payload"].to_string(); match (&user.filter, user_langs) { (Filter::Notification, _) if event != "notification" => Ok(Async::NotReady), - (Filter::Language, Some(ref langs)) if !langs.contains(&toot_lang) => { + (Filter::Language, Some(ref user_langs)) + if !user_langs.contains( + &value["payload"]["language"] + .as_str() + .expect("Redis str") + .to_string(), + ) => + { Ok(Async::NotReady) } _ => Ok(Async::Ready(Some(json!( diff --git a/src/timeline.rs b/src/timeline.rs index 4cdf012..541ac2b 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -1,6 +1,6 @@ //! Filters for all the endpoints accessible for Server Sent Event updates use crate::query; -use crate::user::{Method, Scope, User}; +use crate::user::{Scope, User}; use warp::filters::BoxedFilter; use warp::{path, Filter}; @@ -14,7 +14,7 @@ type TimelineUser = ((String, User),); pub fn user() -> BoxedFilter { path!("api" / "v1" / "streaming" / "user") .and(path::end()) - .and(Scope::Private.get_access_token(Method::HttpPush)) + .and(Scope::Private.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (user.id.to_string(), user)) .boxed() @@ -30,7 +30,7 @@ pub fn user() -> BoxedFilter { pub fn user_notifications() -> BoxedFilter { path!("api" / "v1" / "streaming" / "user" / "notification") .and(path::end()) - .and(Scope::Private.get_access_token(Method::HttpPush)) + .and(Scope::Private.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (user.id.to_string(), user.with_notification_filter())) .boxed() @@ -43,7 +43,7 @@ pub fn user_notifications() -> BoxedFilter { pub fn public() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public") .and(path::end()) - .and(Scope::Public.get_access_token(Method::HttpPush)) + .and(Scope::Public.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Public)) .map(|user: User| ("public".to_owned(), user.with_language_filter())) .boxed() @@ -56,7 +56,7 @@ pub fn public() -> BoxedFilter { pub fn public_media() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public") .and(path::end()) - .and(Scope::Public.get_access_token(Method::HttpPush)) + .and(Scope::Public.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .map(|user: User, q: query::Media| match q.only_media.as_ref() { @@ -73,7 +73,7 @@ pub fn public_media() -> BoxedFilter { pub fn public_local() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public" / "local") .and(path::end()) - .and(Scope::Public.get_access_token(Method::HttpPush)) + .and(Scope::Public.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Public)) .map(|user: User| ("public:local".to_owned(), user.with_language_filter())) .boxed() @@ -85,7 +85,7 @@ pub fn public_local() -> BoxedFilter { /// **public**. Filter: `Language` pub fn public_local_media() -> BoxedFilter { path!("api" / "v1" / "streaming" / "public" / "local") - .and(Scope::Public.get_access_token(Method::HttpPush)) + .and(Scope::Public.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Public)) .and(warp::query()) .and(path::end()) @@ -103,7 +103,7 @@ pub fn public_local_media() -> BoxedFilter { pub fn direct() -> BoxedFilter { path!("api" / "v1" / "streaming" / "direct") .and(path::end()) - .and(Scope::Private.get_access_token(Method::HttpPush)) + .and(Scope::Private.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Private)) .map(|user: User| (format!("direct:{}", user.id), user.with_no_filter())) .boxed() @@ -139,7 +139,7 @@ pub fn hashtag_local() -> BoxedFilter { /// **private**. Filter: `None` pub fn list() -> BoxedFilter { path!("api" / "v1" / "streaming" / "list") - .and(Scope::Private.get_access_token(Method::HttpPush)) + .and(Scope::Private.get_access_token()) .and_then(|token| User::from_access_token(token, Scope::Private)) .and(warp::query()) .and_then(|user: User, q: query::List| (user.authorized_for_list(q.list), Ok(user))) diff --git a/src/user.rs b/src/user.rs index 3923aa2..82e3fd1 100644 --- a/src/user.rs +++ b/src/user.rs @@ -50,7 +50,6 @@ LIMIT 1", &[&token], ) .expect("Hard-coded query will return Some([0 or more rows])"); - dbg!(&result); if !result.is_empty() { let only_row = result.get(0); let id: i64 = only_row.get(1); @@ -133,41 +132,27 @@ pub enum Scope { Public, Private, } -pub enum Method { - WS, - HttpPush, -} impl Scope { - pub fn get_access_token(self, method: Method) -> warp::filters::BoxedFilter<(String,)> { - let token_from_header_http_push = - warp::header::header::("authorization").map(|auth: String| { - dbg!(auth.split(' ').nth(1).unwrap_or("invalid").to_string()); - auth.split(' ').nth(1).unwrap_or("invalid").to_string() - }); + pub fn get_access_token(self) -> warp::filters::BoxedFilter<(String,)> { + let token_from_header_http_push = warp::header::header::("authorization") + .map(|auth: String| auth.split(' ').nth(1).unwrap_or("invalid").to_string()); let token_from_header_ws = - warp::header::header::("Sec-WebSocket-Protocol").map(|auth: String| { - dbg!(&auth); - auth - }); - let token_from_query = warp::query().map(|q: query::Auth| { - dbg!(&q.access_token); - q.access_token - }); + warp::header::header::("Sec-WebSocket-Protocol").map(|auth: String| auth); + let token_from_query = warp::query().map(|q: query::Auth| q.access_token); + + let private_scopes = any_of!( + token_from_header_http_push, + token_from_header_ws, + token_from_query + ); + let public = warp::any().map(|| "no access token".to_string()); - match (self, method) { + match self { // if they're trying to access a private scope without an access token, reject the request - (Scope::Private, Method::HttpPush) => { - any_of!(token_from_query, token_from_header_http_push).boxed() - } - (Scope::Private, Method::WS) => any_of!(token_from_query, token_from_header_ws).boxed(), + Scope::Private => private_scopes.boxed(), // if they're trying to access a public scope without an access token, proceed - (Scope::Public, Method::HttpPush) => { - any_of!(token_from_query, token_from_header_http_push, public).boxed() - } - (Scope::Public, Method::WS) => { - any_of!(token_from_query, token_from_header_ws, public).boxed() - } + Scope::Public => any_of!(private_scopes, public).boxed(), } } } From 1765dc39eee078144e3774963558d6cd7baf6d62 Mon Sep 17 00:00:00 2001 From: Daniel Sockwell Date: Thu, 4 Jul 2019 13:27:11 -0400 Subject: [PATCH 3/3] Check oauth scopes and reject unauthorized requests --- src/main.rs | 46 +++++++++++++++++++++++++++++++++------------- src/user.rs | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 98eb190..2fe5d89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,7 +40,7 @@ use receiver::Receiver; use std::env; use std::net::SocketAddr; use stream::StreamManager; -use user::{Scope, User}; +use user::{OauthScope::*, Scope, User}; use warp::path; use warp::Filter as WarpFilter; @@ -110,29 +110,49 @@ fn main() { h: query::Hashtag, l: query::List, ws: warp::ws::Ws2| { - let unauthorized = Err(warp::reject::custom("Error: Invalid Access Token")); + let scopes = user.scopes.clone(); let timeline = match q.stream.as_ref() { // Public endpoints: tl @ "public" | tl @ "public:local" if m.is_truthy() => format!("{}:media", tl), tl @ "public:media" | tl @ "public:local:media" => tl.to_string(), tl @ "public" | tl @ "public:local" => tl.to_string(), - // User - "user" if user.id == -1 => return unauthorized, - "user" => format!("{}", user.id), - "user:notification" => { - user = user.with_notification_filter(); - format!("{}", user.id) - } // Hashtag endpoints: // TODO: handle missing query tl @ "hashtag" | tl @ "hashtag:local" => format!("{}:{}", tl, h.tag), + // Private endpoints: User + "user" + if user.id > 0 + && (scopes.contains(&Read) || scopes.contains(&ReadStatuses)) => + { + format!("{}", user.id) + } + "user:notification" + if user.id > 0 + && (scopes.contains(&Read) || scopes.contains(&ReadNotifications)) => + { + user = user.with_notification_filter(); + format!("{}", user.id) + } // List endpoint: // TODO: handle missing query - "list" if user.authorized_for_list(l.list).is_err() => return unauthorized, - "list" => format!("list:{}", l.list), + "list" + if user.authorized_for_list(l.list).is_ok() + && (scopes.contains(&Read) || scopes.contains(&ReadList)) => + { + format!("list:{}", l.list) + } + // Direct endpoint: - "direct" if user.id == -1 => return unauthorized, - "direct" => "direct".to_string(), + "direct" + if user.id > 0 + && (scopes.contains(&Read) || scopes.contains(&ReadStatuses)) => + { + "direct".to_string() + } + // Reject unathorized access attempts for private endpoints + "user" | "user:notification" | "direct" | "list" => { + return Err(warp::reject::custom("Error: Invalid Access Token")) + } // Other endpoints don't exist: _ => return Err(warp::reject::custom("Error: Nonexistent WebSocket query")), }; diff --git a/src/user.rs b/src/user.rs index 82e3fd1..11c2a66 100644 --- a/src/user.rs +++ b/src/user.rs @@ -28,18 +28,42 @@ pub enum Filter { pub struct User { pub id: i64, pub access_token: String, + pub scopes: Vec, pub langs: Option>, pub logged_in: bool, pub filter: Filter, } +#[derive(Clone, Debug, PartialEq)] +pub enum OauthScope { + Read, + ReadStatuses, + ReadNotifications, + ReadList, + Other, +} +impl From<&str> for OauthScope { + fn from(scope: &str) -> Self { + use OauthScope::*; + match scope { + "read" => Read, + "read:statuses" => ReadStatuses, + "read:notifications" => ReadNotifications, + "read:lists" => ReadList, + _ => Other, + } + } +} impl User { /// Create a user from the access token supplied in the header or query paramaters - pub fn from_access_token(token: String, scope: Scope) -> Result { + pub fn from_access_token( + access_token: String, + scope: Scope, + ) -> Result { let conn = connect_to_postgres(); let result = &conn .query( " -SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages +SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON @@ -47,17 +71,25 @@ oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1", - &[&token], + &[&access_token], ) .expect("Hard-coded query will return Some([0 or more rows])"); if !result.is_empty() { let only_row = result.get(0); let id: i64 = only_row.get(1); + let scopes = only_row + .get::<_, String>(3) + .split(' ') + .map(|scope: &str| scope.into()) + .filter(|scope| scope != &OauthScope::Other) + .collect(); + dbg!(&scopes); let langs: Option> = only_row.get(2); info!("Granting logged-in access"); Ok(User { id, - access_token: token, + access_token, + scopes, langs, logged_in: true, filter: Filter::None, @@ -66,7 +98,8 @@ LIMIT 1", info!("Granting public access to non-authenticated client"); Ok(User { id: -1, - access_token: token, + access_token, + scopes: Vec::new(), langs: None, logged_in: false, filter: Filter::None, @@ -120,6 +153,7 @@ LIMIT 1", User { id: -1, access_token: String::new(), + scopes: Vec::new(), langs: None, logged_in: false, filter: Filter::None,