Skip to content

Commit f36d39e

Browse files
authored
Added extended logging (#240), fixed bug that prevented setting Manage_Users API permission on new API key, added Manage_Logs API permission
* Added Manage_Logs API permission, added API endpoint to delete logs, added more logging, added filtering and deletion of logs in UI, fixed bug that prevented setting Manage_Users API permission on new API key
1 parent af5f46b commit f36d39e

File tree

26 files changed

+730
-377
lines changed

26 files changed

+730
-377
lines changed

build/go-generate/minifyStaticContent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,6 @@ func fileExists(filename string) bool {
137137
// Auto-generated content below, do not modify
138138
// Version codes can be changed in updateVersionNumbers.go
139139

140-
const jsAdminVersion = 8
140+
const jsAdminVersion = 9
141141
const jsE2EVersion = 5
142142
const cssMainVersion = 4

build/go-generate/updateVersionNumbers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"strings"
1212
)
1313

14-
const versionJsAdmin = 8
14+
const versionJsAdmin = 9
1515
const versionJsDropzone = 5
1616
const versionJsE2EAdmin = 5
1717
const versionCssMain = 4

cmd/gokapi/Main.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func main() {
7272
createSsl(passedFlags)
7373
initCloudConfig(passedFlags)
7474
go storage.CleanUp(true)
75-
logging.AddString("Gokapi started")
75+
logging.LogStartup()
7676
go webserver.Start()
7777

7878
c := make(chan os.Signal)
@@ -85,6 +85,7 @@ func main() {
8585
func shutdown() {
8686
fmt.Println("Shutting down...")
8787
webserver.Shutdown()
88+
logging.LogShutdown()
8889
database.Close()
8990
}
9091

@@ -164,6 +165,7 @@ func initCloudConfig(passedFlags flagparser.MainFlags) {
164165
// Checks for command line arguments that have to be parsed after loading the configuration
165166
func reconfigureServer(passedFlags flagparser.MainFlags) bool {
166167
if passedFlags.Reconfigure {
168+
logging.LogSetup()
167169
setup.RunConfigModification()
168170
return true
169171
}
@@ -210,7 +212,7 @@ func setDeploymentPassword(passedFlags flagparser.MainFlags) {
210212
if passedFlags.DeploymentPassword == "" {
211213
return
212214
}
213-
logging.AddString("Password has been changed for deployment")
215+
logging.LogDeploymentPassword()
214216
configuration.SetDeploymentPassword(passedFlags.DeploymentPassword)
215217
}
216218

internal/configuration/Configuration.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"github.com/forceu/gokapi/internal/configuration/database"
1717
"github.com/forceu/gokapi/internal/environment"
1818
"github.com/forceu/gokapi/internal/helper"
19-
log "github.com/forceu/gokapi/internal/logging"
19+
"github.com/forceu/gokapi/internal/logging"
2020
"github.com/forceu/gokapi/internal/models"
2121
"github.com/forceu/gokapi/internal/storage/filesystem"
2222
"io"
@@ -93,7 +93,7 @@ func Load() {
9393
}
9494
helper.CreateDir(serverSettings.DataDir)
9595
filesystem.Init(serverSettings.DataDir)
96-
log.Init(Environment.DataDir)
96+
logging.Init(Environment.DataDir)
9797
}
9898

9999
// ConnectDatabase loads the database that is defined in the configuration
@@ -152,6 +152,7 @@ func MigrateToV2(authPassword string, allowedUsers []string) {
152152
database.SaveMetaData(file)
153153
}
154154
database.DeleteAllSessions()
155+
logging.UpgradeToV2()
155156
fmt.Println("Migration complete")
156157
}
157158

internal/configuration/database/provider/redis/Redis.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type DatabaseProvider struct {
1818
}
1919

2020
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
21-
const DatabaseSchemeVersion = 4
21+
const DatabaseSchemeVersion = 5
2222

2323
// New returns an instance
2424
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -96,7 +96,7 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
9696
if currentDbVersion < 3 {
9797
fmt.Println("Please update to v1.9.6 before upgrading to 2.0.0")
9898
}
99-
// < v2.0.0-beta
99+
// < v2.0.0-beta1
100100
if currentDbVersion < 4 {
101101
p.DeleteAllSessions()
102102
apiKeys := p.GetAllApiKeys()
@@ -109,6 +109,15 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
109109
p.SaveEnd2EndInfo(legacyE2e, 0)
110110
p.deleteKey("e2einfo")
111111
}
112+
// < v2.0.0-beta2
113+
if currentDbVersion < 5 {
114+
keys := p.GetAllApiKeys()
115+
for _, key := range keys {
116+
if key.IsSystemKey {
117+
p.DeleteApiKey(key.Id)
118+
}
119+
}
120+
}
112121
}
113122

114123
const keyDbVersion = "dbversion"

internal/configuration/database/provider/sqlite/Sqlite.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type DatabaseProvider struct {
2020
}
2121

2222
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
23-
const DatabaseSchemeVersion = 7
23+
const DatabaseSchemeVersion = 8
2424

2525
// New returns an instance
2626
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -81,6 +81,15 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
8181
p.SaveEnd2EndInfo(legacyE2E, 0)
8282
}
8383
}
84+
// < v2.0.0-beta2
85+
if currentDbVersion < 8 {
86+
keys := p.GetAllApiKeys()
87+
for _, key := range keys {
88+
if key.IsSystemKey {
89+
p.DeleteApiKey(key.Id)
90+
}
91+
}
92+
}
8493
}
8594

