Skip to content

Commit

Permalink
Use API for most of UI interactions with an internal API key (#211), …
Browse files Browse the repository at this point in the history
…breaking API changes

* Changed color of API procressing status

* Added internal system key, use system key for API page perm changes, BREAKING: removed session auth

* API: Added /auth/delete, added option to include basic parameters in /auth/create, migrated API overview page completely to use API calls

* Added toast in API menu for clipboard, removed reference to session authentication in documentation

* Changed delete button in upload menu to API call, fixed modify API call in menu #210

* Set Api key in API menu for changing permissions, renamed title for Deleting Uploads permission, refactoring

* Added tests, refactoring
  • Loading branch information
Forceu authored Dec 5, 2024
1 parent 5a155f7 commit ddc72b0
Show file tree
Hide file tree
Showing 28 changed files with 573 additions and 292 deletions.
2 changes: 1 addition & 1 deletion build/go-generate/minifyStaticContent.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,6 @@ func fileExists(filename string) bool {
// Auto-generated content below, do not modify
// Version codes can be changed in updateVersionNumbers.go

const jsAdminVersion = 5
const jsAdminVersion = 6
const jsE2EVersion = 3
const cssMainVersion = 2
2 changes: 1 addition & 1 deletion build/go-generate/updateVersionNumbers.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"strings"
)

const versionJsAdmin = 5
const versionJsAdmin = 6
const versionJsDropzone = 4
const versionJsE2EAdmin = 3
const versionCssMain = 2
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Interacting with the API
============================


All API calls will need an API key as authentication or a valid admin session cookie. An API key can be generated in the web UI in the menu "API". The API key needs to be passed as a header.
All API calls will need an API key as authentication. An API key can be generated in the web UI in the menu "API". The API key needs to be passed as a header.

Example: Getting a list of all stored files with curl
::
Expand Down
3 changes: 0 additions & 3 deletions docs/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,7 @@ Only use this if you are running Gokapi behind a reverse proxy that is capable o
This option disables Gokapis internal authentication completely, except for API calls. The following URLs need to be restricted by the reverse proxy:

- ``/admin``
- ``/apiDelete``
- ``/apiKeys``
- ``/apiNew``
- ``/delete``
- ``/e2eInfo``
- ``/e2eSetup``
- ``/logs``
Expand Down
5 changes: 5 additions & 0 deletions internal/configuration/database/Database.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ func DeleteApiKey(id string) {
db.DeleteApiKey(id)
}

// GetSystemKey returns the latest UI API key
func GetSystemKey() (models.ApiKey, bool) {
return db.GetSystemKey()
}

// E2E Section

// SaveEnd2EndInfo stores the encrypted e2e info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type Database interface {
UpdateTimeApiKey(apikey models.ApiKey)
// DeleteApiKey deletes an API key with the given ID
DeleteApiKey(id string)
// GetSystemKey returns the latest UI API key
GetSystemKey() (models.ApiKey, bool)

// SaveEnd2EndInfo stores the encrypted e2e info
SaveEnd2EndInfo(info models.E2EInfoEncrypted)
Expand Down
23 changes: 23 additions & 0 deletions internal/configuration/database/provider/redis/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,32 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
return apikey, true
}

// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
keys := p.GetAllApiKeys()
foundKey := ""
var latestExpiry int64
for _, key := range keys {
if !key.IsSystemKey {
continue
}
if key.Expiry > latestExpiry {
foundKey = key.Id
latestExpiry = key.Expiry
}
}
if foundKey == "" {
return models.ApiKey{}, false
}
return keys[foundKey], true
}

// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
p.setHashMap(p.buildArgs(prefixApiKeys + apikey.Id).AddFlat(apikey))
if apikey.Expiry != 0 {
p.setExpiryAt(prefixApiKeys+apikey.Id, apikey.Expiry)
}
}

// UpdateTimeApiKey writes the content of LastUsage to the database
Expand Down
13 changes: 12 additions & 1 deletion internal/configuration/database/provider/sqlite/Sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type DatabaseProvider struct {
sqliteDb *sql.DB
}

const DatabaseSchemeVersion = 3
const DatabaseSchemeVersion = 4

// New returns an instance
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
Expand Down Expand Up @@ -57,6 +57,14 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
ALTER TABLE "ApiKeys_New" RENAME TO "ApiKeys";`)
helper.Check(err)
}
// < v1.9.0
if currentDbVersion < 4 {
// Add Column LastUsedString, keeping old data
err := p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN "Expiry" INTEGER;`)
helper.Check(err)
err = p.rawSqlite(`ALTER TABLE "ApiKeys" ADD COLUMN "IsSystemKey" INTEGER;`)
helper.Check(err)
}
}

