// // tank, Import and Playlist Daemon for Aura project // Copyright (C) 2017-2020 Christian Pointner <equinox@helsinki.at> // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // package v1 import ( "github.com/gin-gonic/gin/binding" "net/http" "strconv" "github.com/gin-gonic/gin" "gitlab.servus.at/autoradio/tank/auth" "gitlab.servus.at/autoradio/tank/importer" "gitlab.servus.at/autoradio/tank/store" ) type ShowId struct { ShowId uint64 `json:"showId"` } func idFromString(s string) (uint64, error) { return strconv.ParseUint(s, 10, 64) } func getShowID(c *gin.Context) (uint64, error) { // TODO: get rid of this when we remove the nested routes showID := c.Param("show-id") if showID == "" { showID = c.Query("showId") } if showID != "" { return idFromString(showID) } // read the showId as JSON from the body and allow the body to be read again var showId ShowId if err := c.ShouldBindBodyWith(&showId, binding.JSON); err != nil { return 0, err } return showId.ShowId, nil } func statusCodeFromError(err error) (code int, response ErrorResponse) { code = http.StatusInternalServerError response = ErrorResponse{Error: err.Error()} switch err.(type) { case *store.ErrFileInUse: code = http.StatusConflict response.Detail = err case store.ErrInvalidMetadataField: code = http.StatusBadRequest case store.ErrInvalidPlaylistEntry: code = http.StatusBadRequest case *importer.JobSourceResult: response.Detail = err.(*importer.JobSourceResult).Log default: switch err { case ErrNotFlowJSUpload: code = http.StatusConflict case ErrFlowJSChunkAlreadUploading: code = http.StatusConflict case ErrFileVanished: code = http.StatusConflict case store.ErrNotImplemented: code = http.StatusNotImplemented case store.ErrNotFound: code = http.StatusNotFound case store.ErrShowAlreadyExists: code = http.StatusConflict case store.ErrFileNotNew: code = http.StatusConflict case store.ErrFileImportNotDone: code = http.StatusConflict case store.ErrPlaylistHasMultipleNullDurationEntries: code = http.StatusBadRequest case importer.ErrNotImplemented: code = http.StatusNotImplemented case importer.ErrNotFound: code = http.StatusNotFound case importer.ErrSourceNotSupported: code = http.StatusBadRequest case importer.ErrImportNotRunning: code = http.StatusConflict case importer.ErrSourceAlreadyAttached: code = http.StatusConflict case importer.ErrSourceNotYetAttached: code = http.StatusConflict case importer.ErrSourceNotAUpload: code = http.StatusConflict case importer.ErrTooManyJobs: code = http.StatusTooManyRequests case importer.ErrAlreadyCanceled: code = http.StatusConflict } } return } func sendError(c *gin.Context, err error) { code, response := statusCodeFromError(err) c.JSON(code, response) } func parsePositiveIntegerParameter(c *gin.Context, name string) (int, bool) { valueStr := c.Query(name) if valueStr == "" { return -1, true } value, err := strconv.Atoi(valueStr) if err != nil { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter " + name + " is invalid: " + err.Error()}) return -1, false } if value < 0 { c.JSON(http.StatusBadRequest, ErrorResponse{Error: "query parameter " + name + " must be >= 0"}) return -1, false } return value, true } func getPaginationParameter(c *gin.Context) (offset, limit int, ok bool) { if offset, ok = parsePositiveIntegerParameter(c, "offset"); !ok { return } limit, ok = parsePositiveIntegerParameter(c, "limit") return } func getAuthSession(r *http.Request) *auth.Session { s, ok := auth.SessionFromRequest(r) if !ok || s == nil { panic("no session attached to request - is the auth middleware installed?") } return s } func isRequestReadOnly(c *gin.Context) bool { switch c.Request.Method { case http.MethodGet: return true } return false } func authorizeRequestAllShows(c *gin.Context) (bool, *auth.Session) { s := getAuthSession(c.Request) if s.Privileged { return true, s } if s.ReadOnly && !isRequestReadOnly(c) { c.JSON(http.StatusForbidden, ErrorResponse{Error: "this session is read-only"}) return false, s } if s.AllShows { return true, s } c.JSON(http.StatusForbidden, ErrorResponse{Error: "you are not allowed to access all shows"}) return false, s } func authorizeRequestForShow(c *gin.Context, showID uint64) (bool, *auth.Session) { s := getAuthSession(c.Request) if s.Privileged { return true, s } if s.ReadOnly && !isRequestReadOnly(c) { c.JSON(http.StatusForbidden, ErrorResponse{Error: "this session is read-only"}) return false, s } if s.AllShows { return true, s } for _, show := range s.Shows { if show == showID { return true, s } } for _, show := range s.PublicShows { if show == showID { if isRequestReadOnly(c) { return true, s } else { c.JSON(http.StatusForbidden, ErrorResponse{Error: "this is a public show and you are not an owner"}) return false, s } } } c.JSON(http.StatusForbidden, ErrorResponse{Error: "you are not allowed to access show: " + strconv.FormatUint(showID, 10)}) return false, s }