diff --git a/docs/deployment.md b/docs/deployment.md index 22304fe..a6ae116 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -122,10 +122,12 @@ For later updates, run the repository update helper on one webserver at a time: /opt/idssys/ta-deployment-broker/update.sh ``` -The helper fast-forwards the current tracked branch, rebuilds the image, applies -any pending migrations, recreates the broker container, and waits up to 150 -seconds for its Docker health check. It refuses to pull over tracked local -changes or from a detached/untracked branch. +The helper fetches and compares the current tracked branch with its upstream. If +both commits match, it exits without touching Docker. When an update exists, it +fast-forwards the branch, rebuilds the image, applies any pending migrations, +recreates the broker container, and waits up to 150 seconds for its Docker +health check. It refuses to pull over tracked local changes or from a detached, +untracked, locally-ahead, or diverged branch. ## 7. HAProxy health check @@ -178,7 +180,7 @@ technician's Gitea credentials for package operations. - Audit records are retained indefinitely unless an administrator establishes a database retention policy. The Codes view previews the newest 15 records; the Audit view can filter retained history by event, customer/deployment label, - actor, host, package, source IP, or details and displays up to the newest 250 + user, host, package, source IP, or details and displays up to the newest 250 matching events. - Keep `.env` outside Git and readable only by the service administrator. - Rotate both Gitea package tokens and the OAuth secret if either webserver is diff --git a/internal/app/audit_test.go b/internal/app/audit_test.go index 52d0530..831e5af 100644 --- a/internal/app/audit_test.go +++ b/internal/app/audit_test.go @@ -11,7 +11,7 @@ func TestAuditFiltersFromRequest(t *testing.T) { t.Parallel() request := httptest.NewRequest( "GET", - "/portal?audit_range=7d&audit_event=package_downloaded&audit_customer=Acme&audit_actor=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm", + "/portal?audit_range=7d&audit_event=package_downloaded&audit_customer=Acme&audit_user=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm", nil, ) filters := auditFiltersFromRequest(request) @@ -19,7 +19,7 @@ func TestAuditFiltersFromRequest(t *testing.T) { TimeRange: "7d", EventType: "package_downloaded", CustomerLabel: "Acme", - Actor: "taiadmin", + User: "taiadmin", Hostname: "pve01", PackageSlug: "sentinelone-linux", SourceIP: "10.10.1.25", @@ -38,6 +38,14 @@ func TestAuditFiltersDefaultToThirtyDays(t *testing.T) { } } +func TestAuditFiltersAcceptLegacyActorParameter(t *testing.T) { + t.Parallel() + request := httptest.NewRequest("GET", "/portal?audit_actor=legacy-user", nil) + if filters := auditFiltersFromRequest(request); filters.User != "legacy-user" { + t.Fatalf("user = %q, want legacy-user", filters.User) + } +} + func TestHasAuditQuery(t *testing.T) { t.Parallel() if !hasAuditQuery(httptest.NewRequest("GET", "/portal?audit_hostname=pve01", nil)) { @@ -54,7 +62,7 @@ func TestAuditQueryUsesPlaceholders(t *testing.T) { TimeRange: "all", EventType: "package_downloaded", CustomerLabel: "Acme", - Actor: "taiadmin", + User: "taiadmin", Hostname: "pve01", PackageSlug: "sentinelone-linux", SourceIP: "10.10.1.25", @@ -62,7 +70,7 @@ func TestAuditQueryUsesPlaceholders(t *testing.T) { } query, arguments := auditQuery(filters, 250) if strings.Contains(query, filters.CustomerLabel) || - strings.Contains(query, filters.Actor) || + strings.Contains(query, filters.User) || strings.Contains(query, filters.Hostname) { t.Fatal("filter values must not be interpolated into the SQL query") } diff --git a/internal/app/authorization.go b/internal/app/authorization.go index a3ca546..6d13458 100644 --- a/internal/app/authorization.go +++ b/internal/app/authorization.go @@ -62,6 +62,7 @@ func hasAuditQuery(r *http.Request) bool { "audit_range", "audit_event", "audit_customer", + "audit_user", "audit_actor", "audit_hostname", "audit_package", @@ -155,11 +156,16 @@ func auditFiltersFromRequest(r *http.Request) auditFilters { default: timeRange = "30d" } + userFilter := query.Get("audit_user") + if strings.TrimSpace(userFilter) == "" { + // Preserve audit links created before the portal used "user" terminology. + userFilter = query.Get("audit_actor") + } return auditFilters{ TimeRange: timeRange, EventType: limitedFilter(query.Get("audit_event"), 100), CustomerLabel: limitedFilter(query.Get("audit_customer"), 255), - Actor: limitedFilter(query.Get("audit_actor"), 255), + User: limitedFilter(userFilter, 255), Hostname: limitedFilter(query.Get("audit_hostname"), 255), PackageSlug: limitedFilter(query.Get("audit_package"), 100), SourceIP: limitedFilter(query.Get("audit_ip"), 64), @@ -224,7 +230,7 @@ func auditQuery(filters auditFilters, limit int) (string, []any) { value string }{ {"COALESCE(a.customer_label, '')", filters.CustomerLabel}, - {"ae.actor", filters.Actor}, + {"ae.actor", filters.User}, {"ae.hostname", filters.Hostname}, {"ae.package_slug", filters.PackageSlug}, {"ae.source_ip", filters.SourceIP}, @@ -260,7 +266,7 @@ func (s *Server) listAuditEvents(r *http.Request, filters auditFilters, limit in if err := rows.Scan( &record.EventType, &record.CustomerLabel, - &record.Actor, + &record.User, &record.Hostname, &record.PackageSlug, &record.SourceIP, @@ -606,7 +612,7 @@ func validSHA256(value string) bool { func (s *Server) audit( ctx context.Context, eventType string, - actor string, + user string, authorizationID *uint64, hostname string, packageSlug string, @@ -622,7 +628,7 @@ func (s *Server) audit( `INSERT INTO audit_events (event_type, actor, authorization_id, hostname, package_slug, source_ip, details) VALUES (?, ?, ?, ?, ?, ?, ?)`, - eventType, actor, authID, hostname, packageSlug, sourceIP, details, + eventType, user, authID, hostname, packageSlug, sourceIP, details, ) return err } diff --git a/internal/app/server.go b/internal/app/server.go index e5bec3b..9204ee6 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -71,7 +71,7 @@ type authorizationRecord struct { type auditRecord struct { EventType string CustomerLabel string - Actor string + User string Hostname string PackageSlug string SourceIP string @@ -83,7 +83,7 @@ type auditFilters struct { TimeRange string EventType string CustomerLabel string - Actor string + User string Hostname string PackageSlug string SourceIP string diff --git a/internal/app/templates/audit.html b/internal/app/templates/audit.html index 0683acd..ff6cfe7 100644 --- a/internal/app/templates/audit.html +++ b/internal/app/templates/audit.html @@ -39,7 +39,7 @@ - + diff --git a/internal/app/templates/components.html b/internal/app/templates/components.html index bc3f8f7..090706f 100644 --- a/internal/app/templates/components.html +++ b/internal/app/templates/components.html @@ -28,7 +28,7 @@ {{range .AuditEvents}} @@ -41,7 +41,7 @@ {{if .CustomerLabel}}{{.CustomerLabel}}{{else}}{{end}}
- {{if .Actor}}{{.Actor}}{{end}} + {{if .User}}{{.User}}{{end}} {{if .Hostname}}{{.Hostname}}{{end}} {{if .PackageSlug}}{{.PackageSlug}}{{end}}
diff --git a/update.sh b/update.sh index 58dd3bb..7ee483d 100755 --- a/update.sh +++ b/update.sh @@ -15,26 +15,44 @@ trap 'printf "ERROR: broker update failed at line %d.\n" "$LINENO" >&2' ERR cd "$SCRIPT_DIR" command -v git >/dev/null 2>&1 || fail "git is required" -command -v docker >/dev/null 2>&1 || fail "docker is required" -docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is required" -[[ -f compose.yaml ]] || fail "compose.yaml was not found in ${SCRIPT_DIR}" -[[ -f .env ]] || fail ".env was not found in ${SCRIPT_DIR}" if [[ "${1:-}" != "--post-pull" ]]; then branch="$(git symbolic-ref --quiet --short HEAD)" || fail "the repository is in detached HEAD state" - git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1 || + upstream="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" || fail "branch ${branch} does not have an upstream branch" + remote="$(git config --get "branch.${branch}.remote")" || + fail "branch ${branch} does not have a configured remote" git diff --quiet && git diff --cached --quiet || fail "tracked local changes must be committed or stashed before updating" - printf 'Updating TAPM Deployment Broker from branch %s...\n' "$branch" - git pull --ff-only + printf 'Checking %s against %s...\n' "$branch" "$upstream" + git fetch --prune "$remote" + + local_commit="$(git rev-parse HEAD)" + upstream_commit="$(git rev-parse '@{upstream}')" + if [[ "$local_commit" == "$upstream_commit" ]]; then + printf 'TAPM Deployment Broker is already up to date; no changes were made.\n' + exit 0 + fi + if git merge-base --is-ancestor "$local_commit" "$upstream_commit"; then + printf 'Updating TAPM Deployment Broker from %s...\n' "$upstream" + git merge --ff-only "$upstream_commit" + elif git merge-base --is-ancestor "$upstream_commit" "$local_commit"; then + fail "local branch ${branch} is ahead of ${upstream}; refusing to rebuild" + else + fail "local branch ${branch} has diverged from ${upstream}" + fi # Restart from the newly pulled script so updates to this file take effect. exec "$SCRIPT_PATH" --post-pull fi +command -v docker >/dev/null 2>&1 || fail "docker is required" +docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is required" +[[ -f compose.yaml ]] || fail "compose.yaml was not found in ${SCRIPT_DIR}" +[[ -f .env ]] || fail ".env was not found in ${SCRIPT_DIR}" + printf 'Building broker image...\n' docker compose build broker