// GetDbVersion gets the version number of the database
Expand Down Expand Up @@ -125,6 +133,7 @@ func (p DatabaseProvider) Close() {
func (p DatabaseProvider) RunGarbageCollection() {
p.cleanExpiredSessions()
p.cleanUploadStatus()
p.cleanApiKeys()
}

func (p DatabaseProvider) createNewDatabase() error {
Expand All @@ -133,6 +142,8 @@ func (p DatabaseProvider) createNewDatabase() error {
"FriendlyName" TEXT NOT NULL,
"LastUsed" INTEGER NOT NULL,
"Permissions" INTEGER NOT NULL DEFAULT 0,
"Expiry" INTEGER,
"IsSystemKey" INTEGER,
PRIMARY KEY("Id")
) WITHOUT ROWID;
CREATE TABLE "E2EConfig" (
Expand Down
49 changes: 44 additions & 5 deletions internal/configuration/database/provider/sqlite/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ type schemaApiKeys struct {
FriendlyName string
LastUsed int64
Permissions int
Expiry int64
IsSystemKey int
}

// GetAllApiKeys returns a map with all API keys
func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
result := make(map[string]models.ApiKey)

rows, err := p.sqliteDb.Query("SELECT * FROM ApiKeys")
rows, err := p.sqliteDb.Query("SELECT * FROM ApiKeys WHERE ApiKeys.Expiry == 0 OR ApiKeys.Expiry > ?", currentTime().Unix())
helper.Check(err)
defer rows.Close()
for rows.Next() {
rowData := schemaApiKeys{}
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions)
err = rows.Scan(&rowData.Id, &rowData.FriendlyName, &rowData.LastUsed, &rowData.Permissions, &rowData.Expiry, &rowData.IsSystemKey)
helper.Check(err)
result[rowData.Id] = models.ApiKey{
Id: rowData.Id,
FriendlyName: rowData.FriendlyName,
LastUsed: rowData.LastUsed,
Permissions: uint8(rowData.Permissions),
Expiry: rowData.Expiry,
IsSystemKey: rowData.IsSystemKey == 1,
}
}
return result
Expand All @@ -39,7 +43,7 @@ func (p DatabaseProvider) GetAllApiKeys() map[string]models.ApiKey {
func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE Id = ?", id)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions)
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.ApiKey{}, false
Expand All @@ -53,15 +57,45 @@ func (p DatabaseProvider) GetApiKey(id string) (models.ApiKey, bool) {
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
}

return result, true
}

// GetSystemKey returns the latest UI API key
func (p DatabaseProvider) GetSystemKey() (models.ApiKey, bool) {
var rowResult schemaApiKeys
row := p.sqliteDb.QueryRow("SELECT * FROM ApiKeys WHERE IsSystemKey = 1 ORDER BY Expiry DESC LIMIT 1")
err := row.Scan(&rowResult.Id, &rowResult.FriendlyName, &rowResult.LastUsed, &rowResult.Permissions, &rowResult.Expiry, &rowResult.IsSystemKey)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return models.ApiKey{}, false
}
helper.Check(err)
return models.ApiKey{}, false
}

result := models.ApiKey{
Id: rowResult.Id,
FriendlyName: rowResult.FriendlyName,
LastUsed: rowResult.LastUsed,
Permissions: uint8(rowResult.Permissions),
Expiry: rowResult.Expiry,
IsSystemKey: rowResult.IsSystemKey == 1,
}
return result, true
}

// SaveApiKey saves the API key to the database
func (p DatabaseProvider) SaveApiKey(apikey models.ApiKey) {
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions) VALUES (?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions)
isSystemKey := 0
if apikey.IsSystemKey {
isSystemKey = 1
}
_, err := p.sqliteDb.Exec("INSERT OR REPLACE INTO ApiKeys (Id, FriendlyName, LastUsed, Permissions, Expiry, IsSystemKey) VALUES (?, ?, ?, ?, ?, ?)",
apikey.Id, apikey.FriendlyName, apikey.LastUsed, apikey.Permissions, apikey.Expiry, isSystemKey)
helper.Check(err)
}

Expand All @@ -77,3 +111,8 @@ func (p DatabaseProvider) DeleteApiKey(id string) {
_, err := p.sqliteDb.Exec("DELETE FROM ApiKeys WHERE Id = ?", id)
helper.Check(err)
}

func (p DatabaseProvider) cleanApiKeys() {
_, err := p.sqliteDb.Exec("DELETE FROM ApiKeys WHERE ApiKeys.Expiry > 0 AND ApiKeys.Expiry < ?", currentTime().Unix())
helper.Check(err)
}
2 changes: 1 addition & 1 deletion internal/configuration/setup/ProtectedUrls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ package setup

