Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
43dc0d9
feat: audit logs pub sub msg and router metadata
SATYAsasini Jun 29, 2026
c735edb
fix: adding new constants for auditing
SATYAsasini Jun 30, 2026
1f3face
feat: add new fields in nats message format
SATYAsasini Jul 3, 2026
5480f8d
feat: add pod exec action and resource name
SATYAsasini Jul 6, 2026
a1deb7b
feat: add missing fields in nats
SATYAsasini Jul 6, 2026
cd5b471
feat: casing correction of enums
SATYAsasini Jul 7, 2026
4ec7d7b
feat: new resources enum added
SATYAsasini Jul 9, 2026
a735361
feat: extra resource enusms for audit
SATYAsasini Jul 10, 2026
3b6dd44
feat: adding check for only success api method
SATYAsasini Jul 10, 2026
db24cb2
feat: enums cahnges
SATYAsasini Jul 14, 2026
b49f438
feat: enums cahnges
SATYAsasini Jul 14, 2026
7e57256
feat(common-lib): introduce strenum on audit constants to generate ma…
Bhupesh-V Jul 14, 2026
177acae
feat: new resource type and wf enity added
SATYAsasini Jul 14, 2026
1d0013c
Merge pull request #394 from Bhupesh-V/audit-strenum-const
SATYAsasini Jul 15, 2026
e7490ba
feat: add resource watcher enum
SATYAsasini Jul 16, 2026
b706ed6
feat: add constnat for external link
SATYAsasini Jul 16, 2026
0eff767
feat: add ResourceLockConfigurationAssignment resource type
bhupesh-devtron Jul 22, 2026
9a12c7a
feat: add SSL support for PostgreSQL connections with configurable SS…
Shivam-nagar23 Jul 7, 2026
5c7f0c7
feat: add SSL support for PostgreSQL connections with configurable SS…
Shivam-nagar23 Jul 7, 2026
4fd0f4a
feat: new resource type approval policy added
SATYAsasini Jul 22, 2026
1f60222
Merge branch 'main' into feat-audit-logs-v2
SATYAsasini Jul 23, 2026
d56fe71
feat: remove unused resource entity constnats
SATYAsasini Jul 24, 2026
de04baa
Merge branch 'main' into feat-audit-logs-v2
SATYAsasini Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions common-lib/pubsub-lib/JetStreamUtil.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ const (
STORAGE_VELERO_POST_INSTALLATION_TOPIC string = "STORAGE_VELERO_POST_INSTALLATION_TOPIC"
STORAGE_VELERO_POST_INSTALLATION_GROUP string = "STORAGE_VELERO_POST_INSTALLATION_GROUP"
STORAGE_VELERO_POST_INSTALLATION_DURABLE string = "STORAGE_VELERO_POST_INSTALLATION_DURABLE"
AUDIT_LOG_TOPIC string = "AUDIT_LOG_TOPIC"
AUDIT_LOG_GROUP string = "AUDIT_LOG_GROUP"
AUDIT_LOG_DURABLE string = "AUDIT_LOG_DURABLE"
)

