🎨 Add file access control (#9722)

This commit is contained in:
Yingyi / 颖逸 2023-11-22 22:17:18 +08:00 committed by GitHub
parent f5205d846c
commit 4ca62ef701
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 65 deletions

1
API.md
View File

@ -1032,6 +1032,7 @@ View API token in <kbd>Settings - About</kbd>, request header: `Authorization: T
* `code`: non-zero for exceptions * `code`: non-zero for exceptions
* `-1`: Parameter parsing error * `-1`: Parameter parsing error
* `403`: Permission denied (file is not in the workspace)
* `404`: Not Found (file doesn't exist) * `404`: Not Found (file doesn't exist)
* `405`: Method Not Allowed (it's a directory) * `405`: Method Not Allowed (it's a directory)
* `500`: Server Error (stat file failed / read file failed) * `500`: Server Error (stat file failed / read file failed)

View File

@ -1025,6 +1025,7 @@
* `code`: 非零的异常值 * `code`: 非零的异常值
* `-1`: 参数解析错误 * `-1`: 参数解析错误
* `403`: 无访问权限 (文件不在工作空间下)
* `404`: 未找到 (文件不存在) * `404`: 未找到 (文件不存在)
* `405`: 方法不被允许 (这是一个目录) * `405`: 方法不被允许 (这是一个目录)
* `500`: 服务器错误 (文件查询失败 / 文件读取失败) * `500`: 服务器错误 (文件查询失败 / 文件读取失败)

View File

@ -34,20 +34,34 @@ func zip(c *gin.Context) {
return return
} }
path := arg["path"].(string) entryPath := arg["path"].(string)
zipPath := arg["zipPath"].(string) entryAbsPath, err := util.GetAbsPathInWorkspace(entryPath)
zipFile, err := gulu.Zip.Create(zipPath)
if nil != err { if nil != err {
ret.Code = -1 ret.Code = -1
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
base := filepath.Base(path) zipFilePath := arg["zipPath"].(string)
if gulu.File.IsDir(path) { zipAbsFilePath, err := util.GetAbsPathInWorkspace(zipFilePath)
err = zipFile.AddDirectory(base, path) if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
zipFile, err := gulu.Zip.Create(zipAbsFilePath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
base := filepath.Base(entryAbsPath)
if gulu.File.IsDir(entryAbsPath) {
err = zipFile.AddDirectory(base, entryAbsPath)
} else { } else {
err = zipFile.AddEntry(base, path) err = zipFile.AddEntry(base, entryAbsPath)
} }
if nil != err { if nil != err {
ret.Code = -1 ret.Code = -1
@ -71,9 +85,23 @@ func unzip(c *gin.Context) {
return return
} }
zipPath := arg["zipPath"].(string) zipFilePath := arg["zipPath"].(string)
path := arg["path"].(string) zipAbsFilePath, err := util.GetAbsPathInWorkspace(zipFilePath)
if err := gulu.Zip.Unzip(zipPath, path); nil != err { if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
entryPath := arg["path"].(string)
entryAbsPath, err := util.GetAbsPathInWorkspace(entryPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := gulu.Zip.Unzip(zipAbsFilePath, entryAbsPath); nil != err {
ret.Code = -1 ret.Code = -1
ret.Msg = err.Error() ret.Msg = err.Error()
return return

View File

@ -90,38 +90,45 @@ func getFile(c *gin.Context) {
} }
filePath := arg["path"].(string) filePath := arg["path"].(string)
filePath = filepath.Join(util.WorkspaceDir, filePath) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
info, err := os.Stat(filePath) if nil != err {
ret.Code = http.StatusForbidden
ret.Msg = err.Error()
c.JSON(http.StatusAccepted, ret)
return
}
info, err := os.Stat(fileAbsPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
ret.Code = 404 ret.Code = http.StatusNotFound
ret.Msg = err.Error()
c.JSON(http.StatusAccepted, ret) c.JSON(http.StatusAccepted, ret)
return return
} }
if nil != err { if nil != err {
logging.LogErrorf("stat [%s] failed: %s", filePath, err) logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
c.JSON(http.StatusAccepted, ret) c.JSON(http.StatusAccepted, ret)
return return
} }
if info.IsDir() { if info.IsDir() {
logging.LogErrorf("file [%s] is a directory", filePath) logging.LogErrorf("file [%s] is a directory", fileAbsPath)
ret.Code = 405 ret.Code = http.StatusMethodNotAllowed
ret.Msg = "file is a directory" ret.Msg = "file is a directory"
c.JSON(http.StatusAccepted, ret) c.JSON(http.StatusAccepted, ret)
return return
} }
data, err := filelock.ReadFile(filePath) data, err := filelock.ReadFile(fileAbsPath)
if nil != err { if nil != err {
logging.LogErrorf("read file [%s] failed: %s", filePath, err) logging.LogErrorf("read file [%s] failed: %s", fileAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
c.JSON(http.StatusAccepted, ret) c.JSON(http.StatusAccepted, ret)
return return
} }
contentType := mime.TypeByExtension(filepath.Ext(filePath)) contentType := mime.TypeByExtension(filepath.Ext(fileAbsPath))
if "" == contentType { if "" == contentType {
if m := mimetype.Detect(data); nil != m { if m := mimetype.Detect(data); nil != m {
contentType = m.String() contentType = m.String()
@ -144,40 +151,46 @@ func readDir(c *gin.Context) {
} }
dirPath := arg["path"].(string) dirPath := arg["path"].(string)
dirPath = filepath.Join(util.WorkspaceDir, dirPath) dirAbsPath, err := util.GetAbsPathInWorkspace(dirPath)
info, err := os.Stat(dirPath) if nil != err {
ret.Code = http.StatusForbidden
ret.Msg = err.Error()
return
}
info, err := os.Stat(dirAbsPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
ret.Code = 404 ret.Code = http.StatusNotFound
ret.Msg = err.Error()
return return
} }
if nil != err { if nil != err {
logging.LogErrorf("stat [%s] failed: %s", dirPath, err) logging.LogErrorf("stat [%s] failed: %s", dirAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
if !info.IsDir() { if !info.IsDir() {
logging.LogErrorf("file [%s] is not a directory", dirPath) logging.LogErrorf("file [%s] is not a directory", dirAbsPath)
ret.Code = 405 ret.Code = http.StatusMethodNotAllowed
ret.Msg = "file is not a directory" ret.Msg = "file is not a directory"
return return
} }
entries, err := os.ReadDir(dirPath) entries, err := os.ReadDir(dirAbsPath)
if nil != err { if nil != err {
logging.LogErrorf("read dir [%s] failed: %s", dirPath, err) logging.LogErrorf("read dir [%s] failed: %s", dirAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
files := []map[string]interface{}{} files := []map[string]interface{}{}
for _, entry := range entries { for _, entry := range entries {
path := filepath.Join(dirPath, entry.Name()) path := filepath.Join(dirAbsPath, entry.Name())
info, err = os.Stat(path) info, err = os.Stat(path)
if nil != err { if nil != err {
logging.LogErrorf("stat [%s] failed: %s", path, err) logging.LogErrorf("stat [%s] failed: %s", path, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
@ -202,25 +215,36 @@ func renameFile(c *gin.Context) {
return return
} }
filePath := arg["path"].(string) srcPath := arg["path"].(string)
filePath = filepath.Join(util.WorkspaceDir, filePath) srcAbsPath, err := util.GetAbsPathInWorkspace(srcPath)
if !filelock.IsExist(filePath) { if nil != err {
ret.Code = 404 ret.Code = http.StatusForbidden
ret.Msg = err.Error()
return
}
if !filelock.IsExist(srcAbsPath) {
ret.Code = http.StatusNotFound
ret.Msg = "the [path] file or directory does not exist" ret.Msg = "the [path] file or directory does not exist"
return return
} }
newPath := arg["newPath"].(string) destPath := arg["newPath"].(string)
newPath = filepath.Join(util.WorkspaceDir, newPath) destAbsPath, err := util.GetAbsPathInWorkspace(destPath)
if filelock.IsExist(newPath) { if nil != err {
ret.Code = 409 ret.Code = http.StatusForbidden
ret.Msg = err.Error()
c.JSON(http.StatusAccepted, ret)
return
}
if filelock.IsExist(destAbsPath) {
ret.Code = http.StatusConflict
ret.Msg = "the [newPath] file or directory already exists" ret.Msg = "the [newPath] file or directory already exists"
return return
} }
if err := filelock.Rename(filePath, newPath); nil != err { if err := filelock.Rename(srcAbsPath, destAbsPath); nil != err {
logging.LogErrorf("rename file [%s] to [%s] failed: %s", filePath, newPath, err) logging.LogErrorf("rename file [%s] to [%s] failed: %s", srcAbsPath, destAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
@ -237,22 +261,27 @@ func removeFile(c *gin.Context) {
} }
filePath := arg["path"].(string) filePath := arg["path"].(string)
filePath = filepath.Join(util.WorkspaceDir, filePath) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
_, err := os.Stat(filePath) if nil != err {
ret.Code = http.StatusForbidden
ret.Msg = err.Error()
return
}
_, err = os.Stat(fileAbsPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
ret.Code = 404 ret.Code = http.StatusNotFound
return return
} }
if nil != err { if nil != err {
logging.LogErrorf("stat [%s] failed: %s", filePath, err) logging.LogErrorf("stat [%s] failed: %s", fileAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
if err = filelock.Remove(filePath); nil != err { if err = filelock.Remove(fileAbsPath); nil != err {
logging.LogErrorf("remove [%s] failed: %s", filePath, err) logging.LogErrorf("remove [%s] failed: %s", fileAbsPath, err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }
@ -262,30 +291,36 @@ func putFile(c *gin.Context) {
ret := gulu.Ret.NewResult() ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret) defer c.JSON(http.StatusOK, ret)
var err error
filePath := c.PostForm("path") filePath := c.PostForm("path")
filePath = filepath.Join(util.WorkspaceDir, filePath) fileAbsPath, err := util.GetAbsPathInWorkspace(filePath)
if nil != err {
ret.Code = http.StatusForbidden
ret.Msg = err.Error()
return
}
isDirStr := c.PostForm("isDir") isDirStr := c.PostForm("isDir")
isDir, _ := strconv.ParseBool(isDirStr) isDir, _ := strconv.ParseBool(isDirStr)
var err error
if isDir { if isDir {
err = os.MkdirAll(filePath, 0755) err = os.MkdirAll(fileAbsPath, 0755)
if nil != err { if nil != err {
logging.LogErrorf("make a dir [%s] failed: %s", filePath, err) logging.LogErrorf("make dir [%s] failed: %s", fileAbsPath, err)
} }
} else { } else {
fileHeader, _ := c.FormFile("file") fileHeader, _ := c.FormFile("file")
if nil == fileHeader { if nil == fileHeader {
logging.LogErrorf("form file is nil [path=%s]", filePath) logging.LogErrorf("form file is nil [path=%s]", fileAbsPath)
ret.Code = 400 ret.Code = http.StatusBadRequest
ret.Msg = "form file is nil" ret.Msg = "form file is nil"
return return
} }
for { for {
dir := filepath.Dir(filePath) dir := filepath.Dir(fileAbsPath)
if err = os.MkdirAll(dir, 0755); nil != err { if err = os.MkdirAll(dir, 0755); nil != err {
logging.LogErrorf("put a file [%s] make dir [%s] failed: %s", filePath, dir, err) logging.LogErrorf("put file [%s] make dir [%s] failed: %s", fileAbsPath, dir, err)
break break
} }
@ -303,9 +338,9 @@ func putFile(c *gin.Context) {
break break
} }
err = filelock.WriteFile(filePath, data) err = filelock.WriteFile(fileAbsPath, data)
if nil != err { if nil != err {
logging.LogErrorf("put a file [%s] failed: %s", filePath, err) logging.LogErrorf("write file [%s] failed: %s", fileAbsPath, err)
break break
} }
break break
@ -323,15 +358,15 @@ func putFile(c *gin.Context) {
modTimeInt, parseErr := strconv.ParseInt(modTimeStr, 10, 64) modTimeInt, parseErr := strconv.ParseInt(modTimeStr, 10, 64)
if nil != parseErr { if nil != parseErr {
logging.LogErrorf("parse mod time [%s] failed: %s", modTimeStr, parseErr) logging.LogErrorf("parse mod time [%s] failed: %s", modTimeStr, parseErr)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = parseErr.Error() ret.Msg = parseErr.Error()
return return
} }
modTime = millisecond2Time(modTimeInt) modTime = millisecond2Time(modTimeInt)
} }
if err = os.Chtimes(filePath, modTime, modTime); nil != err { if err = os.Chtimes(fileAbsPath, modTime, modTime); nil != err {
logging.LogErrorf("change time failed: %s", err) logging.LogErrorf("change time failed: %s", err)
ret.Code = 500 ret.Code = http.StatusInternalServerError
ret.Msg = err.Error() ret.Msg = err.Error()
return return
} }

View File

@ -265,3 +265,12 @@ func IsDisplayableAsset(p string) bool {
} }
return false return false
} }
func GetAbsPathInWorkspace(relPath string) (string, error) {
absPath := filepath.Join(WorkspaceDir, relPath)
if IsSubPath(WorkspaceDir, absPath) {
return absPath, nil
} else {
return "", os.ErrPermission
}
}