Check that env can only be positive, add UR limites for non-admin users, refactoring

This commit is contained in:
Marc Ole Bulling
2026-01-24 21:02:08 +01:00
parent 548cd62f27
commit c3cf344fb6
12 changed files with 178 additions and 34 deletions
+1 -1
View File
@@ -299,7 +299,7 @@ func DeleteUser(id int) {
func GetSuperAdmin() (models.User, bool) {
users := db.GetAllUsers()
for _, user := range users {
if user.UserLevel == models.UserLevelSuperAdmin {
if user.IsSuperAdmin() {
return user, true
}
}
@@ -124,8 +124,9 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
}
// < v2.2.0
if currentDbVersion < 6 {
if environment.New().PermRequestGrantedByDefault {
for _, user := range p.GetAllUsers() {
grantUploadPerm := environment.New().PermRequestGrantedByDefault
for _, user := range p.GetAllUsers() {
if grantUploadPerm || user.IsAdmin() {
user.GrantPermission(models.UserPermGuestUploads)
p.SaveUser(user, false)
}
@@ -71,8 +71,9 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
PRIMARY KEY("id")
);`)
helper.Check(err)
if environment.New().PermRequestGrantedByDefault {
for _, user := range p.GetAllUsers() {
grantUploadPerm := environment.New().PermRequestGrantedByDefault
for _, user := range p.GetAllUsers() {
if grantUploadPerm || user.IsAdmin() {
user.GrantPermission(models.UserPermGuestUploads)
p.SaveUser(user, false)
}
@@ -82,12 +83,6 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
p.DeleteApiKey(apiKey.Id)
}
}
for _, user := range p.GetAllUsers() {
if user.UserLevel != models.UserLevelUser {
user.GrantPermission(models.UserPermGuestUploads)
}
p.SaveUser(user, false)
}
}
}
+64 -9
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"os"
"path"
"reflect"
"strconv"
envParser "github.com/caarlos0/env/v6"
"github.com/forceu/gokapi/internal/environment/deprecation"
@@ -20,15 +22,17 @@ type Environment struct {
ConfigFile string `env:"CONFIG_FILE" envDefault:"config.json"`
ConfigPath string
DataDir string `env:"DATA_DIR" envDefault:"data"`
ChunkSizeMB int `env:"CHUNK_SIZE_MB" envDefault:"45"`
LengthId int `env:"LENGTH_ID" envDefault:"15"`
LengthHotlinkId int `env:"LENGTH_HOTLINK_ID" envDefault:"40"`
MaxFileSize int `env:"MAX_FILESIZE" envDefault:"102400"` // 102400==100GB
MaxMemory int `env:"MAX_MEMORY_UPLOAD" envDefault:"50"`
MaxParallelUploads int `env:"MAX_PARALLEL_UPLOADS" envDefault:"3"`
MinFreeSpaceMB int `env:"MIN_FREE_SPACE" envDefault:"400"`
MinLengthPassword int `env:"MIN_LENGTH_PASSWORD" envDefault:"8"`
WebserverPort int `env:"PORT" envDefault:"53842"`
ChunkSizeMB int `env:"CHUNK_SIZE_MB" envDefault:"45" onlyPositive:"true"`
LengthId int `env:"LENGTH_ID" envDefault:"15" onlyPositive:"true"`
LengthHotlinkId int `env:"LENGTH_HOTLINK_ID" envDefault:"40" onlyPositive:"true"`
MaxFileSize int `env:"MAX_FILESIZE" envDefault:"102400" onlyPositive:"true"` // 102400 = 100GB
MaxMemory int `env:"MAX_MEMORY_UPLOAD" envDefault:"50" onlyPositive:"true"`
MaxParallelUploads int `env:"MAX_PARALLEL_UPLOADS" envDefault:"3" onlyPositive:"true"`
MaxFilesGuestUpload int `env:"MAX_FILES_GUESTUPLOAD" envDefault:"100" onlyPositive:"true"`
MaxSizeGuestUploadMb int `env:"MAX_SIZE_GUESTUPLOAD" envDefault:"10240" onlyPositive:"true"` // 10240 = 10GB
MinFreeSpaceMB int `env:"MIN_FREE_SPACE" envDefault:"400" onlyPositive:"true"`
MinLengthPassword int `env:"MIN_LENGTH_PASSWORD" envDefault:"8" onlyPositive:"true"`
WebserverPort int `env:"PORT" envDefault:"53842" onlyPositive:"true"`
DisableCorsCheck bool `env:"DISABLE_CORS_CHECK" envDefault:"false"`
LogToStdout bool `env:"LOG_STDOUT" envDefault:"false"`
HotlinkVideos bool `env:"ENABLE_HOTLINK_VIDEOS" envDefault:"false"`
@@ -54,6 +58,11 @@ func New() Environment {
}
result = parseEnvVars(result)
err := enforceOnlyPositiveDefaults(&result)
if err != nil {
fmt.Println("Error parsing env variables:", err)
osExit(1)
}
result = parseFlags(result)
result.ActiveDeprecations = deprecation.GetActive()
@@ -87,9 +96,55 @@ func parseEnvVars(result Environment) Environment {
return result
}
func enforceOnlyPositiveDefaults(result *Environment) error {
v := reflect.ValueOf(result)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("env must be a pointer to a struct")
}
v = v.Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
fieldVal := v.Field(i)
fieldType := t.Field(i)
if fieldType.Tag.Get("onlyPositive") != "true" {
continue
}
// Only handle signed integers
switch fieldVal.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if fieldVal.Int() >= 0 {
continue
}
defaultStr := fieldType.Tag.Get("envDefault")
defaultVal, err := strconv.ParseInt(defaultStr, 10, fieldVal.Type().Bits())
if err != nil {
return fmt.Errorf("invalid envDefault for field %s: %w", fieldType.Name, err)
}
if fieldVal.CanSet() {
fieldVal.SetInt(defaultVal)
} else {
return fmt.Errorf("cannot set fieldval %s", fieldType.Name)
}
default:
continue
}
}
return nil
}
func parseFlags(result Environment) Environment {
flags := flagparser.ParseFlags()
if flags.IsPortSet {
if flags.Port < 1 {
flags.Port = DefaultPort
}
result.WebserverPort = flags.Port
}
if flags.IsConfigDirSet {
+5
View File
@@ -58,6 +58,11 @@ func (u *User) IsSuperAdmin() bool {
return u.UserLevel == UserLevelSuperAdmin
}
// IsAdmin returns true if the user has the Rank UserLevelSuperAdmin or UserLevelAdmin
func (u *User) IsAdmin() bool {
return u.UserLevel == UserLevelAdmin || u.UserLevel == UserLevelSuperAdmin
}
// IsSameUser returns true, if the user has the same ID
func (u *User) IsSameUser(userId int) bool {
return u.Id == userId
+7 -1
View File
@@ -642,7 +642,7 @@ func showAdminMenu(w http.ResponseWriter, r *http.Request) {
view := (&AdminView{}).convertGlobalConfig(ViewMain, user)
if len(configuration.GetEnvironment().ActiveDeprecations) > 0 {
if user.UserLevel == models.UserLevelSuperAdmin {
if user.IsSuperAdmin() {
view.ShowDeprecationNotice = true
}
}
@@ -736,6 +736,8 @@ type AdminView struct {
ChunkSize int
MaxParallelUploads int
MinLengthPassword int
FileRequestMaxFiles int
FileRequestMaxSize int
TimeNow int64
CustomContent customStatic
}
@@ -832,6 +834,10 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
}
fileRequest.Files = sortMetaData(fileRequest.Files)
u.FileRequests = append(u.FileRequests, fileRequest)
if !user.IsAdmin() {
u.FileRequestMaxFiles = configuration.GetEnvironment().MaxFilesGuestUpload
u.FileRequestMaxSize = configuration.GetEnvironment().MaxSizeGuestUploadMb
}
}
}
+35 -6
View File
@@ -43,7 +43,7 @@ func Process(w http.ResponseWriter, r *http.Request) {
sendError(w, http.StatusUnauthorized, "Unauthorized")
return
}
if routing.AdminOnly && (user.UserLevel != models.UserLevelAdmin && user.UserLevel != models.UserLevelSuperAdmin) {
if routing.AdminOnly && !user.IsAdmin() {
sendError(w, http.StatusUnauthorized, "Unauthorized")
return
}
@@ -375,7 +375,7 @@ func apiChunkReserve(w http.ResponseWriter, r requestParser, _ models.User) {
}
func apiChunkUploadRequestAdd(w http.ResponseWriter, r requestParser, _ models.User) {
func apiChunkUploadRequestAdd(w http.ResponseWriter, r requestParser, user models.User) {
request, ok := r.(*paramChunkUploadRequestAdd)
if !ok {
panic("invalid parameter passed")
@@ -386,10 +386,11 @@ func apiChunkUploadRequestAdd(w http.ResponseWriter, r requestParser, _ models.U
return
}
maxUpload := configuration.Get().MaxFileSizeMB
if !user.IsAdmin() && configuration.GetEnvironment().MaxSizeGuestUploadMb != 0 {
maxUpload = min(maxUpload, configuration.GetEnvironment().MaxSizeGuestUploadMb)
}
if !fileRequest.IsUnlimitedSize() {
if (fileRequest.MaxSize) < maxUpload {
maxUpload = fileRequest.MaxSize
}
maxUpload = min(maxUpload, fileRequest.MaxSize)
}
statusCode, errString := processNewChunk(w, request, maxUpload, fileRequest.Id)
if statusCode != http.StatusOK {
@@ -997,6 +998,28 @@ func apiURequestDelete(w http.ResponseWriter, r requestParser, user models.User)
_, _ = w.Write([]byte("{\"result\":\"OK\"}"))
}
func isUserAllowedUnlimited(request *paramURequestSave, isNewRequest bool, user models.User) bool {
if user.IsAdmin() {
return true
}
isServerLimitMaxSize := configuration.GetEnvironment().MaxSizeGuestUploadMb != 0
isServerLimitMaxFiles := configuration.GetEnvironment().MaxFilesGuestUpload != 0
if isServerLimitMaxSize {
if (request.IsMaxSizeSet || isNewRequest) &&
(request.MaxSizeMb == 0 || request.MaxSizeMb > configuration.GetEnvironment().MaxSizeGuestUploadMb) {
return false
}
}
if isServerLimitMaxFiles {
if (request.IsMaxFilesSet || isNewRequest) &&
(request.MaxFiles == 0 || request.MaxFiles > configuration.GetEnvironment().MaxFilesGuestUpload) {
return false
}
}
return true
}
func apiURequestSave(w http.ResponseWriter, r requestParser, user models.User) {
request, ok := r.(*paramURequestSave)
if !ok {
@@ -1005,6 +1028,12 @@ func apiURequestSave(w http.ResponseWriter, r requestParser, user models.User) {
uploadRequest := models.FileRequest{}
isNewRequest := request.Id == ""
if !isUserAllowedUnlimited(request, isNewRequest, user) {
sendError(w, http.StatusBadRequest, "Only admin users can create requests with unlimited size / file count"+
" or values larger than the server's max size / file count")
return
}
if !isNewRequest {
uploadRequest, ok = database.GetFileRequest(request.Id)
if !ok {
@@ -1035,7 +1064,7 @@ func apiURequestSave(w http.ResponseWriter, r requestParser, user models.User) {
uploadRequest.MaxFiles = request.MaxFiles
}
if request.IsMaxSizeSet {
uploadRequest.MaxSize = request.MaxSize
uploadRequest.MaxSize = request.MaxSizeMb
}
if request.IsNotesSet {
uploadRequest.Notes = request.Notes
+1 -1
View File
@@ -719,7 +719,7 @@ type paramURequestSave struct {
Notes string `header:"notes" supportBase64:"true"`
Expiry int64 `header:"expiry"`
MaxFiles int `header:"maxfiles"`
MaxSize int `header:"maxsize"`
MaxSizeMb int `header:"maxsize"`
IsNameSet bool
IsExpirySet bool
IsMaxFilesSet bool
+1 -1
View File
@@ -1280,7 +1280,7 @@ func (p *paramURequestSave) ParseRequest(r *http.Request) error {
}
p.foundHeaders["maxsize"] = exists
if exists {
p.MaxSize, err = parseHeaderInt(r, "maxsize")
p.MaxSizeMb, err = parseHeaderInt(r, "maxsize")
if err != nil {
return fmt.Errorf("invalid value in header maxsize supplied")
}
@@ -101,6 +101,41 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) {
document.getElementById("mFriendlyName").value = name;
}
if (limitMaxFiles != 0) {
let checkbox = document.getElementById("mc_maxfiles");
if (maxFiles === null || maxFiles == 0) {
maxFiles = limitMaxFiles;
}
checkbox.checked = true;
checkbox.disabled = true;
checkbox.title = "Only admins can set this to unlimited";
checkbox.value = "1";
document.getElementById("mi_maxfiles").setAttribute("max", limitMaxFiles);
} else {
let checkbox = document.getElementById("mc_maxfiles");
checkbox.disabled = false;
checkbox.title = "";
document.getElementById("mi_maxfiles").setAttribute("max", "");
}
if (limitMaxSize != 0) {
let checkbox = document.getElementById("mc_maxsize");
if (maxSize === null || maxSize == 0) {
maxSize = limitMaxSize;
}
checkbox.checked = true;
checkbox.disabled = true;
checkbox.title = "Only admins can set this to unlimited";
checkbox.value = "1";
document.getElementById("mi_maxsize").setAttribute("max", limitMaxSize);
} else {
let checkbox = document.getElementById("mc_maxsize");
checkbox.disabled = false;
checkbox.title = "";
document.getElementById("mi_maxsize").setAttribute("max", "");
}
if (maxFiles === null || maxFiles == 0) {
document.getElementById("mi_maxfiles").value = "1";
document.getElementById("mi_maxfiles").disabled = true;
@@ -111,6 +146,7 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) {
document.getElementById("mc_maxfiles").checked = true;
}
if (maxSize === null || maxSize == 0) {
document.getElementById("mi_maxsize").value = "10";
document.getElementById("mi_maxsize").disabled = true;
@@ -133,7 +169,7 @@ function setModalValues(id, name, maxFiles, maxSize, expiry, notes) {
document.getElementById("mc_expiry").checked = true;
createCalendar("mi_expiry", expiry);
}
document.getElementById("mNotes").value = notes;
document.getElementById("mNotes").value = notes;
}
function editFileRequest(id, name, maxFiles, maxSize, expiry, notes) {
@@ -180,6 +216,20 @@ function saveFileRequest() {
});
}
function checkMaxNumber(element) {
if (element.value == "") {
element.value = "1";
return;
}
let maxVal = element.getAttribute("max");
if (maxVal == "") {
return;
}
if (element.value > maxVal) {
element.value = maxVal;
}
}
function insertOrReplaceFileRequest(jsonResult) {
const tbody = document.getElementById("filerequesttable");
let row = document.getElementById(`row-${jsonResult.id}`);
File diff suppressed because one or more lines are too long
@@ -110,7 +110,10 @@
<script>
var userName = "{{.ActiveUser.Name}}";
var baseUrl = "{{.ServerUrl}}";
var canViewOtherRequests = {{.ActiveUser.HasPermissionListOtherUploads }};
var canViewOtherRequests = {{.ActiveUser.HasPermissionListOtherUploads}};
var limitMaxSize = {{.FileRequestMaxSize}};
var limitMaxFiles = {{.FileRequestMaxFiles}};
</script>
@@ -166,7 +169,7 @@
<input type="checkbox" id="mc_maxfiles" aria-label="Max files" title="Max files" data-toggle-target="mi_maxfiles" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="mMaxFiles">Max Files</span>
<input type="number" min="1" id="mi_maxfiles" disabled class="form-control" aria-label="Max Files" aria-describedby="mMaxFiles" data-allow-regular-paste>
<input type="number" min="1" id="mi_maxfiles" onChange="checkMaxNumber(this)" disabled class="form-control" aria-label="Max Files" aria-describedby="mMaxFiles" data-allow-regular-paste>
</div>
<div class="input-group mb-3">
@@ -175,7 +178,7 @@
data-toggle-target="mi_maxsize" onchange="handleEditCheckboxChange(this)">
</div>
<span class="input-group-text" id="tMaxSize">Max Size&nbsp;</span>
<input type="number" min="1" id="mi_maxsize" disabled class="form-control" aria-label="Max Size" aria-describedby="tMaxSize" data-allow-regular-paste>
<input type="number" min="1" id="mi_maxsize" onChange="checkMaxNumber(this)" disabled class="form-control" aria-label="Max Size" aria-describedby="tMaxSize" data-allow-regular-paste>
</div>
<div class="input-group mb-3">