mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Restrict user permissions by tag
This commit is contained in:
		
							parent
							
								
									f8d0384155
								
							
						
					
					
						commit
						27f1bd90f9
					
				| @ -77,6 +77,19 @@ | |||||||
|             <div v-if="!newUser.permissions.accessAllLibraries" class="my-4"> |             <div v-if="!newUser.permissions.accessAllLibraries" class="my-4"> | ||||||
|               <ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" label="Libraries Accessible to User" /> |               <ui-multi-select-dropdown v-model="newUser.librariesAccessible" :items="libraryItems" label="Libraries Accessible to User" /> | ||||||
|             </div> |             </div> | ||||||
|  | 
 | ||||||
|  |             <div class="flex items-cen~ter my-2 max-w-md"> | ||||||
|  |               <div class="w-1/2"> | ||||||
|  |                 <p>Can Access All Tags</p> | ||||||
|  |               </div> | ||||||
|  |               <div class="w-1/2"> | ||||||
|  |                 <ui-toggle-switch v-model="newUser.permissions.accessAllTags" @input="accessAllTagsToggled" /> | ||||||
|  |               </div> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <div v-if="!newUser.permissions.accessAllTags" class="my-4"> | ||||||
|  |               <ui-multi-select-dropdown v-model="newUser.itemsTagsAccessible" :items="itemTags" label="Tags Accessible to User" /> | ||||||
|  |             </div> | ||||||
|           </div> |           </div> | ||||||
| 
 | 
 | ||||||
