landed display of system audit logs
This commit is contained in:
+70
-36
@@ -11,6 +11,7 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
@@ -126,6 +127,45 @@ const (
|
|||||||
// auditWriteQueue is a channel to store audit records in the background.
|
// auditWriteQueue is a channel to store audit records in the background.
|
||||||
var auditWriteQueue chan *AuditRecord
|
var auditWriteQueue chan *AuditRecord
|
||||||
|
|
||||||
|
// auditWriter is the routine that stores audit records in trhe background.
|
||||||
|
func auditWriter(workChan chan *AuditRecord, doneChan chan bool) {
|
||||||
|
for ar := range workChan {
|
||||||
|
err := ar.Store(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("dropped audit record (%+v) on the floor: %v", *ar, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doneChan <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupAuditWriter sets up the background audit writer.
|
||||||
|
func setupAuditWriter() func() {
|
||||||
|
auditWriteQueue = make(chan *AuditRecord, config.GlobalConfig.Tuning.Queues.AuditWrites)
|
||||||
|
doneChan := make(chan bool)
|
||||||
|
go auditWriter(auditWriteQueue, doneChan)
|
||||||
|
return func() {
|
||||||
|
close(auditWriteQueue)
|
||||||
|
<-doneChan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store stores the audit record in the database.
|
||||||
|
func (ar *AuditRecord) Store(ctx context.Context) error {
|
||||||
|
if ar.Record > 0 {
|
||||||
|
return fmt.Errorf("audit record %d already stored", ar.Record)
|
||||||
|
}
|
||||||
|
moment := time.Now().UTC()
|
||||||
|
rs, err := amdb.ExecContext(ctx, `INSERT INTO audit (on_date, event, uid, commid, ip, data1, data2, data3, data4)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, moment, ar.Event, ar.Uid, ar.CommId, ar.IP,
|
||||||
|
ar.Data1, ar.Data2, ar.Data3, ar.Data4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ar.Record, _ = rs.LastInsertId()
|
||||||
|
ar.OnDate = moment
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
/* AmNewAudit creates a new audit record.
|
/* AmNewAudit creates a new audit record.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* rectype - Audit record type.
|
* rectype - Audit record type.
|
||||||
@@ -191,34 +231,6 @@ func AmNewCommAudit(rectype int32, uid int32, commid int32, ip string, data ...s
|
|||||||
return &rc
|
return &rc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store stores the audit record in the database.
|
|
||||||
func (ar *AuditRecord) Store(ctx context.Context) error {
|
|
||||||
if ar.Record > 0 {
|
|
||||||
return fmt.Errorf("audit record %d already stored", ar.Record)
|
|
||||||
}
|
|
||||||
moment := time.Now().UTC()
|
|
||||||
rs, err := amdb.ExecContext(ctx, `INSERT INTO audit (on_date, event, uid, commid, ip, data1, data2, data3, data4)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`, moment, ar.Event, ar.Uid, ar.CommId, ar.IP,
|
|
||||||
ar.Data1, ar.Data2, ar.Data3, ar.Data4)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ar.Record, _ = rs.LastInsertId()
|
|
||||||
ar.OnDate = moment
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// auditWriter is the routine that stores audit records in trhe background.
|
|
||||||
func auditWriter(workChan chan *AuditRecord, doneChan chan bool) {
|
|
||||||
for ar := range workChan {
|
|
||||||
err := ar.Store(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("dropped audit record (%+v) on the floor: %v", *ar, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doneChan <- true
|
|
||||||
}
|
|
||||||
|
|
||||||
// AmStoreAudit stores the audit record in the background.
|
// AmStoreAudit stores the audit record in the background.
|
||||||
func AmStoreAudit(rec *AuditRecord) {
|
func AmStoreAudit(rec *AuditRecord) {
|
||||||
if rec != nil {
|
if rec != nil {
|
||||||
@@ -226,13 +238,35 @@ func AmStoreAudit(rec *AuditRecord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupAuditWriter sets up the background audit writer.
|
// AmListAuditRecords lists a section of the audit records.
|
||||||
func setupAuditWriter() func() {
|
func AmListAuditRecords(ctx context.Context, comm *Community, offset, max int) ([]AuditRecord, int, error) {
|
||||||
auditWriteQueue = make(chan *AuditRecord, config.GlobalConfig.Tuning.Queues.AuditWrites)
|
var row *sql.Row
|
||||||
doneChan := make(chan bool)
|
if comm != nil {
|
||||||
go auditWriter(auditWriteQueue, doneChan)
|
row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM audit WHERE commid = ?", comm.Id)
|
||||||
return func() {
|
} else {
|
||||||
close(auditWriteQueue)
|
row = amdb.QueryRowContext(ctx, "SELECT COUNT(*) FROM audit")
|
||||||
<-doneChan
|
}
|
||||||
|
var count int
|
||||||
|
err := row.Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
return nil, -1, err
|
||||||
|
}
|
||||||
|
var rc []AuditRecord
|
||||||
|
if comm != nil {
|
||||||
|
if offset > 0 {
|
||||||
|
err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit WHERE commid = ? ORDER BY on_date DESC LIMIT ? OFFSET ?", comm.Id, max, offset)
|
||||||
|
} else {
|
||||||
|
err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit WHERE commid = ? ORDER BY on_date DESC LIMIT ?", comm.Id, max)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if offset > 0 {
|
||||||
|
err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit ORDER BY on_date DESC LIMIT ? OFFSET ?", max, offset)
|
||||||
|
} else {
|
||||||
|
err = amdb.SelectContext(ctx, &rc, "SELECT * FROM audit ORDER BY on_date DESC LIMIT ?", max)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, count, err
|
||||||
|
}
|
||||||
|
return rc, count, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -561,17 +561,6 @@ func AmGetAnonUser(ctx context.Context) (*User, error) {
|
|||||||
return rc, err
|
return rc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AmGetBOFH returns the user account of the global system administrator.
|
|
||||||
func AmGetBOFH(ctx context.Context) (*User, error) {
|
|
||||||
row := amdb.QueryRowContext(ctx, "SELECT uid FROM users WHERE base_lvl = ?", AmRole("Global.BOFH").Level())
|
|
||||||
var uid int32
|
|
||||||
err := row.Scan(&uid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return AmGetUser(ctx, uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// hashPassword hashes the password value.
|
// hashPassword hashes the password value.
|
||||||
func hashPassword(password string) string {
|
func hashPassword(password string) string {
|
||||||
if len(password) == 0 {
|
if len(password) == 0 {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ _(italicized items can be deferred)_
|
|||||||
- ~~Edit Global Properties~~
|
- ~~Edit Global Properties~~
|
||||||
- ~~View/Edit IP Address Bans~~
|
- ~~View/Edit IP Address Bans~~
|
||||||
- ~~User Account Management~~
|
- ~~User Account Management~~
|
||||||
- System Audit Logs
|
- ~~System Audit Logs~~
|
||||||
- Import User Accounts
|
- Import User Accounts
|
||||||
- ~~Conferences list:~~
|
- ~~Conferences list:~~
|
||||||
- ~~Find~~
|
- ~~Find~~
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ func setupEcho() *echo.Echo {
|
|||||||
e.POST("/sysadmin/users/:uname/photo", ui.AmWrap(AdminUserPhoto))
|
e.POST("/sysadmin/users/:uname/photo", ui.AmWrap(AdminUserPhoto))
|
||||||
e.GET("/sysadmin/ipban", ui.AmWrap(IPBanList))
|
e.GET("/sysadmin/ipban", ui.AmWrap(IPBanList))
|
||||||
e.GET("/sysadmin/ipban/add", ui.AmWrap(AddIPBanForm))
|
e.GET("/sysadmin/ipban/add", ui.AmWrap(AddIPBanForm))
|
||||||
|
e.Match(GetAndPost, "/sysadmin/audit", ui.AmWrap(SystemAudit))
|
||||||
e.POST("/sysadmin/ipban/add", ui.AmWrap(AddIPBan))
|
e.POST("/sysadmin/ipban/add", ui.AmWrap(AddIPBan))
|
||||||
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
e.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||||
@@ -195,10 +196,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
bofh, err := database.AmGetBOFH(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up to trap SIGINT/SIGTERM and shut down gracefully
|
// Set up to trap SIGINT/SIGTERM and shut down gracefully
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
@@ -215,7 +212,7 @@ func main() {
|
|||||||
e := setupEcho()
|
e := setupEcho()
|
||||||
|
|
||||||
// Audit the startup
|
// Audit the startup
|
||||||
database.AmStoreAudit(database.AmNewAudit(database.AuditStartup, bofh.Uid, myIP.String(),
|
database.AmStoreAudit(database.AmNewAudit(database.AuditStartup, 0, myIP.String(),
|
||||||
fmt.Sprintf("version=%s", config.AMSTERDAM_VERSION)))
|
fmt.Sprintf("version=%s", config.AMSTERDAM_VERSION)))
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
@@ -234,5 +231,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audit the shutdown
|
// Audit the shutdown
|
||||||
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, bofh.Uid, myIP.String()))
|
database.AmStoreAudit(database.AmNewAudit(database.AuditShutdown, 0, myIP.String()))
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -683,3 +683,66 @@ func AddIPBan(ctxt ui.AmContext) (string, any) {
|
|||||||
}
|
}
|
||||||
return dlg.RenderError(ctxt, err.Error())
|
return dlg.RenderError(ctxt, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SystemAudit(ctxt ui.AmContext) (string, any) {
|
||||||
|
if !database.AmTestPermission("Global.SysAdminAccess", ctxt.CurrentUser().BaseLevel) {
|
||||||
|
return "error", ENOACCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
ofs := 0
|
||||||
|
maxRecs := ctxt.Globals().NumAuditPage
|
||||||
|
if ctxt.Verb() == "POST" {
|
||||||
|
if ctxt.FormFieldIsSet("prev") {
|
||||||
|
ofs = min(0, ofs-int(maxRecs))
|
||||||
|
} else if ctxt.FormFieldIsSet("next") {
|
||||||
|
ofs += int(maxRecs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auditRecs, total, err := database.AmListAuditRecords(ctxt.Ctx(), nil, ofs, int(maxRecs))
|
||||||
|
if err != nil {
|
||||||
|
return "error", err
|
||||||
|
}
|
||||||
|
|
||||||
|
descr := make([]string, len(auditRecs))
|
||||||
|
userName := make([]string, len(auditRecs))
|
||||||
|
communityName := make([]string, len(auditRecs))
|
||||||
|
for i, ar := range auditRecs {
|
||||||
|
descr[i] = database.AmAuditText(int(ar.Event))
|
||||||
|
if ar.Uid > 0 {
|
||||||
|
user, err := database.AmGetUser(ctxt.Ctx(), ar.Uid)
|
||||||
|
if err != nil {
|
||||||
|
userName[i] = fmt.Sprintf("<<%v>>", err)
|
||||||
|
} else {
|
||||||
|
userName[i] = user.Username
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userName[i] = ""
|
||||||
|
}
|
||||||
|
if ar.CommId > 0 {
|
||||||
|
comm, err := database.AmGetCommunity(ctxt.Ctx(), ar.CommId)
|
||||||
|
if err != nil {
|
||||||
|
communityName[i] = fmt.Sprintf("<<%v>>", err)
|
||||||
|
} else {
|
||||||
|
communityName[i] = comm.Name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
communityName[i] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt.VarMap().Set("total", total)
|
||||||
|
ctxt.VarMap().Set("ofs", ofs)
|
||||||
|
ctxt.VarMap().Set("auditRecords", auditRecs)
|
||||||
|
ctxt.VarMap().Set("descr", descr)
|
||||||
|
ctxt.VarMap().Set("user", userName)
|
||||||
|
ctxt.VarMap().Set("community", communityName)
|
||||||
|
if ofs > 0 {
|
||||||
|
ctxt.VarMap().Set("showPrev", true)
|
||||||
|
}
|
||||||
|
if ofs+int(maxRecs) < total {
|
||||||
|
ctxt.VarMap().Set("showNext", true)
|
||||||
|
}
|
||||||
|
ctxt.SetFrameTitle("System Audit Records")
|
||||||
|
return "framed", "audit.jet"
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ menudefs:
|
|||||||
link: "/sysadmin/users"
|
link: "/sysadmin/users"
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- text: "System Audit Logs"
|
- text: "System Audit Logs"
|
||||||
link: "/TODO/sysadmin/audit"
|
link: "/sysadmin/audit"
|
||||||
permission: "Global.SysAdminAccess"
|
permission: "Global.SysAdminAccess"
|
||||||
- text: "Import User Accounts"
|
- text: "Import User Accounts"
|
||||||
link: "/TODO/sysadmin/import"
|
link: "/TODO/sysadmin/import"
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
{*
|
||||||
|
* Amsterdam Web Communities System
|
||||||
|
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold mb-2">System Audit Records</h1>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backlink -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<a class="text-blue-700 hover:text-blue-900 text-sm flex items-center gap-2 w-fit" href="/sysadmin">
|
||||||
|
<span>←</span>
|
||||||
|
Return to System Administration Menu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search results -->
|
||||||
|
<hr class="border-gray-400 mb-4">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<div class="text-sm text-black">Displaying records <b>{{ ofs + 1 }}</b> to <b>{{ ofs + len(auditRecords) }}</b> of <b>{{ total }}</b></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<div class="bg-white border border-gray-300 rounded-lg overflow-hidden">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-100 border-b-2 border-gray-300">
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider whitespace-nowrap">Date/Time</th>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Description</th>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">User</th>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">Community</th>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider">IP Address</th>
|
||||||
|
<th class="px-2 py-1 text-left text-xs font-bold text-gray-700 uppercase tracking-wider" colspan="4">Additional Data</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
{{ range i, a := auditRecords }}
|
||||||
|
<tr class="hover:bg-blue-50">
|
||||||
|
<td class="px-2 py-1 text-sm whitespace-nowrap text-gray-700">{{ DisplayDateTime( a.OnDate, .) }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ descr[i] }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ user[i] }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ community[i] }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ iif(isset(a.IP), a.IP, "") }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ iif(isset(a.Data1), a.Data1, "") }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ iif(isset(a.Data2), a.Data2, "") }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ iif(isset(a.Data3), a.Data3, "") }}</td>
|
||||||
|
<td class="px-2 py-1 text-sm text-gray-700">{{ iif(isset(a.Data4), a.Data4, "") }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ if isset(showPrev) || isset(showNext) }}
|
||||||
|
<!-- Bottom Navigation -->
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<form method="POST" action="/sysadmin/audit">
|
||||||
|
<input type="hidden" name="ofs" value="{{ ofs }}"/>
|
||||||
|
{{ if isset(showPrev) }}
|
||||||
|
<button type="submit" name="prev"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
⏪ Prev
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
{{ if isset(showNext) }}
|
||||||
|
<button type="submit" name="next"
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2 rounded font-medium transition-colors">
|
||||||
|
Next ⏩
|
||||||
|
</button>
|
||||||
|
{{ end }}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user