Skip to content

feat: audit trail in stdout logs #411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8839bd1
feat: audit trail in stdout logs
fredmaggiowski Dec 3, 2024
97bb1ed
optional auditing
fredmaggiowski Dec 3, 2024
ef76ca4
minor fixes + coverage
fredmaggiowski Dec 3, 2024
074a75c
Update audit/agent_log_test.go
fredmaggiowski Dec 3, 2024
e221502
more tests
fredmaggiowski Dec 3, 2024
afe956a
fix
fredmaggiowski Dec 3, 2024
f9d4e2c
fix
fredmaggiowski Dec 3, 2024
8f92bb4
refactor: renamed builtin, ensure context value and error in response…
fredmaggiowski Dec 3, 2024
58c50bc
test: null logger
fredmaggiowski Dec 3, 2024
6fe8c42
golang upgrade
fredmaggiowski Dec 3, 2024
76bd7b8
dump tests
fredmaggiowski Dec 3, 2024
498be87
Update service/opa_transport_test.go
fredmaggiowski Dec 3, 2024
f69eea8
chore: restore go mod
fredmaggiowski Dec 3, 2024
25ff45d
test audit factory
fredmaggiowski Dec 3, 2024
8e9c277
refactir: map[string]any
fredmaggiowski Dec 3, 2024
17af74c
Apply suggestions from code review
fredmaggiowski Dec 3, 2024
62d9a99
fix: copyright
fredmaggiowski Dec 3, 2024
99a357b
refactor: move audit package to interanl
fredmaggiowski Dec 3, 2024
6f85293
test: coverage for core custom builtins
fredmaggiowski Dec 3, 2024
667ea71
refactor: append instead of for-loop
fredmaggiowski Dec 3, 2024
f63ab87
refactor: import
fredmaggiowski Dec 3, 2024
9c1bdfd
test: unit agent log
fredmaggiowski Dec 3, 2024
86e3dac
feat: trace policy failures as well
fredmaggiowski Dec 3, 2024
e2a143a
rem: enabled flag
fredmaggiowski Dec 3, 2024
11d2717
rem: enabled flag
fredmaggiowski Dec 3, 2024
a75053e
fix: omitempty + refactor moved code
fredmaggiowski Dec 3, 2024
031747e
add extra unit
fredmaggiowski Dec 3, 2024
6d38186
more coverage
fredmaggiowski Dec 3, 2024
5055223
fix: trace request verb and path
fredmaggiowski Dec 4, 2024
96872ce
test case for requrst policy
fredmaggiowski Dec 4, 2024
4a7f695
test case for requrst policy
fredmaggiowski Dec 4, 2024
6f7d8e1
more test
fredmaggiowski Dec 4, 2024
3bdbbef
more tests
fredmaggiowski Dec 4, 2024
3e74554
refactor: constant fr audit tag
fredmaggiowski Dec 5, 2024
ceee236
fix: trace enabled for service
fredmaggiowski Dec 5, 2024
93ecebb
Merge branch 'main' into feat/audit-trail-in-stdout-logs
fredmaggiowski Dec 5, 2024
935cd76
target service name
fredmaggiowski Dec 5, 2024
eebc0e4
log service name + user agent
fredmaggiowski Dec 5, 2024
5c92be6
refactor: use constant for user-agent
fredmaggiowski Dec 5, 2024
ef1c3bf
refactor: use constant for user-agent
fredmaggiowski Dec 5, 2024
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
22 changes: 22 additions & 0 deletions audit/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2024 Mia srl
//
// 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 "context"

type Agent interface {
Trace(context.Context, Audit)
Cache() AuditCache
}
49 changes: 49 additions & 0 deletions audit/agent_log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2024 Mia srl
//
// 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 (
"context"
"fmt"

"github.com/rond-authz/rond/logging"
)

type logAgent struct {
l logging.Logger
cache AuditCache
}

func NewLogAgent(l logging.Logger) Agent {
return &logAgent{
l: l,
cache: &SingleRecordCache{},
}
}

func (a *logAgent) Trace(ctx context.Context, auditData Audit) {
data := a.cache.Load("")
auditData.generateID()
if data != nil {
auditData.applyDataFromPolicy(data)
}

fmt.Printf("ADDITIONAL DATA IN TRACE %+v\n", data)
a.l.WithField("trail", toMap(auditData)).Info("audit trail")
}

func (a *logAgent) Cache() AuditCache {
return a.cache
}
47 changes: 47 additions & 0 deletions audit/agent_log_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2024 Mia srl
//
// 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 (
"context"
"testing"

rondlogrus "github.com/rond-authz/rond/logging/logrus"
"github.com/stretchr/testify/require"

"github.com/sirupsen/logrus/hooks/test"
)

func TestLogAgtent(t *testing.T) {
l, hook := test.NewNullLogger()
agent := NewLogAgent(rondlogrus.NewLogger(l))

agent.Trace(context.TODO(), Audit{
AggregationID: "the-aggregation-id",
Authorization: AuthzInfo{
Allowed: true,
PolicyName: "some_policy",
},
Subject: SubjectInfo{
ID: "some user",
Groups: []string{"g1", "g2"},
},
RequestBody: []byte("some body"),
})

entries := hook.AllEntries()
require.Len(t, entries, 1)
require.Equal(t, "audit trail", entries[0].Message)
}
24 changes: 24 additions & 0 deletions audit/agent_noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2024 Mia srl
//
// 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 "context"

