Skip to content

Commit 96cc669

Browse files
committed
add upload keys CRUD and granular authorization
- CRUD endpoints: GET/POST/DELETE /uploadkeys (admin only) - Upload keys denied from generic check_library_role (safe by default) - Media: upload allowed, listing restricted to own uploads (uploadkey filter) - Tags: read/create gated behind key.tags flag - People: read/create gated behind key.people flag update restricted to add_alts and add_socials only - Validate key expiry at auth time - Set uploadkey field on media during upload - Add people column to uploadkeys table (migration 010) - Fix library visibility bug (key.id → key.library) - Fix uploaderkey → uploadkey column name in media update - Map ShareTokenInsufficient to 403 (was falling to 500)
1 parent 3a62a6b commit 96cc669

File tree

13 files changed

+190
-22
lines changed

13 files changed

+190
-22
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ async fn app() -> Result<Router> {
271271
.nest("/library", routes::libraries::routes(mc.clone())) // duplicate for legacy
272272
.nest("/users", routes::users::routes(mc.clone()))
273273
.nest("/credentials", routes::credentials::routes(mc.clone()))
274+
.nest("/uploadkeys", routes::upload_keys::routes(mc.clone()))
274275
.nest("/backups", routes::backups::routes(mc.clone()))
275276
.nest("/plugins", routes::plugins::routes(mc.clone()))
276277
.nest("/sse", routes::sse::routes(mc.clone()))

src/model/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ impl Error {
204204
requested_user: _,
205205
} => (StatusCode::FORBIDDEN, ClientError::FORBIDDEN),
206206
Error::UserListNotAuth { user: _ } => (StatusCode::FORBIDDEN, ClientError::FORBIDDEN),
207+
Error::ShareTokenInsufficient => (StatusCode::FORBIDDEN, ClientError::FORBIDDEN),
207208
Error::UserUpdateNotAuthorized {
208209
user: _,
209210
update_user: _,

src/model/libraries.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ pub(super) fn map_library_for_user(
206206
ConnectedUser::UploadKey(key) => {
207207
let mut library_out =
208208
ServerLibraryForRead::into_with_role(library, &vec![LibraryRole::Write]);
209-
if library_out.id == key.id {
209+
if library_out.id == key.library {
210210
library_out.root = None;
211211
library_out.settings = None;
212212
Some(library_out)

src/model/medias.rs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use crate::{
6969
use rs_plugin_common_interfaces::domain::ItemWithRelations;
7070
use crate::{
7171
domain::{
72-
library::LibraryRole,
72+
library::{LibraryLimits, LibraryRole},
7373
media::{
7474
FileType, Media, MediaForAdd, MediaForInsert, MediaForUpdate, MediaItemReference,
7575
MediaWithAction, MediasMessage, UploadProgressMessage,
@@ -162,6 +162,8 @@ pub struct MediaQuery {
162162

163163
pub vcodec: Option<String>,
164164

165+
pub uploadkey: Option<String>,
166+
165167
pub page_key: Option<String>,
166168

167169
/// For legacy if user put serialized query in filter field
@@ -451,26 +453,41 @@ impl ModelController {
451453
pub async fn get_medias(
452454
&self,
453455
library_id: &str,
454-
query: MediaQuery,
456+
mut query: MediaQuery,
455457
requesting_user: &ConnectedUser,
456458
) -> RsResult<Vec<ItemWithRelations<Media>>> {
459+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
460+
// UploadKey: can only see medias uploaded with this key
461+
query.uploadkey = Some(key.id.clone());
462+
let limits = LibraryLimits::default();
463+
let store = self.store.get_library_store(library_id)?;
464+
let medias = store.get_medias(query, limits).await?;
465+
return Ok(medias);
466+
}
457467
let progress_user = self
458468
.get_library_mapped_user(library_id, requesting_user.user_id()?)
459469
.await
460470
.ok();
461471
let mut limits = requesting_user.check_library_role(library_id, LibraryRole::Read)?;
462472
limits.user_id = progress_user;
463473
let store = self.store.get_library_store(library_id)?;
464-
let people = store.get_medias(query, limits).await?;
465-
Ok(people)
474+
let medias = store.get_medias(query, limits).await?;
475+
Ok(medias)
466476
}
467477

468478
pub async fn count_medias(
469479
&self,
470480
library_id: &str,
471-
query: MediaQuery,
481+
mut query: MediaQuery,
472482
requesting_user: &ConnectedUser,
473483
) -> RsResult<u64> {
484+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
485+
query.uploadkey = Some(key.id.clone());
486+
let limits = LibraryLimits::default();
487+
let store = self.store.get_library_store(library_id)?;
488+
let count = store.count_medias(query, limits).await?;
489+
return Ok(count);
490+
}
474491
let limits = requesting_user.check_library_role(library_id, LibraryRole::Read)?;
475492
let store = self.store.get_library_store(library_id)?;
476493
let count = store.count_medias(query, limits).await?;
@@ -1308,7 +1325,11 @@ impl ModelController {
13081325
reader: T,
13091326
requesting_user: &ConnectedUser,
13101327
) -> RsResult<Media> {
1311-
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
1328+
if requesting_user.check_upload_key(library_id).is_ok() {
1329+
// UploadKey: allowed to write medias
1330+
} else {
1331+
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
1332+
}
13121333
let store = self.store.get_library_store(library_id)?;
13131334
let mut infos = infos.unwrap_or_default();
13141335
let upload_id = infos.upload_id.clone().unwrap_or_else(|| nanoid!());
@@ -1371,6 +1392,11 @@ impl ModelController {
13711392
}
13721393
}
13731394

1395+
let upload_key_id = if let Ok(key) = requesting_user.check_upload_key(library_id) {
1396+
Some(key.id.clone())
1397+
} else {
1398+
None
1399+
};
13741400
let mut new_file = MediaForAdd {
13751401
name: filename.to_string(),
13761402
source: Some(source.to_string()),
@@ -1383,6 +1409,7 @@ impl ModelController {
13831409
iv: infos.iv.clone(),
13841410
thumbsize: infos.thumbsize,
13851411
uploader: requesting_user.user_id().ok(),
1412+
uploadkey: upload_key_id,
13861413
..Default::default()
13871414
};
13881415

src/model/people.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,11 @@ impl ModelController {
211211
query: PeopleQuery,
212212
requesting_user: &ConnectedUser,
213213
) -> Result<Vec<Person>> {
214-
requesting_user.check_library_role(library_id, LibraryRole::Read)?;
214+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
215+
if !key.people { return Err(Error::ShareTokenInsufficient); }
216+
} else {
217+
requesting_user.check_library_role(library_id, LibraryRole::Read)?;
218+
}
215219
let store = self.store.get_library_store_optional(library_id).ok_or(
216220
Error::LibraryStoreNotFoundFor(library_id.to_string(), "get_people".to_string()),
217221
)?;
@@ -278,7 +282,18 @@ impl ModelController {
278282
mut update: PersonForUpdate,
279283
requesting_user: &ConnectedUser,
280284
) -> RsResult<Person> {
281-
requesting_user.check_library_role(library_id, LibraryRole::Admin)?;
285+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
286+
if !key.people { return Err(Error::ShareTokenInsufficient.into()); }
287+
// Upload keys can only add alts and links, nothing else
288+
update = PersonForUpdate {
289+
add_alts: update.add_alts,
290+
add_social_url: update.add_social_url,
291+
add_socials: update.add_socials,
292+
..Default::default()
293+
};
294+
} else {
295+
requesting_user.check_library_role(library_id, LibraryRole::Admin)?;
296+
}
282297
let store = self.store.get_library_store_optional(library_id).ok_or(
283298
Error::LibraryStoreNotFoundFor(library_id.to_string(), "update_person".to_string()),
284299
)?;
@@ -323,7 +338,11 @@ impl ModelController {
323338
new_person: PersonForAdd,
324339
requesting_user: &ConnectedUser,
325340
) -> Result<Person> {
326-
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
341+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
342+
if !key.people { return Err(Error::ShareTokenInsufficient); }
343+
} else {
344+
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
345+
}
327346
let store = self.store.get_library_store_optional(library_id).ok_or(
328347
Error::LibraryStoreNotFoundFor(library_id.to_string(), "add_pesron".to_string()),
329348
)?;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE uploadkeys ADD COLUMN people INTEGER DEFAULT "0";

src/model/store/sql/library/medias.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,13 @@ impl SqliteLibraryStore {
499499
));
500500
}
501501

502+
if let Some(key) = query.uploadkey {
503+
where_query.add_where(SqlWhereType::Equal(
504+
"uploadkey".to_string(),
505+
Box::new(key),
506+
));
507+
}
508+
502509
where_query.add_oder(OrderBuilder::new(sort.to_owned(), query.order.clone()));
503510
if sort == "rating" {
504511
where_query.add_oder(OrderBuilder::new("m.added".to_owned(), SqlOrder::DESC));
@@ -1068,7 +1075,7 @@ impl SqliteLibraryStore {
10681075
where_query.add_update(&update.lang, "lang");
10691076

10701077
where_query.add_update(&update.uploader, "uploader");
1071-
where_query.add_update(&update.uploadkey, "uploaderkey");
1078+
where_query.add_update(&update.uploadkey, "uploadkey");
10721079

10731080
where_query.add_update(&update.original_hash, "originalhash");
10741081
where_query.add_update(&update.original_id, "originalid");

src/model/store/sql/users.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ impl SqliteStore {
209209
pub async fn get_upload_key(&self, key: String) -> Result<UploadKey> {
210210
let keyc = key.clone();
211211
let row = self.server_store.call( move |conn| {
212-
let mut query = conn.prepare("SELECT id, library_ref, expiry, tags FROM uploadkeys where id = ?")?;
212+
let mut query = conn.prepare("SELECT id, library_ref, expiry, tags, people FROM uploadkeys where id = ?")?;
213213

214214
let rows = query.query_map(
215215
params![key], Self::row_to_uploadkey,
@@ -221,13 +221,42 @@ impl SqliteStore {
221221
Ok(uploadkey.clone())
222222
}
223223

224+
pub async fn get_upload_keys(&self) -> Result<Vec<UploadKey>> {
225+
let row = self.server_store.call(move |conn| {
226+
let mut query = conn.prepare("SELECT id, library_ref, expiry, tags, people FROM uploadkeys")?;
227+
let rows = query.query_map([], Self::row_to_uploadkey)?;
228+
let keys: Vec<UploadKey> = rows.collect::<std::result::Result<Vec<UploadKey>, rusqlite::Error>>()?;
229+
Ok(keys)
230+
}).await?;
231+
Ok(row)
232+
}
233+
234+
pub async fn add_upload_key(&self, key: UploadKey) -> Result<()> {
235+
self.server_store.call(move |conn| {
236+
conn.execute(
237+
"INSERT INTO uploadkeys (id, library_ref, expiry, tags, people) VALUES (?, ?, ?, ?, ?)",
238+
params![key.id, key.library, key.expiry, key.tags, key.people],
239+
)?;
240+
Ok(())
241+
}).await?;
242+
Ok(())
243+
}
244+
245+
pub async fn remove_upload_key(&self, key_id: String) -> Result<()> {
246+
self.server_store.call(move |conn| {
247+
conn.execute("DELETE FROM uploadkeys WHERE id = ?", &[&key_id])?;
248+
Ok(())
249+
}).await?;
250+
Ok(())
251+
}
252+
224253
fn row_to_uploadkey(row: &Row) -> rusqlite::Result<UploadKey> {
225254
Ok(UploadKey {
226255
id: row.get(0)?,
227256
library: row.get(1)?,
228257
expiry: row.get(2)?,
229258
tags: row.get(3)?,
230-
259+
people: row.get(4)?,
231260
})
232261
}
233262
}

src/model/tags.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ impl TagQuery {
100100
impl ModelController {
101101

102102
pub async fn get_tags(&self, library_id: &str, query: TagQuery, requesting_user: &ConnectedUser) -> RsResult<Vec<Tag>> {
103-
requesting_user.check_library_role(library_id, LibraryRole::Read)?;
103+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
104+
if !key.tags { return Err(crate::Error::Forbiden.into()); }
105+
} else {
106+
requesting_user.check_library_role(library_id, LibraryRole::Read)?;
107+
}
104108
let store = self.store.get_library_store(library_id)?;
105109
let tags = store.get_tags(query).await?;
106110
Ok(tags)
@@ -290,7 +294,11 @@ impl ModelController {
290294

291295

292296
pub async fn add_tag(&self, library_id: &str, new_tag: TagForAdd, requesting_user: &ConnectedUser) -> RsResult<Tag> {
293-
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
297+
if let Ok(key) = requesting_user.check_upload_key(library_id) {
298+
if !key.tags { return Err(crate::Error::Forbiden.into()); }
299+
} else {
300+
requesting_user.check_library_role(library_id, LibraryRole::Write)?;
301+
}
294302
let store = self.store.get_library_store(library_id)?;
295303
let backup = TagForInsert {
296304
id: nanoid!(),

src/model/users.rs

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ impl ConnectedUser {
5555

5656

5757

58+
pub fn check_upload_key(&self, library_id: &str) -> Result<&UploadKey> {
59+
if let ConnectedUser::UploadKey(key) = &self {
60+
if key.library == library_id {
61+
Ok(key)
62+
} else {
63+
Err(Error::ShareTokenInsufficient)
64+
}
65+
} else {
66+
Err(Error::NotServerConnected)
67+
}
68+
}
69+
5870
pub fn user_id(&self) -> Result<String> {
5971
if let ConnectedUser::Server(user) = &self {
6072
Ok(user.id.clone())
@@ -118,12 +130,8 @@ impl ConnectedUser {
118130
},
119131
_ => Err(Error::ShareTokenInsufficient),
120132
}
121-
} else if let ConnectedUser::UploadKey(key) = &self {
122-
if key.library == library_id {
123-
Ok(LibraryLimits::default())
124-
} else {
125-
Err(Error::ShareTokenInsufficient)
126-
}
133+
} else if let ConnectedUser::UploadKey(_) = &self {
134+
Err(Error::ShareTokenInsufficient)
127135
} else {
128136
Err(Error::NotServerConnected)
129137
}
@@ -332,6 +340,18 @@ pub struct UploadKey {
332340
pub expiry: Option<i64>,
333341
#[serde(default)]
334342
pub tags: bool,
343+
#[serde(default)]
344+
pub people: bool,
345+
}
346+
347+
#[derive(Debug, Serialize, Deserialize, Clone)]
348+
pub struct UploadKeyForCreate {
349+
pub library: String,
350+
pub expiry: Option<i64>,
351+
#[serde(default)]
352+
pub tags: bool,
353+
#[serde(default)]
354+
pub people: bool,
335355
}
336356

337357
impl ServerUser {
@@ -579,6 +599,29 @@ impl ModelController {
579599
Ok(self.store.get_upload_key(key).await?)
580600
}
581601

602+
pub async fn get_upload_keys(&self, requesting_user: &ConnectedUser) -> RsResult<Vec<UploadKey>> {
603+
requesting_user.check_role(&UserRole::Admin)?;
604+
Ok(self.store.get_upload_keys().await?)
605+
}
606+
607+
pub async fn add_upload_key(&self, params: UploadKeyForCreate, requesting_user: &ConnectedUser) -> RsResult<UploadKey> {
608+
requesting_user.check_role(&UserRole::Admin)?;
609+
let key = UploadKey {
610+
id: nanoid::nanoid!(),
611+
library: params.library,
612+
expiry: params.expiry,
613+
tags: params.tags,
614+
people: params.people,
615+
};
616+
self.store.add_upload_key(key.clone()).await?;
617+
Ok(key)
618+
}
619+
620+
pub async fn remove_upload_key(&self, key_id: &str, requesting_user: &ConnectedUser) -> RsResult<()> {
621+
requesting_user.check_role(&UserRole::Admin)?;
622+
self.store.remove_upload_key(key_id.to_string()).await?;
623+
Ok(())
624+
}
582625

583626
pub async fn redeem_invitation(&self, code: String, user: ConnectedUser) -> RsResult<String> {
584627
let connected_user = match user {

0 commit comments

Comments
 (0)