// protectedUrls contains a list of URLs that need to be protected if authentication is disabled.
// This list will be displayed during the setup
var protectedUrls = []string{"/admin", "/apiDelete", "/apiKeys", "/apiNew", "/delete", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
var protectedUrls = []string{"/admin", "/apiKeys", "/e2eInfo", "/e2eSetup", "/logs", "/uploadChunk", "/uploadComplete", "/uploadStatus"}
9 changes: 6 additions & 3 deletions internal/models/Api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ const (
// ApiPermNone means no permission granted
const ApiPermNone = 0

// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod
const ApiPermAllNoApiMod = 23

// ApiPermAll means all permission granted
const ApiPermAll = 31

// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod
// This is the default for new API keys that are created from the UI
const ApiPermAllNoApiMod = ApiPermAll - ApiPermApiMod

// ApiKey contains data of a single api key
type ApiKey struct {
Id string `json:"Id" redis:"Id"`
FriendlyName string `json:"FriendlyName" redis:"FriendlyName"`
LastUsed int64 `json:"LastUsed" redis:"LastUsed"`
Permissions uint8 `json:"Permissions" redis:"Permissions"`
Expiry int64 `json:"Expiry" redis:"Expiry"` // Does not expire if 0
IsSystemKey bool `json:"IsSystemKey" redis:"IsSystemKey"`
}

func (key *ApiKey) GetReadableDate() string {
Expand Down
9 changes: 4 additions & 5 deletions internal/models/FileList.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,16 @@ func (f *File) ToJsonResult(serverUrl string, includeFilename bool) string {
if err != nil {
return errorAsJson(err)
}
result := Result{

byteOutput, err := json.Marshal(Result{
Result: "OK",
IncludeFilename: includeFilename,
FileInfo: info,
}

bytes, err := json.Marshal(result)
})
if err != nil {
return errorAsJson(err)
}
return string(bytes)
return string(byteOutput)
}

// RequiresClientDecryption checks if the file needs to be decrypted by the client
Expand Down
31 changes: 2 additions & 29 deletions internal/webserver/Webserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,8 @@ func Start() {

mux.HandleFunc("/admin", requireLogin(showAdminMenu, false))
mux.HandleFunc("/api/", processApi)
mux.HandleFunc("/apiDelete", requireLogin(deleteApiKey, false))
mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, false))
mux.HandleFunc("/apiNew", requireLogin(newApiKey, false))
mux.HandleFunc("/d", showDownload)
mux.HandleFunc("/delete", requireLogin(deleteFile, false))
mux.HandleFunc("/downloadFile", downloadFile)
mux.HandleFunc("/e2eInfo", requireLogin(e2eInfo, true))
mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, false))
Expand Down Expand Up @@ -302,21 +299,6 @@ func showApiAdmin(w http.ResponseWriter, r *http.Request) {
helper.CheckIgnoreTimeout(err)
}

// Handling of /apiNew
func newApiKey(w http.ResponseWriter, r *http.Request) {
api.NewKey(true)
redirect(w, "apiKeys")
}

// Handling of /apiDelete
func deleteApiKey(w http.ResponseWriter, r *http.Request) {
keys, ok := r.URL.Query()["id"]
if ok {
api.DeleteKey(keys[0])
}
redirect(w, "apiKeys")
}

// Handling of /api/
func processApi(w http.ResponseWriter, r *http.Request) {
api.Process(w, r, configuration.Get().MaxMemory)
Expand Down Expand Up @@ -505,17 +487,6 @@ func getE2eInfo(w http.ResponseWriter) {
_, _ = w.Write(bytesE2e)
}

// Handling of /delete
// User needs to be admin. Deletes the requested file
func deleteFile(w http.ResponseWriter, r *http.Request) {
keyId := queryUrl(w, r, "admin")
if keyId == "" {
return
}
storage.DeleteFile(keyId, true)
redirect(w, "admin")
}

// Checks if a file is associated with the GET parameter from the current URL
// Stops for 500ms to limit brute forcing if invalid key and redirects to redirectUrl
func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string {
Expand Down Expand Up @@ -593,6 +564,7 @@ type UploadView struct {
DefaultPassword string
Logs string
PublicName string
SystemKey string
IsAdminView bool
IsDownloadView bool
IsApiView bool
Expand Down Expand Up @@ -678,6 +650,7 @@ func (u *UploadView) convertGlobalConfig(view int) *UploadView {
u.MaxParallelUploads = config.MaxParallelUploads
u.ChunkSize = config.ChunkSize
u.IncludeFilename = config.IncludeFilename
u.SystemKey = api.GetSystemKey()
return u
}

Expand Down
Loading

0 comments on commit ddc72b0

Please sign in to comment.