Files
vibesonic/internal/subsonic/users.go
T

285 lines
8.6 KiB
Go

package subsonic
import (
"encoding/hex"
"strconv"
"strings"
vsauth "git.noroutes.net/NoRoutes/vibetunes/internal/auth"
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
)
func init() {
register("getUser", handleGetUser)
register("getUsers", handleGetUsers)
register("createUser", handleCreateUser)
register("updateUser", handleUpdateUser)
register("deleteUser", handleDeleteUser)
register("changePassword", handleChangePassword)
}
func userToModel(rc *Context, u *database.User) User {
db := rc.Server.DB
m := User{
Username: u.Username,
Email: u.Email,
ScrobblingEnabled: true,
AdminRole: u.IsAdmin,
SettingsRole: u.Allows(u.Permissions.Settings),
DownloadRole: u.Allows(u.Permissions.Download),
UploadRole: u.Allows(u.Permissions.Upload),
PlaylistRole: u.Allows(u.Permissions.Playlist),
CoverArtRole: u.Allows(u.Permissions.CoverArt),
CommentRole: u.Allows(u.Permissions.Comment),
PodcastRole: u.Allows(u.Permissions.Podcast),
StreamRole: u.Allows(u.Permissions.Stream),
JukeboxRole: u.Allows(u.Permissions.Jukebox),
ShareRole: u.Allows(u.Permissions.Share),
VideoConversionRole: u.Allows(u.Permissions.VideoConversion),
}
if fm, err := loadFolderMap(db); err == nil {
allowedIDs, _ := database.AllowedMusicFolderIDs(db, u.ID)
if len(allowedIDs) == 0 {
for _, f := range fm.folders {
n, _ := strconv.Atoi(fm.IntID(f.ID))
m.Folder = append(m.Folder, n)
}
} else {
for _, id := range allowedIDs {
n, _ := strconv.Atoi(fm.IntID(id))
m.Folder = append(m.Folder, n)
}
}
}
return m
}
func handleGetUser(rc *Context) *apiError {
username, aerr := rc.Params.Required("username")
if aerr != nil {
return aerr
}
if username != rc.User.Username && !rc.User.IsAdmin {
return errUnauthorized("viewing another user's account")
}
target, err := database.GetUserByUsername(rc.Server.DB, username)
if err != nil {
return errNotFound("User")
}
model := userToModel(rc, target)
rc.Response = newResponse()
rc.Response.User = &model
return nil
}
func handleGetUsers(rc *Context) *apiError {
if !rc.User.IsAdmin {
return errUnauthorized("listing users")
}
users, err := database.ListUsers(rc.Server.DB)
if err != nil {
return newError(ErrGeneric, err.Error())
}
out := &Users{}
for _, u := range users {
out.User = append(out.User, userToModel(rc, u))
}
rc.Response = newResponse()
rc.Response.Users = out
return nil
}
// permissionsFromParams builds a Permissions set for a brand-new user:
// any role param not present defaults to false (an explicit opt-in
// model for account creation).
func permissionsFromParams(p params) database.Permissions {
get := func(name string) bool { return p.Bool(name, false) }
return database.Permissions{
Settings: get("settingsRole"),
Stream: get("streamRole"),
Download: get("downloadRole"),
Upload: get("uploadRole"),
Playlist: get("playlistRole"),
CoverArt: get("coverArtRole"),
Comment: get("commentRole"),
Podcast: get("podcastRole"),
Share: get("shareRole"),
Jukebox: get("jukeboxRole"),
VideoConversion: get("videoConversionRole"),
}
}
// mergePermissionsFromParams overlays only the role params actually
// present in the request onto existing, so updateUser calls that omit
// some roles (e.g. a client only toggling one checkbox) don't silently
// revoke the rest.
func mergePermissionsFromParams(p params, existing database.Permissions) database.Permissions {
set := func(name string, cur bool) bool {
if !p.Has(name) {
return cur
}
return p.Bool(name, cur)
}
return database.Permissions{
Settings: set("settingsRole", existing.Settings),
Stream: set("streamRole", existing.Stream),
Download: set("downloadRole", existing.Download),
Upload: set("uploadRole", existing.Upload),
Playlist: set("playlistRole", existing.Playlist),
CoverArt: set("coverArtRole", existing.CoverArt),
Comment: set("commentRole", existing.Comment),
Podcast: set("podcastRole", existing.Podcast),
Share: set("shareRole", existing.Share),
Jukebox: set("jukeboxRole", existing.Jukebox),
VideoConversion: set("videoConversionRole", existing.VideoConversion),
}
}
func decodePassword(raw string) string {
if strings.HasPrefix(raw, "enc:") {
if decoded, err := hex.DecodeString(strings.TrimPrefix(raw, "enc:")); err == nil {
return string(decoded)
}
}
return raw
}
func handleCreateUser(rc *Context) *apiError {
if !rc.User.IsAdmin {
return errUnauthorized("creating users")
}
username, aerr := rc.Params.Required("username")
if aerr != nil {
return aerr
}
rawPassword, aerr := rc.Params.Required("password")
if aerr != nil {
return aerr
}
email := rc.Params.Get("email")
isAdmin := rc.Params.Bool("adminRole", false)
user, err := database.CreateUser(rc.Server.DB, username, email, "", isAdmin)
if err != nil {
return newError(ErrGeneric, "could not create user: "+err.Error())
}
if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, user.ID, decodePassword(rawPassword)); err != nil {
return newError(ErrGeneric, err.Error())
}
if err := database.SetUserPermissions(rc.Server.DB, user.ID, permissionsFromParams(rc.Params)); err != nil {
return newError(ErrGeneric, err.Error())
}
if folderIDs := rc.Params.GetAll("musicFolderId"); len(folderIDs) > 0 {
if err := applyFolderRestriction(rc, user.ID, folderIDs); err != nil {
return err
}
}
rc.Response = newResponse()
return nil
}
func applyFolderRestriction(rc *Context, userID string, folderIntIDs []string) *apiError {
fm, err := loadFolderMap(rc.Server.DB)
if err != nil {
return newError(ErrGeneric, err.Error())
}
var ids []string
for _, s := range folderIntIDs {
if f := fm.ByIntID(s); f != nil {
ids = append(ids, f.ID)
}
}
if err := database.SetAllowedMusicFolders(rc.Server.DB, userID, ids); err != nil {
return newError(ErrGeneric, err.Error())
}
return nil
}
func handleUpdateUser(rc *Context) *apiError {
if !rc.User.IsAdmin {
return errUnauthorized("modifying users")
}
username, aerr := rc.Params.Required("username")
if aerr != nil {
return aerr
}
target, err := database.GetUserByUsername(rc.Server.DB, username)
if err != nil {
return errNotFound("User")
}
if email := rc.Params.Get("email"); email != "" {
if err := database.SetUserEmail(rc.Server.DB, target.ID, email); err != nil {
return newError(ErrGeneric, err.Error())
}
}
if rc.Params.Has("adminRole") {
if err := database.SetUserAdmin(rc.Server.DB, target.ID, rc.Params.Bool("adminRole", target.IsAdmin)); err != nil {
return newError(ErrGeneric, err.Error())
}
}
if rc.Params.Has("password") {
if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, target.ID, decodePassword(rc.Params.Get("password"))); err != nil {
return newError(ErrGeneric, err.Error())
}
}
merged := mergePermissionsFromParams(rc.Params, target.Permissions)
if err := database.SetUserPermissions(rc.Server.DB, target.ID, merged); err != nil {
return newError(ErrGeneric, err.Error())
}
if folderIDs := rc.Params.GetAll("musicFolderId"); len(folderIDs) > 0 {
if aerr := applyFolderRestriction(rc, target.ID, folderIDs); aerr != nil {
return aerr
}
}
rc.Response = newResponse()
return nil
}
func handleDeleteUser(rc *Context) *apiError {
if !rc.User.IsAdmin {
return errUnauthorized("deleting users")
}
username, aerr := rc.Params.Required("username")
if aerr != nil {
return aerr
}
target, err := database.GetUserByUsername(rc.Server.DB, username)
if err != nil {
return errNotFound("User")
}
if target.ID == rc.User.ID {
return newError(ErrGeneric, "cannot delete your own account")
}
if err := database.DeleteUser(rc.Server.DB, target.ID); err != nil {
return newError(ErrGeneric, err.Error())
}
rc.Response = newResponse()
return nil
}
func handleChangePassword(rc *Context) *apiError {
username, aerr := rc.Params.Required("username")
if aerr != nil {
return aerr
}
rawPassword, aerr := rc.Params.Required("password")
if aerr != nil {
return aerr
}
if username != rc.User.Username && !rc.User.IsAdmin {
return errUnauthorized("changing another user's password")
}
target, err := database.GetUserByUsername(rc.Server.DB, username)
if err != nil {
return errNotFound("User")
}
if err := vsauth.SetUserPassword(rc.Server.DB, rc.Server.EncryptionKey, target.ID, decodePassword(rawPassword)); err != nil {
return newError(ErrGeneric, err.Error())
}
rc.Response = newResponse()
return nil
}