type NatsTopic struct {
Expand Down Expand Up @@ -199,6 +202,7 @@ var natsTopicMapping = map[string]NatsTopic{
STORAGE_MODULE_TOPIC: {topicName: STORAGE_MODULE_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_MODULE_GROUP, consumerName: STORAGE_MODULE_DURABLE},
STORAGE_VELERO_INSTALL_TOPIC: {topicName: STORAGE_VELERO_INSTALL_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_VELERO_INSTALL_GROUP, consumerName: STORAGE_VELERO_INSTALL_DURABLE},
STORAGE_VELERO_POST_INSTALLATION_TOPIC: {topicName: STORAGE_VELERO_POST_INSTALLATION_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: STORAGE_VELERO_POST_INSTALLATION_GROUP, consumerName: STORAGE_VELERO_POST_INSTALLATION_DURABLE},
AUDIT_LOG_TOPIC: {topicName: AUDIT_LOG_TOPIC, streamName: ORCHESTRATOR_STREAM, queueName: AUDIT_LOG_GROUP, consumerName: AUDIT_LOG_DURABLE},
}

var NatsStreamWiseConfigMapping = map[string]NatsStreamConfig{
Expand Down Expand Up @@ -246,6 +250,7 @@ var NatsConsumerWiseConfigMapping = map[string]NatsConsumerConfig{
STORAGE_MODULE_DURABLE: {},
STORAGE_VELERO_INSTALL_DURABLE: {},
STORAGE_VELERO_POST_INSTALLATION_DURABLE: {},
AUDIT_LOG_DURABLE: {},
}

// getConsumerConfigMap will fetch the consumer wise config from the json string
Expand Down
156 changes: 156 additions & 0 deletions common-lib/pubsub-lib/audit/AuditLogEvent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2024. Devtron Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package audit

import (
"encoding/json"
"time"
)

// TimeFormat is the wire format for AuditLogEvent.Time (ISO-8601, numeric zone).
const TimeFormat = "2006-01-02T15:04:05-0700"

// RouteName builds the MODULE:RESOURCE:ACTION route name read back by the
// orchestrator's audit log service. Using this helper keeps route tags
// consistent with the typed Module/Resource/Action constants instead of
// hand-written strings.
func RouteName(module AuditModule, resource AuditResource, action AuditAction) string {
return string(module) + ":" + string(resource) + ":" + string(action)
}

// ResourceRef identifies the concrete resource an audited action touched.
type ResourceRef struct {
Type string `json:"type"`
Name string `json:"name,omitempty"`
}

// EnrichmentEntity carries the raw identifier (e.g. an app_id) that the
// audit-log service resolves into the full entity during enrichment.
type EnrichmentEntity struct {
Identifier string `json:"identifier"`
}

// AuditLogEvent is the canonical NATS payload published on AUDIT_LOG_TOPIC.
// It is shared between the publisher (orchestrator) and the consumer
// (audit-log service) so both agree on the contract.
type AuditLogEvent struct {
ApiPath string `json:"apiPath"`
Time string `json:"time"`
Resource ResourceRef `json:"resource"`
Module string `json:"module"`
Type string `json:"type"` // action verb, e.g. "create"
Action string `json:"action"` // human sentence, e.g. "Created application 'dashboard'"
UpdatedBy string `json:"updatedBy"` // acting user (email) who performed the action
ClientIp string `json:"clientIp"` // client IP the request originated from
RequestMethod string `json:"requestMethod"` // HTTP method of the audited request
ApiResponseCode int `json:"apiResponseCode"` // HTTP status code the request returned
ResponseTime time.Duration `json:"responseTime"` // request handling duration (nanoseconds)
Payload map[string]interface{} `json:"payload"` // the actual request body of the audited call, always present
EnrichmentContext map[string]EnrichmentEntity `json:"enrichmentContext,omitempty"`
}

// NewAuditLogEvent constructs an event with the structural metadata parsed
// from a route name. eventTime is supplied by the caller (time.Now()) so this
// package stays free of implicit clock reads.
func NewAuditLogEvent(apiPath string, eventTime time.Time, module AuditModule, resourceType AuditResource, action AuditAction) *AuditLogEvent {
return &AuditLogEvent{
ApiPath: apiPath,
Time: eventTime.Format(TimeFormat),
Module: module.ToString(),
Type: action.ToString(),
Resource: ResourceRef{Type: resourceType.ToString()},
Payload: make(map[string]interface{}),
EnrichmentContext: make(map[string]EnrichmentEntity),
}
}

// WithResourceName sets the human-facing name of the affected resource.
func (e *AuditLogEvent) WithResourceName(name string) *AuditLogEvent {
e.Resource.Name = name
return e
}

// WithActor records the acting user (email) and originating client IP. The
// acting user is always available and is the "updated by" of the audited action.
func (e *AuditLogEvent) WithActor(updatedBy, clientIp string) *AuditLogEvent {
e.UpdatedBy = updatedBy
e.ClientIp = clientIp
return e
}

// WithRequest records the request's HTTP method, response status code and
// handling duration.
func (e *AuditLogEvent) WithRequest(method string, apiResponseCode int, responseTime time.Duration) *AuditLogEvent {
e.RequestMethod = method
e.ApiResponseCode = apiResponseCode
e.ResponseTime = responseTime
return e
}

// IsApiSuccess reports whether the audited request completed successfully (HTTP 2xx).
// Only successful actions are published to NATS — failed or denied requests
// (4xx/5xx) are not audited.
func (e *AuditLogEvent) IsApiSuccess() bool {
return e.ApiResponseCode >= 200 && e.ApiResponseCode < 300
}

// WithAction sets the human-readable action sentence.
func (e *AuditLogEvent) WithAction(action string) *AuditLogEvent {
e.Action = action
return e
}

// WithPayloadField adds a single key/value to the payload bag.
func (e *AuditLogEvent) WithPayloadField(key string, value interface{}) *AuditLogEvent {
if e.Payload == nil {
e.Payload = make(map[string]interface{})
}
e.Payload[key] = value
return e
}

// WithEnrichment records an identifier (e.g. app_id) under an entity key
// (e.g. "app") for the consumer to resolve. Empty identifiers are ignored so
// callers can pass through missing path vars without polluting the context.
func (e *AuditLogEvent) WithEnrichment(entity, identifier string) *AuditLogEvent {
if identifier == "" {
return e
}
if e.EnrichmentContext == nil {
e.EnrichmentContext = make(map[string]EnrichmentEntity)
}
e.EnrichmentContext[entity] = EnrichmentEntity{Identifier: identifier}
return e
}

// Marshal serializes the event to a JSON string for PubSubClientService.Publish.
func (e *AuditLogEvent) Marshal() (string, error) {
data, err := json.Marshal(e)
if err != nil {
return "", err
}
return string(data), nil
}

// UnmarshalAuditLogEvent parses a NATS payload back into an event (consumer side).
func UnmarshalAuditLogEvent(data string) (*AuditLogEvent, error) {
event := &AuditLogEvent{}
if err := json.Unmarshal([]byte(data), event); err != nil {
return nil, err
}
return event, nil
}
79 changes: 79 additions & 0 deletions common-lib/pubsub-lib/audit/AuditLogEvent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2024. Devtron Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package audit

import (
"encoding/json"
"testing"
"time"
)

func TestAuditLogEvent_MarshalShape(t *testing.T) {
ts := time.Date(2026, 6, 29, 6, 7, 11, 0, time.UTC)
event := NewAuditLogEvent("/orchestrator/app/edit", ts, ModuleAppManagement, ResourceApplication, ActionCreate).
WithResourceName("dashboard").
WithAction("Created application 'dashboard'").
WithPayloadField("request_path", "/orchestrator/app/edit").
WithPayloadField("http_method", "POST").
WithEnrichment(EntityApp, "42").
WithEnrichment(EntityUser, "7").
WithEnrichment(EntityCluster, "") // empty identifier must be ignored

out, err := event.Marshal()
if err != nil {
t.Fatalf("marshal: %v", err)
}

// Round-trip into a generic map to assert the wire contract.
var m map[string]interface{}
if err := json.Unmarshal([]byte(out), &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}

if m["apiPath"] != "/orchestrator/app/edit" {
t.Errorf("apiPath = %v", m["apiPath"])
}
if m["module"] != "application_management" {
t.Errorf("module = %v", m["module"])
}
if m["type"] != "create" {
t.Errorf("type = %v", m["type"])
}
if m["time"] != "2026-06-29T06:07:11+0000" {
t.Errorf("time = %v", m["time"])
}
resource := m["resource"].(map[string]interface{})
if resource["type"] != "application" || resource["name"] != "dashboard" {
t.Errorf("resource = %v", resource)
}
enrich := m["enrichmentContext"].(map[string]interface{})
if _, ok := enrich[EntityCluster]; ok {
t.Errorf("empty-identifier entity must be dropped, got %v", enrich)
}
if enrich[EntityApp].(map[string]interface{})["identifier"] != "42" {
t.Errorf("app identifier = %v", enrich[EntityApp])
}

// Round-trip back to a typed event.
back, err := UnmarshalAuditLogEvent(out)
if err != nil {
t.Fatalf("unmarshal event: %v", err)
}
if back.Resource.Name != "dashboard" || back.EnrichmentContext[EntityUser].Identifier != "7" {
t.Errorf("round-trip mismatch: %+v", back)
}
}
Loading
Loading