8695
func getLegacyE2EConfig(p DatabaseProvider) models.E2EInfoEncrypted {

internal/logging/Logging.go

Lines changed: 170 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logging
22

33
import (
4+
"bufio"
45
"fmt"
56
"github.com/forceu/gokapi/internal/environment"
67
"github.com/forceu/gokapi/internal/helper"
@@ -16,6 +17,13 @@ import (
1617
var logPath = "config/log.txt"
1718
var mutex sync.Mutex
1819

20+
const categoryInfo = "info"
21+
const categoryDownload = "download"
22+
const categoryUpload = "upload"
23+
const categoryEdit = "edit"
24+
const categoryAuth = "auth"
25+
const categoryWarning = "warning"
26+
1927
var outputToStdout = false
2028

2129
// Init sets the path where to write the log file to
@@ -25,27 +33,176 @@ func Init(filePath string) {
2533
outputToStdout = env.LogToStdout
2634
}
2735

28-
// AddString adds a line to the logfile including the current date. Non-Blocking
29-
func AddString(text string) {
30-
output := formatDate(text)
36+
// GetAll returns all log entries as a single string and if the log file exists
37+
func GetAll() (string, bool) {
38+
if helper.FileExists(logPath) {
39+
content, err := os.ReadFile(logPath)
40+
helper.Check(err)
41+
return string(content), true
42+
} else {
43+
return fmt.Sprintf("[%s] No log file found!", categoryWarning), false
44+
}
45+
}
46+
47+
// createLogEntry adds a line to the logfile including the current date. Also outputs to Stdout if set.
48+
func createLogEntry(category, text string, blocking bool) {
49+
output := createLogFormat(category, text)
3150
if outputToStdout {
3251
fmt.Println(output)
3352
}
34-
go writeToFile(output)
53+
if blocking {
54+
writeToFile(output)
55+
} else {
56+
go writeToFile(output)
57+
}
3558
}
3659

37-
// GetLogPath returns the relative path to the log file
38-
func GetLogPath() string {
39-
return logPath
60+
func createLogFormat(category, text string) string {
61+
return createLogFormatCustomTimestamp(category, text, time.Now())
62+
}
63+
func createLogFormatCustomTimestamp(category, text string, timestamp time.Time) string {
64+
return fmt.Sprintf("%s [%s] %s", getDate(timestamp), category, text)
4065
}
4166

42-
// AddDownload adds a line to the logfile when a download was requested. Non-Blocking
43-
func AddDownload(file *models.File, r *http.Request, saveIp bool) {
67+
// LogStartup adds a log entry to indicate that Gokapi has started. Non-blocking
68+
func LogStartup() {
69+
createLogEntry(categoryInfo, "Gokapi started", false)
70+
}
71+
72+
// LogShutdown adds a log entry to indicate that Gokapi is shutting down. Blocking call
73+
func LogShutdown() {
74+
createLogEntry(categoryInfo, "Gokapi shutting down", true)
75+
}
76+
77+
// LogSetup adds a log entry to indicate that the setup was run. Non-blocking
78+
func LogSetup() {
79+
createLogEntry(categoryAuth, "Re-running Gokapi setup", false)
80+
}
81+
82+
// LogDeploymentPassword adds a log entry to indicate that a deployment password was set. Non-blocking
83+
func LogDeploymentPassword() {
84+
createLogEntry(categoryAuth, "Setting new admin password", false)
85+
}
86+
87+
// LogUserDeletion adds a log entry to indicate that a user was deleted. Non-blocking
88+
func LogUserDeletion(modifiedUser, userEditor models.User) {
89+
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was deleted by %s (user #%d)",
90+
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
91+
}
92+
93+
// LogUserEdit adds a log entry to indicate that a user was modified. Non-blocking
94+
func LogUserEdit(modifiedUser, userEditor models.User) {
95+
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was modified by %s (user #%d)",
96+
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
97+
}
98+
99+
// LogUserCreation adds a log entry to indicate that a user was created. Non-blocking
100+
func LogUserCreation(modifiedUser, userEditor models.User) {
101+
createLogEntry(categoryAuth, fmt.Sprintf("%s (#%d) was created by %s (user #%d)",
102+
modifiedUser.Name, modifiedUser.Id, userEditor.Name, userEditor.Id), false)
103+
}
104+
105+
// LogDownload adds a log entry when a download was requested. Non-Blocking
106+
func LogDownload(file models.File, r *http.Request, saveIp bool) {
44107
if saveIp {
45-
AddString(fmt.Sprintf("Download: Filename %s, IP %s, ID %s, Useragent %s", file.Name, getIpAddress(r), file.Id, r.UserAgent()))
108+
createLogEntry(categoryDownload, fmt.Sprintf("%s, IP %s, ID %s, Useragent %s", file.Name, getIpAddress(r), file.Id, r.UserAgent()), false)
46109
} else {
47-
AddString(fmt.Sprintf("Download: Filename %s, ID %s, Useragent %s", file.Name, file.Id, r.UserAgent()))
110+
createLogEntry(categoryDownload, fmt.Sprintf("%s, ID %s, Useragent %s", file.Name, file.Id, r.UserAgent()), false)
111+
}
112+
}
113+
114+
// LogUpload adds a log entry when an upload was created. Non-Blocking
115+
func LogUpload(file models.File, user models.User) {
116+
createLogEntry(categoryUpload, fmt.Sprintf("%s, ID %s, uploaded by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
117+
}
118+
119+
// LogEdit adds a log entry when an upload was edited. Non-Blocking
120+
func LogEdit(file models.File, user models.User) {
121+
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, edited by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
122+
}
123+
124+
// LogReplace adds a log entry when an upload was replaced. Non-Blocking
125+
func LogReplace(originalFile, newContent models.File, user models.User) {
126+
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s had content replaced with %s (ID %s) by %s (user #%d)",
127+
originalFile.Name, originalFile.Id, newContent.Name, newContent.Id, user.Name, user.Id), false)
128+
}
129+
130+
// LogDelete adds a log entry when an upload was deleted. Non-Blocking
131+
func LogDelete(file models.File, user models.User) {
132+
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, deleted by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
133+
}
134+
135+
// UpgradeToV2 adds tags to existing logs
136+
// deprecated
137+
func UpgradeToV2() {
138+
content, exists := GetAll()
139+
mutex.Lock()
140+
if !exists {
141+
return
142+
}
143+
var newLogs strings.Builder
144+
scanner := bufio.NewScanner(strings.NewReader(content))
145+
for scanner.Scan() {
146+
line := scanner.Text()
147+
if strings.Contains(line, "Gokapi started") {
148+
line = strings.Replace(line, "Gokapi started", "["+categoryInfo+"] Gokapi started", 1)
149+
}
150+
if strings.Contains(line, "Download: Filename") {
151+
line = strings.Replace(line, "Download: Filename", "["+categoryDownload+"] Filename", 1)
152+
}
153+
newLogs.WriteString(line)
154+
newLogs.WriteString("\n")
48155
}
156+
helper.Check(scanner.Err())
157+
err := os.WriteFile(logPath, []byte(newLogs.String()), 0600)
158+
helper.Check(err)
159+
defer mutex.Unlock()
160+
}
161+
162+
func DeleteLogs(userName string, userId int, cutoff int64, r *http.Request) {
163+
if cutoff == 0 {
164+
deleteAllLogs(userName, userId, r)
165+
return
166+
}
167+
mutex.Lock()
168+
logFile, err := os.ReadFile(logPath)
169+
helper.Check(err)
170+
var newFile strings.Builder
171+
scanner := bufio.NewScanner(strings.NewReader(string(logFile)))
172+
newFile.WriteString(getLogDeletionMessage(userName, userId, r, time.Unix(cutoff, 0)))
173+
for scanner.Scan() {
174+
line := scanner.Text()
175+
timeEntry, err := parseTimeLogEntry(line)
176+
if err != nil {
177+
fmt.Println(err)
178+
continue
179+
}
180+
if timeEntry.Unix() > cutoff {
181+
newFile.WriteString(line + "\n")
182+
}
183+
}
184+
err = os.WriteFile(logPath, []byte(newFile.String()), 0600)
185+
helper.Check(err)
186+
defer mutex.Unlock()
187+
}
188+
189+
func parseTimeLogEntry(input string) (time.Time, error) {
190+
const layout = "Mon, 02 Jan 2006 15:04:05 MST"
191+
lineContent := strings.Split(input, " [")
192+
return time.Parse(layout, lineContent[0])
193+
}
194+
195+
func getLogDeletionMessage(userName string, userId int, r *http.Request, timestamp time.Time) string {
196+
return createLogFormatCustomTimestamp(categoryWarning, fmt.Sprintf("Previous logs deleted by %s (user #%d) on %s. IP: %s\n",
197+
userName, userId, getDate(time.Now()), getIpAddress(r)), timestamp)
198+
}
199+
200+
func deleteAllLogs(userName string, userId int, r *http.Request) {
201+
mutex.Lock()
202+
defer mutex.Unlock()
203+
message := getLogDeletionMessage(userName, userId, r, time.Now())
204+
err := os.WriteFile(logPath, []byte(message), 0600)
205+
helper.Check(err)
49206
}
50207

51208
func writeToFile(text string) {
@@ -58,8 +215,8 @@ func writeToFile(text string) {
58215
helper.Check(err)
59216
}
60217

61-
func formatDate(input string) string {
62-
return time.Now().UTC().Format(time.RFC1123) + " " + input
218+
func getDate(timestamp time.Time) string {
219+
return timestamp.UTC().Format(time.RFC1123)
63220
}
64221

65222
func getIpAddress(r *http.Request) string {

internal/logging/Logging_test.go

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,10 @@ func TestInit(t *testing.T) {
3939

4040
func TestAddString(t *testing.T) {
4141
test.FileDoesNotExist(t, "test/log.txt")
42-
AddString("Hello")
43-
// Need sleep, as AddString() is non-blocking
44-
time.Sleep(500 * time.Millisecond)
42+
createLogEntry(categoryInfo, "Hello", true)
4543
test.FileExists(t, "test/log.txt")
4644
content, _ := os.ReadFile("test/log.txt")
47-
test.IsEqualBool(t, strings.Contains(string(content), "UTC Hello"), true)
45+
test.IsEqualBool(t, strings.Contains(string(content), "UTC [info] Hello"), true)
4846
}
4947

5048
func TestAddDownload(t *testing.T) {
@@ -55,19 +53,15 @@ func TestAddDownload(t *testing.T) {
5553
r := httptest.NewRequest("GET", "/test", nil)
5654
r.Header.Set("User-Agent", "testAgent")
5755
r.Header.Add("X-REAL-IP", "1.1.1.1")
58-
AddDownload(&file, r, true)
59-
// Need sleep, as AddDownload() is non-blocking
56+
LogDownload(file, r, true)
57+
// Need sleep, as LogDownload() is non-blocking
6058
time.Sleep(500 * time.Millisecond)
6159
content, _ := os.ReadFile("test/log.txt")
62-
test.IsEqualBool(t, strings.Contains(string(content), "UTC Download: Filename testName, IP 1.1.1.1, ID testId, Useragent testAgent"), true)
60+
test.IsEqualBool(t, strings.Contains(string(content), "UTC [download] testName, IP 1.1.1.1, ID testId, Useragent testAgent"), true)
6361
r.Header.Add("X-REAL-IP", "2.2.2.2")
64-
AddDownload(&file, r, false)
65-
// Need sleep, as AddDownload() is non-blocking
62+
LogDownload(file, r, false)
63+
// Need sleep, as LogDownload() is non-blocking
6664
time.Sleep(500 * time.Millisecond)
6765
content, _ = os.ReadFile("test/log.txt")
6866
test.IsEqualBool(t, strings.Contains(string(content), "2.2.2.2"), false)
6967
}
70-
71-
func TestGetLogPath(t *testing.T) {
72-
test.IsEqualString(t, GetLogPath(), "test/log.txt")
73-
}

0 commit comments

Comments
 (0)