type noopAgent struct{}

func NewNoopAgent() Agent { return &noopAgent{} }

func (a *noopAgent) Trace(context.Context, Audit) {}
func (a *noopAgent) Cache() AuditCache { return &SingleRecordCache{} }
126 changes: 126 additions & 0 deletions audit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2024 Mia srl
//
// 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 (
"reflect"
"slices"

"github.com/google/uuid"
)

const (
AuditAdditionalDataGrantedPermissionKey = "authorization.permissions"
AuditAdditionalDataGrantedBindingKey = "authorization.binding"
)

var reservedLabelKeys = []string{
AuditAdditionalDataGrantedPermissionKey,
AuditAdditionalDataGrantedBindingKey,
}

type auditReservedFields struct {
ID string `audit:"id"`
}

type Audit struct {
auditReservedFields
AggregationID string `audit:"aggregationId"`
Authorization AuthzInfo `audit:"authorization"`
Subject SubjectInfo `audit:"subject"`
RequestBody interface{} `audit:"requestBody"`
Labels map[string]interface{} `audit:"labels"`
}

type authzinfoReservedFields struct {
GrantingPermission string `audit:"permission"`
GrantingBindingID string `audit:"binding"`
}

type AuthzInfo struct {
Allowed bool `audit:"allowed"`
PolicyName string `audit:"policyName"`
authzinfoReservedFields
}

type SubjectInfo struct {
ID string `audit:"id"`
Groups []string `audit:"groups"`
}

func (a *Audit) generateID() {
a.auditReservedFields.ID = uuid.NewString()
}

func (a *Audit) applyDataFromPolicy(data map[string]interface{}) {
grantedBinding, ok := data[AuditAdditionalDataGrantedBindingKey]
if ok && grantedBinding != nil {
str, ok := grantedBinding.(string)
if ok {
a.Authorization.authzinfoReservedFields.GrantingBindingID = str
}
}

grantedPermission, ok := data[AuditAdditionalDataGrantedPermissionKey]
if ok && grantedPermission != nil {
str, ok := grantedPermission.(string)
if ok {
a.Authorization.authzinfoReservedFields.GrantingBindingID = str
}
}

if a.Labels == nil {
a.Labels = make(map[string]interface{})
}
for k, v := range data {
if slices.Contains(reservedLabelKeys, k) {
continue
}

a.Labels[k] = v
}
}

func toMap(val interface{}) map[string]interface{} {
const tagTitle = "audit"

var data map[string]interface{} = make(map[string]interface{})
varType := reflect.TypeOf(val)
if varType.Kind() != reflect.Struct {
return nil
}

value := reflect.ValueOf(val)
for i := 0; i < varType.NumField(); i++ {
if !value.Field(i).CanInterface() {
// Skip unexported fields
continue
}
tag, ok := varType.Field(i).Tag.Lookup(tagTitle)
var fieldName string
if ok && len(tag) > 0 {
fieldName = tag
} else {
fieldName = varType.Field(i).Name
}
if varType.Field(i).Type.Kind() != reflect.Struct {
data[fieldName] = value.Field(i).Interface()
} else {
data[fieldName] = toMap(value.Field(i).Interface())
}
}

return data
}
53 changes: 53 additions & 0 deletions audit/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2024 Mia srl
//
// 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 (
"testing"

"github.com/stretchr/testify/require"
)

func TestToMap(t *testing.T) {
type SubStruct struct {
F float64 `audit:"f"`
}
type ToConvert struct {
S string `audit:"s"`
I int `audit:"i"`
St SubStruct `audit:"st"`
Sl []string `audit:"sl"`
}

c := ToConvert{
S: "val",
I: 42,
St: SubStruct{F: 4.2},
Sl: []string{"g1", "g2"},
}

result := toMap(c)
require.Equal(t,
map[string]interface{}{
"s": "val",
"i": 42,
"st": map[string]interface{}{
"f": 4.2,
},
"sl": []string{"g1", "g2"},
},
result,
)
}
52 changes: 52 additions & 0 deletions audit/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2024 Mia srl
//
// 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

type Data map[string]interface{}

type AuditCache interface {
Store(id string, d Data)
Load(id string) Data
}

// func New() AuditCache {
// return &cache{m: sync.Map{}}
// }

// type cache struct {
// m sync.Map
// }

// func (c *cache) Store(id string, d Data) {
// c.m.Store(id, d)
// }

// func (c *cache) Load(id string) Data {
// d, _ := c.m.LoadAndDelete(id)
// data, _ := d.(Data)
// return data
// }

type SingleRecordCache struct {
data Data
}

func (c *SingleRecordCache) Store(id string, d Data) {
c.data = d
}

func (c *SingleRecordCache) Load(id string) Data {
return c.data
}
Loading
Loading