add permission control

This commit is contained in:
2022-07-22 20:28:42 +08:00
parent b0280767cb
commit 51e5f2d0fb
13 changed files with 127 additions and 185 deletions

View File

@@ -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. - `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. - `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. - `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. 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 ```json
{ {
"status": "OK" "status": "OK"
} }
``` ```
@@ -107,7 +118,7 @@ Sometime errors happen, server will return the following JSON object, which `err
```json ```json
{ {
"error": "Wrong password" "error": "Wrong password"
} }
``` ```

View File

@@ -38,8 +38,39 @@
}, },
{ "name": "MP3 128k", "args": "-c:a mp3 -ab 128k -vn", "format": "mp3" }, { "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": "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": { "tmpfs": {
"file_life_time": 600, "file_life_time": 600,

View File

@@ -94,11 +94,11 @@ func NewAPI(config commonconfig.Config) (*API, error) {
apiMux.HandleFunc("/update_review", api.HandleUpdateReview) apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview) apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser) apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
// below needs admin // database
apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/walk", api.HandleWalk)
apiMux.HandleFunc("/reset", api.HandleReset) 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")))) mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
return api, nil return api, nil

View File

@@ -14,12 +14,6 @@ type WalkRequest struct {
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) { func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
var err error var err error
// check admin
err = api.CheckAdmin(w, r)
if err != nil {
api.HandleError(w, r, err)
return
}
log.Println("[api] Reset database") log.Println("[api] Reset database")
@@ -46,13 +40,6 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
return return
} }
// check admin
err = api.CheckAdmin(w, r)
if err != nil {
api.HandleError(w, r, err)
return
}
// check root empty // check root empty
if walkRequest.Root == "" { if walkRequest.Root == "" {
api.HandleErrorString(w, r, `key "root" can't be empty`) api.HandleErrorString(w, r, `key "root" can't be empty`)

View File

@@ -56,13 +56,6 @@ type GetFeedbacksResponse struct {
} }
func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) { 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() feedbacks, err := api.Db.GetFeedbacks()
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
@@ -85,15 +78,8 @@ type DeleteFeedbackRequest struct {
} }
func (api *API) HandleDeleteFeedback(w http.ResponseWriter, r *http.Request) { 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{} req := &DeleteFeedbackRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return

View File

@@ -11,15 +11,8 @@ type DeleteFileRequest struct {
} }
func (api *API) HandleDeleteFile(w http.ResponseWriter, r *http.Request) { 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{} req := &DeleteFileRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -42,15 +35,8 @@ type UpdateFilenameRequest struct {
} }
func (api *API) HandleUpdateFilename(w http.ResponseWriter, r *http.Request) { 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{} req := &UpdateFilenameRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -72,15 +58,8 @@ type ResetFilenameRequest struct {
} }
func (api *API) HandleResetFilename(w http.ResponseWriter, r *http.Request) { 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{} req := &ResetFilenameRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return

View File

@@ -11,15 +11,8 @@ type ResetFoldernameRequest struct {
} }
func (api *API) HandleResetFoldername(w http.ResponseWriter, r *http.Request) { 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{} req := &ResetFoldernameRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
@@ -49,13 +42,6 @@ func (api *API) HandleUpdateFoldername(w http.ResponseWriter, r *http.Request) {
return 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) log.Println("[api] Update foldername folderID", req.ID, req.Foldername)
err = api.Db.UpdateFoldername(req.ID, req.Foldername) err = api.Db.UpdateFoldername(req.ID, req.Foldername)

View File

@@ -116,13 +116,8 @@ func (api *API) CheckUserCanModifyReview(w http.ResponseWriter, r *http.Request,
return err return err
} }
err = api.CheckNotAnonymous(w, r) userLevel := api.GetUserLevel(r)
if err != nil { if userLevel != database.RoleAdmin {
return err
}
err = api.CheckAdmin(w, r)
if err != nil {
userID, err := api.GetUserID(w, r) userID, err := api.GetUserID(w, r)
if err != nil { if err != nil {
return err return err

View File

@@ -34,15 +34,8 @@ type InsertTagResponse struct {
} }
func (api *API) HandleInsertTag(w http.ResponseWriter, r *http.Request) { 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{} req := &database.Tag{}
err = json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return 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) { 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{} req := &database.Tag{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -135,15 +121,8 @@ type DeleteTagRequest struct {
} }
func (api *API) HandleDeleteTag(w http.ResponseWriter, r *http.Request) { 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{} req := &DeleteTagRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return

View File

@@ -13,15 +13,8 @@ type PutTagOnFileRequest struct {
} }
func (api *API) HandlePutTagOnFile(w http.ResponseWriter, r *http.Request) { 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{} req := &PutTagOnFileRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -87,15 +80,8 @@ type DeleteTagOnFileRequest struct {
} }
func (api *API) HandleDeleteTagOnFile(w http.ResponseWriter, r *http.Request) { 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{} req := &DeleteTagOnFileRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return

View File

@@ -142,44 +142,6 @@ func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) {
api.HandleOK(w, r) 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) { func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) {
session, _ := api.store.Get(r, api.defaultSessionName) session, _ := api.store.Get(r, api.defaultSessionName)
userId, ok := session.Values["userId"] userId, ok := session.Values["userId"]
@@ -218,14 +180,8 @@ type UpdateUserActiveRequest struct {
} }
func (api *API) HandleUpdateUserActive(w http.ResponseWriter, r *http.Request) { 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{} req := &UpdateUserActiveRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -245,16 +201,11 @@ type UpdateUsernameRequest struct {
} }
func (api *API) HandleUpdateUsername(w http.ResponseWriter, r *http.Request) { func (api *API) HandleUpdateUsername(w http.ResponseWriter, r *http.Request) {
// reject anonymous user // middileware reject anonymous user
err := api.CheckNotAnonymous(w, r)
if err != nil {
api.HandleError(w, r, err)
return
}
req := &UpdateUsernameRequest{} req := &UpdateUsernameRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return
@@ -326,15 +277,10 @@ type UpdateUserPasswordRequest struct {
} }
func (api *API) HandleUpdateUserPassword(w http.ResponseWriter, r *http.Request) { func (api *API) HandleUpdateUserPassword(w http.ResponseWriter, r *http.Request) {
// reject anonymous user // middleware reject anonymous user
err := api.CheckNotAnonymous(w, r)
if err != nil {
api.HandleError(w, r, err)
return
}
req := &UpdateUserPasswordRequest{} req := &UpdateUserPasswordRequest{}
err = json.NewDecoder(r.Body).Decode(req) err := json.NewDecoder(r.Body).Decode(req)
if err != nil { if err != nil {
api.HandleError(w, r, err) api.HandleError(w, r, err)
return return

55
pkg/api/middleware.go Normal file
View File

@@ -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
}

View File

@@ -6,12 +6,13 @@ type Config struct {
} }
type APIConfig struct { type APIConfig struct {
DatabaseName string `json:"database_name"` DatabaseName string `json:"database_name"`
SingleThread bool `json:"single_thread,default=true"` SingleThread bool `json:"single_thread,default=true"`
Addr string `json:"addr"` Addr string `json:"addr"`
FfmpegThreads int64 `json:"ffmpeg_threads"` FfmpegThreads int64 `json:"ffmpeg_threads"`
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"` FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
SECRET string `json:"secret"` SECRET string `json:"secret"`
Permission map[string]int64 `json:"permission"`
} }
type FfmpegConfigList struct { type FfmpegConfigList struct {