diff --git a/internal/configuration/configupgrade/Upgrade.go b/internal/configuration/configupgrade/Upgrade.go index 2d0e093..b98ae00 100644 --- a/internal/configuration/configupgrade/Upgrade.go +++ b/internal/configuration/configupgrade/Upgrade.go @@ -71,26 +71,25 @@ func updateConfig(settings *models.Configuration, env *environment.Environment) fmt.Println("--> See the release notes for more information") osExit(1) return - } else { - fmt.Println("Setting admin user to " + adminUser) - settings.Authentication.Username = adminUser } - if settings.Authentication.Method == models.AuthenticationOAuth2 && len(oldAuth.Authentication.OAuthUsers) > 0 { - LegacyUsersHeaderOauth = oldAuth.Authentication.OAuthUsers - settings.Authentication.OnlyRegisteredUsers = true - } - if settings.Authentication.Method == models.AuthenticationHeader && len(oldAuth.Authentication.HeaderUsers) > 0 { - LegacyUsersHeaderOauth = oldAuth.Authentication.HeaderUsers - settings.Authentication.OnlyRegisteredUsers = true - } - for _, user := range LegacyUsersHeaderOauth { - if strings.Contains(user, "*") { - fmt.Println("FAILED UPDATE") - fmt.Println("--> If using Oauth or Header authentication and restricting the users, please remove any wildcards before upgrading.") - fmt.Println("--> See the release notes for more information") - osExit(1) - return - } + fmt.Println("Setting admin user to " + adminUser) + settings.Authentication.Username = adminUser + } + if settings.Authentication.Method == models.AuthenticationOAuth2 && len(oldAuth.Authentication.OAuthUsers) > 0 { + LegacyUsersHeaderOauth = oldAuth.Authentication.OAuthUsers + settings.Authentication.OnlyRegisteredUsers = true + } + if settings.Authentication.Method == models.AuthenticationHeader && len(oldAuth.Authentication.HeaderUsers) > 0 { + LegacyUsersHeaderOauth = oldAuth.Authentication.HeaderUsers + settings.Authentication.OnlyRegisteredUsers = true + } + for _, user := range LegacyUsersHeaderOauth { + if strings.Contains(user, "*") { + fmt.Println("FAILED UPDATE") + fmt.Println("--> If using Oauth or Header authentication and restricting the users, please remove any wildcards before upgrading.") + fmt.Println("--> See the release notes for more information") + osExit(1) + return } } } diff --git a/internal/logging/Logging.go b/internal/logging/Logging.go index 83d4b13..b1b4cad 100644 --- a/internal/logging/Logging.go +++ b/internal/logging/Logging.go @@ -39,9 +39,8 @@ func GetAll() (string, bool) { content, err := os.ReadFile(logPath) helper.Check(err) return string(content), true - } else { - return fmt.Sprintf("[%s] No log file found!", categoryWarning), false } + return fmt.Sprintf("[%s] No log file found!", categoryWarning), false } // createLogEntry adds a line to the logfile including the current date. Also outputs to Stdout if set. @@ -159,6 +158,8 @@ func UpgradeToV2() { defer mutex.Unlock() } +// DeleteLogs removes all logs before the cutoff timestamp and inserts a new log that the user +// deleted the previous logs func DeleteLogs(userName string, userId int, cutoff int64, r *http.Request) { if cutoff == 0 { deleteAllLogs(userName, userId, r) diff --git a/internal/models/ApiKey.go b/internal/models/ApiKey.go index 675d9b5..4df4796 100644 --- a/internal/models/ApiKey.go +++ b/internal/models/ApiKey.go @@ -45,6 +45,7 @@ type ApiKey struct { UserId int `json:"UserId" redis:"UserId"` } +// ApiPermission contains zero or more permissions as an uint8 format type ApiPermission uint8 // GetReadableDate returns the date as YYYY-MM-DD HH:MM:SS diff --git a/internal/models/FileList.go b/internal/models/FileList.go index 23bc822..21f47bc 100644 --- a/internal/models/FileList.go +++ b/internal/models/FileList.go @@ -32,27 +32,27 @@ type File struct { // FileApiOutput will be displayed for public outputs from the ID, hiding sensitive information type FileApiOutput struct { - Id string `json:"Id"` // The internal ID of the file - Name string `json:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files - Size string `json:"Size"` // Filesize in a human-readable format - HotlinkId string `json:"HotlinkId"` // If file is a picture file and can be hotlinked, this is the ID for the hotlink - ContentType string `json:"ContentType"` // The MIME type for the file - ExpireAtString string `json:"ExpireAtString"` // Time expiry in a human-readable format in local time - UrlDownload string `json:"UrlDownload"` // The public download URL for the file - UrlHotlink string `json:"UrlHotlink"` // The public hotlink URL for the file - UploadDate int64 `json:"UploadDate" redis:"UploadDate"` // UTC timestamp of upload time - ExpireAt int64 `json:"ExpireAt"` // "UTC timestamp of file expiry - SizeBytes int64 `json:"SizeBytes"` // Filesize in bytes - DownloadsRemaining int `json:"DownloadsRemaining"` // The remaining downloads for this file - DownloadCount int `json:"DownloadCount"` // The amount of times the file has been downloaded - UnlimitedDownloads bool `json:"UnlimitedDownloads"` // True if the uploader did not limit the downloads - UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time - RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side - IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted - IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted - IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file - IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage - UploaderId int `json:"UploaderId"` // The user ID of the uploader + Id string `json:"Id"` // The internal ID of the file + Name string `json:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files + Size string `json:"Size"` // Filesize in a human-readable format + HotlinkId string `json:"HotlinkId"` // If file is a picture file and can be hotlinked, this is the ID for the hotlink + ContentType string `json:"ContentType"` // The MIME type for the file + ExpireAtString string `json:"ExpireAtString"` // Time expiry in a human-readable format in local time + UrlDownload string `json:"UrlDownload"` // The public download URL for the file + UrlHotlink string `json:"UrlHotlink"` // The public hotlink URL for the file + UploadDate int64 `json:"UploadDate"` // UTC timestamp of upload time + ExpireAt int64 `json:"ExpireAt"` // "UTC timestamp of file expiry + SizeBytes int64 `json:"SizeBytes"` // Filesize in bytes + DownloadsRemaining int `json:"DownloadsRemaining"` // The remaining downloads for this file + DownloadCount int `json:"DownloadCount"` // The amount of times the file has been downloaded + UnlimitedDownloads bool `json:"UnlimitedDownloads"` // True if the uploader did not limit the downloads + UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time + RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side + IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted + IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted + IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file + IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage + UploaderId int `json:"UploaderId"` // The user ID of the uploader } // EncryptionInfo holds information about the encryption used on the file diff --git a/internal/models/User.go b/internal/models/User.go index 5ef0ded..3b474f3 100644 --- a/internal/models/User.go +++ b/internal/models/User.go @@ -6,8 +6,10 @@ import ( "time" ) +// UserPermission contains zero or more permissions as uint16 type UserPermission uint16 +// User contains information about the Gokapi user type User struct { Id int `json:"id" redis:"id"` Name string `json:"name" redis:"Name"` @@ -50,30 +52,51 @@ func (u *User) ToJson() string { return string(result) } +// UserLevelSuperAdmin indicates that this is the single user with the most permissions const UserLevelSuperAdmin UserRank = 0 + +// UserLevelAdmin indicates that this user has by default all permissions (unless they affect the super-admin) const UserLevelAdmin UserRank = 1 + +// UserLevelUser indicates that this user has only basic permissions by default const UserLevelUser UserRank = 2 +// UserRank indicates the rank that is assigned to the user type UserRank uint8 +// IsSuperAdmin returns true if the user has the Rank UserLevelSuperAdmin func (u *User) IsSuperAdmin() bool { return u.UserLevel == UserLevelSuperAdmin } + +// IsSameUser returns true, if the user has the same ID func (u *User) IsSameUser(userId int) bool { return u.Id == userId } const ( + // UserPermReplaceUploads allows to replace uploads UserPermReplaceUploads UserPermission = 1 << iota + // UserPermListOtherUploads allows to also list uploads by other users UserPermListOtherUploads + // UserPermEditOtherUploads allows editing of uploads by other users UserPermEditOtherUploads + // UserPermReplaceOtherUploads allows replacing of uploads by other users UserPermReplaceOtherUploads + // UserPermDeleteOtherUploads allows deleting uploads by other users UserPermDeleteOtherUploads + // UserPermManageLogs allows viewing and deleting logs UserPermManageLogs + // UserPermManageApiKeys allows editing and deleting of API keys by other users UserPermManageApiKeys + // UserPermManageUsers allows creating and editing of users, including granting and revoking permissions UserPermManageUsers ) + +// UserPermissionNone means that the user has no permissions const UserPermissionNone UserPermission = 0 + +// UserPermissionAll means that the user has all permissions const UserPermissionAll UserPermission = 255 // GrantPermission grants one or more permissions diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 470f6bc..25924d9 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -921,8 +921,7 @@ func requireLogin(next http.HandlerFunc, isUiCall, isPwChangeView bool) http.Han return } } - c := context.WithValue(r.Context(), "user", user) - r = r.WithContext(c) + r = authentication.SetUserInRequest(r, user) next.ServeHTTP(w, r) return } diff --git a/internal/webserver/api/Api.go b/internal/webserver/api/Api.go index a62da29..cdbed6a 100644 --- a/internal/webserver/api/Api.go +++ b/internal/webserver/api/Api.go @@ -373,10 +373,8 @@ func apiChunkComplete(w http.ResponseWriter, r requestParser, user models.User) go doBlockingPartCompleteChunk(nil, request, user) _, _ = io.WriteString(w, "{\"result\":\"OK\"}") return - } else { - doBlockingPartCompleteChunk(w, request, user) } - + doBlockingPartCompleteChunk(w, request, user) } func doBlockingPartCompleteChunk(w http.ResponseWriter, request *paramChunkComplete, user models.User) { diff --git a/internal/webserver/api/Api_test.go b/internal/webserver/api/Api_test.go index f4703c3..6ac8fc2 100644 --- a/internal/webserver/api/Api_test.go +++ b/internal/webserver/api/Api_test.go @@ -824,7 +824,7 @@ func getApiPermMap(t *testing.T) map[models.ApiPermission]string { result[models.ApiPermManageLogs] = "PERM_MANAGE_LOGS" sum := 0 - for perm, _ := range result { + for perm := range result { sum = sum + int(perm) } if sum != int(models.ApiPermAll) { @@ -846,7 +846,7 @@ func getUserPermMap(t *testing.T) map[models.UserPermission]string { result[models.UserPermManageUsers] = "PERM_USERS" sum := 0 - for perm, _ := range result { + for perm := range result { sum = sum + int(perm) } if sum != int(models.UserPermissionAll) { diff --git a/internal/webserver/authentication/Authentication.go b/internal/webserver/authentication/Authentication.go index 2d08413..58296ef 100644 --- a/internal/webserver/authentication/Authentication.go +++ b/internal/webserver/authentication/Authentication.go @@ -1,6 +1,7 @@ package authentication import ( + "context" "crypto/subtle" "encoding/json" "errors" @@ -18,9 +19,13 @@ import ( "strings" ) +type userNameContext string + // CookieOauth is the cookie name used for login const CookieOauth = "state" +const userNameContextKey userNameContext = "userName" + var authSettings models.AuthenticationConfig // Init needs to be called first to process the authentication configuration @@ -71,15 +76,22 @@ func checkAuthConfig(config models.AuthenticationConfig) error { } } +// GetUserFromRequest returns the user that has been authenticated with the request func GetUserFromRequest(r *http.Request) (models.User, error) { c := r.Context() - user, ok := c.Value("user").(models.User) + user, ok := c.Value(userNameContextKey).(models.User) if !ok { return models.User{}, errors.New("user not found in context") } return user, nil } +// SetUserInRequest saves the user that has been authenticated with the request +func SetUserInRequest(r *http.Request, user models.User) *http.Request { + c := context.WithValue(r.Context(), userNameContextKey, user) + return r.WithContext(c) +} + // IsAuthenticated returns true and the user ID if authenticated func IsAuthenticated(w http.ResponseWriter, r *http.Request) (models.User, bool) { switch authSettings.Method { diff --git a/internal/webserver/authentication/Authentication_test.go b/internal/webserver/authentication/Authentication_test.go index 6a0f58c..91d14e9 100644 --- a/internal/webserver/authentication/Authentication_test.go +++ b/internal/webserver/authentication/Authentication_test.go @@ -195,7 +195,7 @@ func TestGetUserFromRequest(t *testing.T) { _, r := test.GetRecorder("GET", "/", nil, nil, nil) _, err := GetUserFromRequest(r) test.IsNotNil(t, err) - c := context.WithValue(r.Context(), "user", "invalid") + c := context.WithValue(r.Context(), userNameContextKey, "invalid") rInvalid := r.WithContext(c) _, err = GetUserFromRequest(rInvalid) test.IsNotNil(t, err) @@ -210,8 +210,7 @@ func TestGetUserFromRequest(t *testing.T) { ResetPassword: true, } - c = context.WithValue(r.Context(), "user", user) - rValid := r.WithContext(c) + rValid := SetUserInRequest(r, user) retrievedUser, err := GetUserFromRequest(rValid) test.IsNil(t, err) test.IsEqual(t, retrievedUser, user)