|           <div class="flex pt-4"> |           <div class="flex pt-4"> | ||||||
| @ -103,7 +116,9 @@ export default { | |||||||
|       processing: false, |       processing: false, | ||||||
|       newUser: {}, |       newUser: {}, | ||||||
|       isNew: true, |       isNew: true, | ||||||
|       accountTypes: ['guest', 'user', 'admin'] |       accountTypes: ['guest', 'user', 'admin'], | ||||||
|  |       tags: [], | ||||||
|  |       loadingTags: false | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @ -135,9 +150,40 @@ export default { | |||||||
|     }, |     }, | ||||||
|     libraryItems() { |     libraryItems() { | ||||||
|       return this.libraries.map((lib) => ({ text: lib.name, value: lib.id })) |       return this.libraries.map((lib) => ({ text: lib.name, value: lib.id })) | ||||||
|  |     }, | ||||||
|  |     itemTags() { | ||||||
|  |       return this.tags.map((t) => { | ||||||
|  |         return { | ||||||
|  |           text: t, | ||||||
|  |           value: t | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|  |     accessAllTagsToggled(val) { | ||||||
|  |       if (!val) { | ||||||
|  |         this.fetchAllTags() | ||||||
|  |       } | ||||||
|  |       if (!val && !this.newUser.itemTagsAccessible.length) { | ||||||
|  |         this.newUser.itemTagsAccessible = this.libraries.map((l) => l.id) | ||||||
|  |       } else if (val && this.newUser.itemTagsAccessible.length) { | ||||||
|  |         this.newUser.itemTagsAccessible = [] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     fetchAllTags() { | ||||||
|  |       this.loadingTags = true | ||||||
|  |       this.$axios | ||||||
|  |         .$get(`/api/tags`) | ||||||
|  |         .then((tags) => { | ||||||
|  |           this.tags = tags | ||||||
|  |           this.loadingTags = false | ||||||
|  |         }) | ||||||
|  |         .catch((error) => { | ||||||
|  |           console.error('Failed to load tags', error) | ||||||
|  |           this.loadingTags = false | ||||||
|  |         }) | ||||||
|  |     }, | ||||||
|     accessAllLibrariesToggled(val) { |     accessAllLibrariesToggled(val) { | ||||||
|       if (!val && !this.newUser.librariesAccessible.length) { |       if (!val && !this.newUser.librariesAccessible.length) { | ||||||
|         this.newUser.librariesAccessible = this.libraries.map((l) => l.id) |         this.newUser.librariesAccessible = this.libraries.map((l) => l.id) | ||||||
| @ -223,20 +269,22 @@ export default { | |||||||
|         download: type !== 'guest', |         download: type !== 'guest', | ||||||
|         update: type === 'admin', |         update: type === 'admin', | ||||||
|         delete: type === 'admin', |         delete: type === 'admin', | ||||||
|         upload: type === 'admin' |         upload: type === 'admin', | ||||||
|  |         accessAllLibraries: true, | ||||||
|  |         accessAllTags: true | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     init() { |     init() { | ||||||
|       this.isNew = !this.account |       this.isNew = !this.account | ||||||
|       if (this.account) { |       if (this.account) { | ||||||
|         var librariesAccessible = this.account.librariesAccessible || [] |  | ||||||
|         this.newUser = { |         this.newUser = { | ||||||
|           username: this.account.username, |           username: this.account.username, | ||||||
|           password: this.account.password, |           password: this.account.password, | ||||||
|           type: this.account.type, |           type: this.account.type, | ||||||
|           isActive: this.account.isActive, |           isActive: this.account.isActive, | ||||||
|           permissions: { ...this.account.permissions }, |           permissions: { ...this.account.permissions }, | ||||||
|           librariesAccessible: [...librariesAccessible] |           librariesAccessible: [...(this.account.librariesAccessible || [])], | ||||||
|  |           itemTagsAccessible: [...(this.account.itemTagsAccessible || [])] | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         this.newUser = { |         this.newUser = { | ||||||
| @ -249,7 +297,8 @@ export default { | |||||||
|             update: false, |             update: false, | ||||||
|             delete: false, |             delete: false, | ||||||
|             upload: false, |             upload: false, | ||||||
|             accessAllLibraries: true |             accessAllLibraries: true, | ||||||
|  |             accessAllTags: true | ||||||
|           }, |           }, | ||||||
|           librariesAccessible: [] |           librariesAccessible: [] | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -233,7 +233,9 @@ class LibraryController { | |||||||
|     if (!req.params.series) { |     if (!req.params.series) { | ||||||
|       return res.status(403).send('Invalid series') |       return res.status(403).send('Invalid series') | ||||||
|     } |     } | ||||||
|     var libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id && li.book.series === req.params.series) |     var libraryItems = this.db.libraryItems.filter(li => { | ||||||
|  |       return li.libraryId === req.library.id && li.book.series === req.params.series | ||||||
|  |     }) | ||||||
|     if (!libraryItems.length) { |     if (!libraryItems.length) { | ||||||
|       return res.status(404).send('Series not found') |       return res.status(404).send('Series not found') | ||||||
|     } |     } | ||||||
| @ -530,7 +532,9 @@ class LibraryController { | |||||||
|       return res.status(404).send('Library not found') |       return res.status(404).send('Library not found') | ||||||
|     } |     } | ||||||
|     req.library = library |     req.library = library | ||||||
|     req.libraryItems = this.db.libraryItems.filter(li => li.libraryId === library.id) |     req.libraryItems = this.db.libraryItems.filter(li => { | ||||||
|  |       return li.libraryId === library.id && req.user.checkCanAccessLibraryItemWithTags(li.media.tags) | ||||||
|  |     }) | ||||||
|     next() |     next() | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -262,6 +262,11 @@ class LibraryItemController { | |||||||
|       return res.sendStatus(403) |       return res.sendStatus(403) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Check user can access this library item
 | ||||||
|  |     if (!req.user.checkCanAccessLibraryItemWithTags(item.media.tags)) { | ||||||
|  |       return res.sendStatus(403) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { |     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||||
|       Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) |       Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) | ||||||
|       return res.sendStatus(403) |       return res.sendStatus(403) | ||||||
|  | |||||||
| @ -170,5 +170,21 @@ class MiscController { | |||||||
|     } |     } | ||||||
|     res.json({ user: req.user }) |     res.json({ user: req.user }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getAllTags(req, res) { | ||||||
|  |     if (!req.user.isRoot) { | ||||||
|  |       Logger.error(`[MiscController] Non-root user attempted to getAllTags`) | ||||||
|  |       return res.sendStatus(404) | ||||||
|  |     } | ||||||
|  |     var tags = [] | ||||||
|  |     this.db.libraryItems.forEach((li) => { | ||||||
|  |       if (li.media.tags && li.media.tags.length) { | ||||||
|  |         li.media.tags.forEach((tag) => { | ||||||
|  |           if (!tags.includes(tag)) tags.push(tag) | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |     res.json(tags) | ||||||
|  |   } | ||||||
| } | } | ||||||
| module.exports = new MiscController() | module.exports = new MiscController() | ||||||
| @ -20,6 +20,7 @@ class User { | |||||||
|     this.settings = {} |     this.settings = {} | ||||||
|     this.permissions = {} |     this.permissions = {} | ||||||
|     this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
 |     this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
 | ||||||
|  |     this.itemTagsAccessible = [] // Empty if ALL item tags accessible
 | ||||||
| 
 | 
 | ||||||
|     if (user) { |     if (user) { | ||||||
|       this.construct(user) |       this.construct(user) | ||||||
| @ -44,6 +45,9 @@ class User { | |||||||
|   get canAccessAllLibraries() { |   get canAccessAllLibraries() { | ||||||
|     return !!this.permissions.accessAllLibraries && this.isActive |     return !!this.permissions.accessAllLibraries && this.isActive | ||||||
|   } |   } | ||||||
|  |   get canAccessAllTags() { | ||||||
|  |     return !!this.permissions.accessAllTags && this.isActive | ||||||
|  |   } | ||||||
|   get hasPw() { |   get hasPw() { | ||||||
|     return !!this.pash && !!this.pash.length |     return !!this.pash && !!this.pash.length | ||||||
|   } |   } | ||||||
| @ -68,7 +72,8 @@ class User { | |||||||
|       update: true, |       update: true, | ||||||
|       delete: this.type === 'root', |       delete: this.type === 'root', | ||||||
|       upload: this.type === 'root' || this.type === 'admin', |       upload: this.type === 'root' || this.type === 'admin', | ||||||
|       accessAllLibraries: true |       accessAllLibraries: true, | ||||||
|  |       accessAllTags: true | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -87,7 +92,8 @@ class User { | |||||||
|       createdAt: this.createdAt, |       createdAt: this.createdAt, | ||||||
|       settings: this.settings, |       settings: this.settings, | ||||||
|       permissions: this.permissions, |       permissions: this.permissions, | ||||||
|       librariesAccessible: [...this.librariesAccessible] |       librariesAccessible: [...this.librariesAccessible], | ||||||
|  |       itemTagsAccessible: [...this.itemTagsAccessible] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -105,7 +111,8 @@ class User { | |||||||
|       createdAt: this.createdAt, |       createdAt: this.createdAt, | ||||||
|       settings: this.settings, |       settings: this.settings, | ||||||
|       permissions: this.permissions, |       permissions: this.permissions, | ||||||
|       librariesAccessible: [...this.librariesAccessible] |       librariesAccessible: [...this.librariesAccessible], | ||||||
|  |       itemTagsAccessible: [...this.itemTagsAccessible] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -151,8 +158,11 @@ class User { | |||||||
| 
 | 
 | ||||||
|     // Library restriction permissions added v1.4.14, defaults to all libraries
 |     // Library restriction permissions added v1.4.14, defaults to all libraries
 | ||||||
|     if (this.permissions.accessAllLibraries === undefined) this.permissions.accessAllLibraries = true |     if (this.permissions.accessAllLibraries === undefined) this.permissions.accessAllLibraries = true | ||||||
|  |     // Library restriction permissions added v2.0, defaults to all libraries
 | ||||||
|  |     if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true | ||||||
| 
 | 
 | ||||||
|     this.librariesAccessible = (user.librariesAccessible || []).map(l => l) |     this.librariesAccessible = [...(user.librariesAccessible || [])] | ||||||
|  |     this.itemTagsAccessible = [...(user.itemTagsAccessible || [])] | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   update(payload) { |   update(payload) { | ||||||
| @ -190,6 +200,19 @@ class User { | |||||||
|         this.librariesAccessible = [] |         this.librariesAccessible = [] | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // Update accessible libraries
 | ||||||
|  |     if (payload.itemTagsAccessible !== undefined) { | ||||||
|  |       if (payload.itemTagsAccessible.length) { | ||||||
|  |         if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) { | ||||||
|  |           hasUpdates = true | ||||||
|  |           this.itemTagsAccessible = [...payload.itemTagsAccessible] | ||||||
|  |         } | ||||||
|  |       } else if (this.itemTagsAccessible.length > 0) { | ||||||
|  |         hasUpdates = true | ||||||
|  |         this.itemTagsAccessible = [] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|     return hasUpdates |     return hasUpdates | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -270,6 +293,11 @@ class User { | |||||||
|     return this.librariesAccessible.includes(libraryId) |     return this.librariesAccessible.includes(libraryId) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   checkCanAccessLibraryItemWithTags(tags) { | ||||||
|  |     if (this.permissions.accessAllTags || !tags || !tags.length) return true | ||||||
|  |     return this.itemTagsAccessible.some(tag => tags.includes(tag)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   findBookmark(libraryItemId, time) { |   findBookmark(libraryItemId, time) { | ||||||
|     return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) |     return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -192,6 +192,7 @@ class ApiRouter { | |||||||
|     this.router.get('/search/books', MiscController.findBooks.bind(this)) |     this.router.get('/search/books', MiscController.findBooks.bind(this)) | ||||||
|     this.router.get('/search/podcast', MiscController.findPodcasts.bind(this)) |     this.router.get('/search/podcast', MiscController.findPodcasts.bind(this)) | ||||||
|     this.router.get('/search/authors', MiscController.findAuthor.bind(this)) |     this.router.get('/search/authors', MiscController.findAuthor.bind(this)) | ||||||
|  |     this.router.get('/tags', MiscController.getAllTags.bind(this)) | ||||||
| 
 | 
 | ||||||
|     // OLD
 |     // OLD
 | ||||||
|     // this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
 |     // this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this))
 | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user