diff --git a/README.md b/README.md index ef45540..06a3ded 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,17 @@ Go to register page, select the role to admin, and register the first admin acco - `file_life_time` integer type (second). Life time for temporary file. If the temporary file is not accessed for more than this time, back-end server will delete this file. - `cleaner_internal` integer type (second). Interval for `tmpfs` checking temporary file. - `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.** Directory will be created if not exists. +- `permission`. Specify each API's permission level. + - `0` for no permission required. + - `1` require admin level (highest level) permission. + - `2` require normal user level permission. That is, both admins and registered users can access to this URL, except anonymous users. + - If you want to avoid abuse of the playback API, you can adjust the permission level for these 5 playback-related APIs. + - `/get_file` get file with `io.copy()` method + - `/get_file_direct` get file with `http.serveFile()` method + - `/get_file_stream` call ffmpeg and stream its `stdout` output + - `/prepare_file_stream_direct` call ffmpeg to convert a file + - `/get_file_stream_direct` get the converted file with `http.serveFile()` + - Other URLs not metion in `config.json` will have `0` permission level by default. For windows user, make sure you have `ffmpeg` installed. @@ -99,7 +110,7 @@ API does not need to respond any data will return the following JSON object. ```json { - "status": "OK" + "status": "OK" } ``` @@ -107,7 +118,7 @@ Sometime errors happen, server will return the following JSON object, which `err ```json { - "error": "Wrong password" + "error": "Wrong password" } ``` diff --git a/config.json b/config.json index 64347a7..2dd109d 100644 --- a/config.json +++ b/config.json @@ -38,8 +38,39 @@ }, { "name": "MP3 128k", "args": "-c:a mp3 -ab 128k -vn", "format": "mp3" }, { "name": "MP3 320k", "args": "-c:a mp3 -ab 320k -vn", "format": "mp3" }, - { "name": "全损音质 8k", "args": "-c:a libopus -ab 8k -vn", "format": "webm" } - ] + { + "name": "全损音质 8k", + "args": "-c:a libopus -ab 8k -vn", + "format": "webm" + } + ], + "permission": { + "/register": 0, + "/get_file": 0, + "/get_file_direct": 0, + "/get_file_stream": 0, + "/prepare_file_stream_direct": 0, + "/get_file_stream_direct": 0, + "/walk": 1, + "/reset": 1, + "/update_user_active": 1, + "/get_feedbacks": 1, + "/delete_feedback": 1, + "/delete_file": 1, + "/update_filename": 1, + "/reset_filename": 1, + "/reset_foldername": 1, + "/update_foldername": 1, + "/insert_tag": 1, + "/update_tag": 1, + "/delete_tag": 1, + "/put_tag_on_file": 1, + "/delete_tag_on_file": 1, + "/delete_review": 2, + "/update_review": 2, + "/update_user_password": 2, + "/update_username": 2 + } }, "tmpfs": { "file_life_time": 600, diff --git a/pkg/api/api.go b/pkg/api/api.go index 7bd637a..fe3a293 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -94,11 +94,11 @@ func NewAPI(config commonconfig.Config) (*API, error) { apiMux.HandleFunc("/update_review", api.HandleUpdateReview) apiMux.HandleFunc("/delete_review", api.HandleDeleteReview) apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser) - // below needs admin + // database apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/reset", api.HandleReset) - mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux)) + mux.Handle("/api/v1/", http.StripPrefix("/api/v1", api.PermissionMiddleware(apiMux))) mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build")))) return api, nil diff --git a/pkg/api/handle_database_manage.go b/pkg/api/handle_database_manage.go index eddcd9b..dd8c7ec 100644 --- a/pkg/api/handle_database_manage.go +++ b/pkg/api/handle_database_manage.go @@ -14,12 +14,6 @@ type WalkRequest struct { func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) { var err error - // check admin - err = api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } log.Println("[api] Reset database") @@ -46,13 +40,6 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) { return } - // check admin - err = api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - // check root empty if walkRequest.Root == "" { api.HandleErrorString(w, r, `key "root" can't be empty`) diff --git a/pkg/api/handle_feedback.go b/pkg/api/handle_feedback.go index 3f2eb89..104087a 100644 --- a/pkg/api/handle_feedback.go +++ b/pkg/api/handle_feedback.go @@ -56,13 +56,6 @@ type GetFeedbacksResponse struct { } func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) { - // check if admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - feedbacks, err := api.Db.GetFeedbacks() if err != nil { api.HandleError(w, r, err) @@ -85,15 +78,8 @@ type DeleteFeedbackRequest struct { } func (api *API) HandleDeleteFeedback(w http.ResponseWriter, r *http.Request) { - // check if admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &DeleteFeedbackRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/api/handle_manage_file.go b/pkg/api/handle_manage_file.go index 1aba693..b044aca 100644 --- a/pkg/api/handle_manage_file.go +++ b/pkg/api/handle_manage_file.go @@ -11,15 +11,8 @@ type DeleteFileRequest struct { } func (api *API) HandleDeleteFile(w http.ResponseWriter, r *http.Request) { - // check admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &DeleteFileRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -42,15 +35,8 @@ type UpdateFilenameRequest struct { } func (api *API) HandleUpdateFilename(w http.ResponseWriter, r *http.Request) { - // check admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &UpdateFilenameRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -72,15 +58,8 @@ type ResetFilenameRequest struct { } func (api *API) HandleResetFilename(w http.ResponseWriter, r *http.Request) { - // check admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &ResetFilenameRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/api/handle_manage_folder.go b/pkg/api/handle_manage_folder.go index a799ba0..38e2444 100644 --- a/pkg/api/handle_manage_folder.go +++ b/pkg/api/handle_manage_folder.go @@ -11,15 +11,8 @@ type ResetFoldernameRequest struct { } func (api *API) HandleResetFoldername(w http.ResponseWriter, r *http.Request) { - // check admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &ResetFoldernameRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return @@ -49,13 +42,6 @@ func (api *API) HandleUpdateFoldername(w http.ResponseWriter, r *http.Request) { return } - // check is admin - err = api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - log.Println("[api] Update foldername folderID", req.ID, req.Foldername) err = api.Db.UpdateFoldername(req.ID, req.Foldername) diff --git a/pkg/api/handle_review.go b/pkg/api/handle_review.go index 53caa05..bf1d44f 100644 --- a/pkg/api/handle_review.go +++ b/pkg/api/handle_review.go @@ -116,13 +116,8 @@ func (api *API) CheckUserCanModifyReview(w http.ResponseWriter, r *http.Request, return err } - err = api.CheckNotAnonymous(w, r) - if err != nil { - return err - } - - err = api.CheckAdmin(w, r) - if err != nil { + userLevel := api.GetUserLevel(r) + if userLevel != database.RoleAdmin { userID, err := api.GetUserID(w, r) if err != nil { return err diff --git a/pkg/api/handle_tag.go b/pkg/api/handle_tag.go index ba0a8be..3f03716 100644 --- a/pkg/api/handle_tag.go +++ b/pkg/api/handle_tag.go @@ -34,15 +34,8 @@ type InsertTagResponse struct { } func (api *API) HandleInsertTag(w http.ResponseWriter, r *http.Request) { - // check if user is admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &database.Tag{} - err = json.NewDecoder(r.Body).Decode(&req) + err := json.NewDecoder(r.Body).Decode(&req) if err != nil { api.HandleError(w, r, err) return @@ -107,15 +100,8 @@ func (api *API) HandleGetTagInfo(w http.ResponseWriter, r *http.Request) { } func (api *API) HandleUpdateTag(w http.ResponseWriter, r *http.Request) { - // check if user is admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &database.Tag{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -135,15 +121,8 @@ type DeleteTagRequest struct { } func (api *API) HandleDeleteTag(w http.ResponseWriter, r *http.Request) { - // check if user is admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &DeleteTagRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/api/handle_tag_and_file.go b/pkg/api/handle_tag_and_file.go index e9df22e..8a8d442 100644 --- a/pkg/api/handle_tag_and_file.go +++ b/pkg/api/handle_tag_and_file.go @@ -13,15 +13,8 @@ type PutTagOnFileRequest struct { } func (api *API) HandlePutTagOnFile(w http.ResponseWriter, r *http.Request) { - // check if the user is admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &PutTagOnFileRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -87,15 +80,8 @@ type DeleteTagOnFileRequest struct { } func (api *API) HandleDeleteTagOnFile(w http.ResponseWriter, r *http.Request) { - // check if the user is admin - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &DeleteTagOnFileRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/api/handle_user.go b/pkg/api/handle_user.go index a3f0038..9f0afb1 100644 --- a/pkg/api/handle_user.go +++ b/pkg/api/handle_user.go @@ -142,44 +142,6 @@ func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) { api.HandleOK(w, r) } -func (api *API) CheckAdmin(w http.ResponseWriter, r *http.Request) error { - session, _ := api.store.Get(r, api.defaultSessionName) - userId, ok := session.Values["userId"] - if !ok { - return ErrNotLoggedIn - } - - user, err := api.Db.GetUserById(userId.(int64)) - if err != nil { - return err - } - - if user.Role != database.RoleAdmin { - return ErrNotAdmin - } - - return nil -} - -func (api *API) CheckNotAnonymous(w http.ResponseWriter, r *http.Request) error { - session, _ := api.store.Get(r, api.defaultSessionName) - userId, ok := session.Values["userId"] - if !ok { - return ErrNotLoggedIn - } - - user, err := api.Db.GetUserById(userId.(int64)) - if err != nil { - return err - } - - if user.Role == database.RoleAnonymous { - return ErrAnonymous - } - - return nil -} - func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) { session, _ := api.store.Get(r, api.defaultSessionName) userId, ok := session.Values["userId"] @@ -218,14 +180,8 @@ type UpdateUserActiveRequest struct { } func (api *API) HandleUpdateUserActive(w http.ResponseWriter, r *http.Request) { - err := api.CheckAdmin(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } - req := &UpdateUserActiveRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -245,16 +201,11 @@ type UpdateUsernameRequest struct { } func (api *API) HandleUpdateUsername(w http.ResponseWriter, r *http.Request) { - // reject anonymous user - err := api.CheckNotAnonymous(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } + // middileware reject anonymous user req := &UpdateUsernameRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return @@ -326,15 +277,10 @@ type UpdateUserPasswordRequest struct { } func (api *API) HandleUpdateUserPassword(w http.ResponseWriter, r *http.Request) { - // reject anonymous user - err := api.CheckNotAnonymous(w, r) - if err != nil { - api.HandleError(w, r, err) - return - } + // middleware reject anonymous user req := &UpdateUserPasswordRequest{} - err = json.NewDecoder(r.Body).Decode(req) + err := json.NewDecoder(r.Body).Decode(req) if err != nil { api.HandleError(w, r, err) return diff --git a/pkg/api/middleware.go b/pkg/api/middleware.go new file mode 100644 index 0000000..a827bc4 --- /dev/null +++ b/pkg/api/middleware.go @@ -0,0 +1,55 @@ +package api + +import ( + "errors" + "net/http" +) + +func (api *API) PermissionMiddleware(next http.Handler) http.Handler { + // 0 anonymous user + // 1 admin + // 2 normal user + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // get permission of URL + permission, ok := api.APIConfig.Permission[r.URL.Path] + // 0 means no permission required + if !ok || permission == 0 { + next.ServeHTTP(w, r) + return + } + + // ger user permission level + userLevel := api.GetUserLevel(r) + + // admin has root (highest) permission level 1 + if userLevel == 1 { + next.ServeHTTP(w, r) + return + } + + // anonymous userLevel 0 don't have any permission + // check permission level for other users + if userLevel == 0 || userLevel > permission { + api.HandleError(w, r, errors.New("No enougth permission")) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (api *API) GetUserLevel(r *http.Request) int64 { + session, _ := api.store.Get(r, api.defaultSessionName) + userId, ok := session.Values["userId"] + if !ok { + // not logined user is considered anonymous user + return 0 + } + + user, err := api.Db.GetUserById(userId.(int64)) + if err != nil { + return 0 + } + + return user.Role +} diff --git a/pkg/commonconfig/config.go b/pkg/commonconfig/config.go index fa29420..ea77079 100644 --- a/pkg/commonconfig/config.go +++ b/pkg/commonconfig/config.go @@ -6,12 +6,13 @@ type Config struct { } type APIConfig struct { - DatabaseName string `json:"database_name"` - SingleThread bool `json:"single_thread,default=true"` - Addr string `json:"addr"` - FfmpegThreads int64 `json:"ffmpeg_threads"` - FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` - SECRET string `json:"secret"` + DatabaseName string `json:"database_name"` + SingleThread bool `json:"single_thread,default=true"` + Addr string `json:"addr"` + FfmpegThreads int64 `json:"ffmpeg_threads"` + FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` + SECRET string `json:"secret"` + Permission map[string]int64 `json:"permission"` } type FfmpegConfigList struct {