Skip to content

Commit f1713e2

Browse files
Saranya-jenaHarness
authored andcommitted
chore: [ML-1370]: Added resource type and resource identifier filter in list_user_audits tool and added e2e (#158)
* chore: [ML-1370]: resolved comments Signed-off-by: Saranya-jena <[email protected]> * chore: [ML-1370]: Added resource type and resource identifier filter in list_user_audits toolm added e2e Signed-off-by: Saranya-jena <[email protected]>
1 parent 8739bd4 commit f1713e2

File tree

3 files changed

+230
-1
lines changed

3 files changed

+230
-1
lines changed

client/dto/audit.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type ListAuditEventsFilter struct {
6666
FilterType string `json:"filterType,omitempty"`
6767
StartTime int64 `json:"startTime,omitempty"`
6868
EndTime int64 `json:"endTime,omitempty"`
69+
Resources []AuditResource `json:"resources,omitempty"`
6970
}
7071

7172
type AuditOutput[T any] struct {

pkg/harness/tools/audit.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ import (
66
"fmt"
77
"log/slog"
88
"math"
9+
"strings"
910
"time"
1011

1112
"github.com/harness/harness-mcp/client"
13+
"github.com/harness/harness-mcp/client/dto"
1214
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
1315
"github.com/mark3labs/mcp-go/mcp"
1416
"github.com/mark3labs/mcp-go/server"
@@ -78,6 +80,37 @@ func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditServ
7880
"ROLE_ASSIGNMENT_CREATED", "ROLE_ASSIGNMENT_UPDATED", "ROLE_ASSIGNMENT_DELETED", "MOVE", "ENABLED", "DISABLED", "DISMISS_ANOMALY", "RERUN", "BYPASS", "STABLE_VERSION_CHANGED",
7981
"SYNC_START", "START_IMPERSONATION", "END_IMPERSONATION", "MOVE_TO_GIT", "FREEZE_BYPASS", "EXPIRED", "FORCE_PUSH"),
8082
),
83+
84+
mcp.WithString("resource_type",
85+
mcp.Description("Optional resource type to filter by."),
86+
mcp.Enum("ORGANIZATION", "PROJECT", "USER_GROUP", "SECRET", "CERTIFICATE", "STREAMING_DESTINATION", "RESOURCE_GROUP",
87+
"USER", "ROLE", "PIPELINE", "TRIGGER", "TEMPLATE", "INPUT_SET", "DELEGATE_CONFIGURATION", "DELEGATE_GROUPS",
88+
"SERVICE", "ENVIRONMENT", "ENVIRONMENT_GROUP", "DELEGATE", "SERVICE_ACCOUNT", "CONNECTOR", "API_KEY",
89+
"TOKEN", "DELEGATE_TOKEN", "DASHBOARD", "DASHBOARD_FOLDER", "GOVERNANCE_POLICY", "GOVERNANCE_POLICY_SET",
90+
"VARIABLE", "CHAOS_HUB", "MONITORED_SERVICE", "CHAOS_INFRASTRUCTURE", "CHAOS_EXPERIMENT", "CHAOS_GAMEDAY",
91+
"CHAOS_PROBE", "STO_TARGET", "STO_EXEMPTION", "SERVICE_LEVEL_OBJECTIVE", "PERSPECTIVE", "PERSPECTIVE_BUDGET",
92+
"PERSPECTIVE_REPORT", "COST_CATEGORY", "COMMITMENT_ORCHESTRATOR_SETUP", "COMMITMENT_ACTIONS",
93+
"CLUSTER_ORCHESTRATOR_SETUP", "CLUSTER_ACTIONS", "SMTP", "PERSPECTIVE_FOLDER", "AUTOSTOPPING_RULE",
94+
"AUTOSTOPPING_LB", "AUTOSTOPPING_STARTSTOP", "SETTING", "NG_LOGIN_SETTINGS", "DEPLOYMENT_FREEZE",
95+
"CLOUD_ASSET_GOVERNANCE_RULE", "CLOUD_ASSET_GOVERNANCE_RULE_SET", "CLOUD_ASSET_GOVERNANCE_RULE_ENFORCEMENT",
96+
"TARGET_GROUP", "FEATURE_FLAG", "FEATURE_FLAG_STALE_CONFIG", "NG_ACCOUNT_DETAILS", "BUDGET_GROUP",
97+
"IP_ALLOWLIST_CONFIG", "NETWORK_MAP", "CET_AGENT_TOKEN", "CET_CRITICAL_EVENT", "CHAOS_SECURITY_GOVERNANCE",
98+
"END_USER_LICENSE_AGREEMENT", "WORKSPACE", "IAC_MODULE", "SEI_CONFIGURATION_SETTINGS", "SEI_COLLECTIONS",
99+
"SEI_INSIGHTS", "SEI_PANORAMA", "CET_SAVED_FILTER", "GITOPS_AGENT", "GITOPS_REPOSITORY", "GITOPS_CLUSTER",
100+
"GITOPS_CREDENTIAL_TEMPLATE", "GITOPS_REPOSITORY_CERTIFICATE", "GITOPS_GNUPG_KEY", "GITOPS_PROJECT_MAPPING",
101+
"GITOPS_APPLICATION", "GITOPS_APPLICATION_SET", "CODE_REPOSITORY", "CODE_REPOSITORY_SETTINGS", "CODE_BRANCH_RULE",
102+
"CODE_PUSH_RULE", "CODE_TAG_RULE", "CODE_BRANCH", "CODE_TAG", "CODE_WEBHOOK", "MODULE_LICENSE",
103+
"IDP_BACKSTAGE_CATALOG_ENTITY", "IDP_BACKSTAGE_SCAFFOLDER_TASK", "IDP_APP_CONFIGS", "IDP_CONFIG_ENV_VARIABLES",
104+
"IDP_PROXY_HOST", "IDP_SCORECARDS", "IDP_CHECKS", "IDP_ALLOW_LIST", "IDP_OAUTH_CONFIG", "IDP_CATALOG_CONNECTOR",
105+
"IDP_GIT_INTEGRATIONS", "IDP_PERMISSIONS", "IDP_CATALOG", "IDP_WORKFLOW", "SERVICE_DISCOVERY_AGENT",
106+
"APPLICATION_MAP", "IDP_LAYOUT", "IDP_PLUGINS", "NOTIFICATION_CHANNEL", "NOTIFICATION_RULE",
107+
"IDP_CATALOG_CUSTOM_PROPERTIES", "CLOUD_ASSET_GOVERNANCE_RULE_EVALUATION", "ARTIFACT_REGISTRY_UPSTREAM_PROXY",
108+
"ARTIFACT_REGISTRY", "BANNER", "GITX_WEBHOOK", "FILE", "CHAOS_IMAGE_REGISTRY", "DB_SCHEMA", "DB_INSTANCE",
109+
"CCM_ANOMALY", "CCM_ANOMALY_ALERT", "CLOUD_ASSET_GOVERNANCE_NOTIFICATION", "CDE_GITSPACE", "DEFAULT_NOTIFICATION_TEMPLATE_SET"),
110+
),
111+
mcp.WithString("resource_identifier",
112+
mcp.Description("Optional resource identifier to filter by. Must be used with resource_type."),
113+
),
81114
WithScope(config, false),
82115
WithPagination(),
83116
),
@@ -94,6 +127,16 @@ func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditServ
94127
return mcp.NewToolResultError(err.Error()), nil
95128
}
96129

130+
resourceType, err := OptionalParam[string](request, "resource_type")
131+
if err != nil {
132+
return mcp.NewToolResultError(err.Error()), nil
133+
}
134+
135+
resourceIdentifier, err := OptionalParam[string](request, "resource_identifier")
136+
if err != nil {
137+
return mcp.NewToolResultError(err.Error()), nil
138+
}
139+
97140
scope, err := FetchScope(config, request, false)
98141
if err != nil {
99142
return mcp.NewToolResultError(err.Error()), nil
@@ -115,15 +158,32 @@ func ListUserAuditTrailTool(config *config.Config, auditClient *client.AuditServ
115158
endTimeMilliseconds := convertDateToMilliseconds(endTime)
116159
slog.Info("Converted time range", "start_time_ms", startTimeMilliseconds, "end_time_ms", endTimeMilliseconds)
117160

161+
// Create filter options
162+
opts := &dto.ListAuditEventsFilter{}
163+
164+
// Set default filter type
165+
opts.FilterType = "Audit"
166+
167+
// Add resource filter if provided
168+
if strings.TrimSpace(resourceType) != "" {
169+
opts.Resources = []dto.AuditResource{{
170+
Type: resourceType,
171+
Identifier: resourceIdentifier,
172+
}}
173+
slog.Info("Adding resource filter", "resource_type", resourceType, "resource_identifier", resourceIdentifier)
174+
}
175+
118176
slog.Info("Calling ListUserAuditTrail API",
119177
"user_id_list", userIDList,
120178
"actions", actionsList,
179+
"resource_type", resourceType,
180+
"resource_identifier", resourceIdentifier,
121181
"page", page,
122182
"size", size,
123183
"start_time_ms", startTimeMilliseconds,
124184
"end_time_ms", endTimeMilliseconds)
125185

126-
data, err := auditClient.ListUserAuditTrail(ctx, scope, userIDList, actionsList, page, size, startTimeMilliseconds, endTimeMilliseconds, nil)
186+
data, err := auditClient.ListUserAuditTrail(ctx, scope, userIDList, actionsList, page, size, startTimeMilliseconds, endTimeMilliseconds, opts)
127187
if err != nil {
128188
return nil, fmt.Errorf("failed to list the audit logs: %w", err)
129189
}

test/e2e/audit_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//go:build e2e
2+
3+
package e2e
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
"testing"
11+
"time"
12+
13+
"github.com/harness/harness-mcp/client/dto"
14+
"github.com/mark3labs/mcp-go/mcp"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// TestListAuditTools verifies that the list_user_audits tool is available
19+
func TestListAuditTools(t *testing.T) {
20+
t.Parallel()
21+
22+
mcpClient := setupMCPClient(t)
23+
ctx := context.Background()
24+
25+
// List available tools
26+
request := mcp.ListToolsRequest{}
27+
response, err := mcpClient.ListTools(ctx, request)
28+
require.NoError(t, err, "expected to list tools successfully")
29+
30+
// Check that audit tools are available
31+
var foundAuditTools = make(map[string]string)
32+
auditToolPatterns := []string{
33+
"list_user_audits",
34+
}
35+
36+
// Print all available tools
37+
fmt.Println("Available tools in TestListAuditTools:")
38+
for _, tool := range response.Tools {
39+
fmt.Printf("- %s\n", tool.Name)
40+
41+
// Check if this tool matches any of our patterns
42+
for _, pattern := range auditToolPatterns {
43+
if strings.Contains(strings.ToLower(tool.Name), pattern) {
44+
foundAuditTools[pattern] = tool.Name
45+
break
46+
}
47+
}
48+
}
49+
50+
// Print what we found
51+
fmt.Println("Found audit tools:")
52+
for pattern, actualName := range foundAuditTools {
53+
fmt.Printf("- %s -> %s\n", pattern, actualName)
54+
}
55+
56+
// Check if we found the list_user_audits tool
57+
require.Contains(t, foundAuditTools, "list_user_audits", "expected to find list_user_audits tool")
58+
}
59+
60+
// TestListUserAuditsWithResourceFilter verifies that the list_user_audits tool works correctly with resource filtering
61+
func TestListUserAuditsWithResourceFilter(t *testing.T) {
62+
t.Parallel()
63+
64+
mcpClient := setupMCPClient(t)
65+
ctx := context.Background()
66+
accountID := getE2EAccountID(t)
67+
68+
// Get current time and one week ago for time range
69+
now := time.Now().UTC()
70+
oneWeekAgo := now.AddDate(0, 0, -7)
71+
72+
// Format times in ISO 8601 format
73+
startTime := oneWeekAgo.Format(time.RFC3339)
74+
endTime := now.Format(time.RFC3339)
75+
76+
// Call the list_user_audits tool with resource filter
77+
request := mcp.CallToolRequest{}
78+
request.Params.Name = "list_user_audits"
79+
request.Params.Arguments = map[string]any{
80+
"accountIdentifier": accountID,
81+
"orgIdentifier": getE2EOrgID(),
82+
"projectIdentifier": getE2EProjectID(),
83+
"page": 0,
84+
"size": 10,
85+
"start_time": startTime,
86+
"end_time": endTime,
87+
"resource_type": "PIPELINE",
88+
}
89+
90+
response, err := mcpClient.CallTool(ctx, request)
91+
require.NoError(t, err, "expected to call 'list_user_audits' tool successfully")
92+
if response.IsError {
93+
t.Logf("Error response: %v", response.Content)
94+
}
95+
require.False(t, response.IsError, "expected result not to be an error")
96+
97+
// Verify response content
98+
require.NotEmpty(t, response.Content, "expected content to not be empty")
99+
100+
// Parse the response to extract audit events
101+
textContent, ok := response.Content[0].(mcp.TextContent)
102+
require.True(t, ok, "expected content to be of type TextContent")
103+
104+
var auditResponse dto.AuditOutput[dto.AuditListItem]
105+
err = json.Unmarshal([]byte(textContent.Text), &auditResponse)
106+
require.NoError(t, err, "expected to unmarshal response successfully")
107+
108+
// Verify the response structure
109+
require.Equal(t, "SUCCESS", auditResponse.Status, "expected status to be SUCCESS")
110+
t.Logf("Found %d audit events", len(auditResponse.Data.Content))
111+
112+
// Verify that all returned audit events are for the specified resource
113+
for _, audit := range auditResponse.Data.Content {
114+
require.Equal(t, "PIPELINE", audit.Resource.Type, "expected resource type to be PIPELINE")
115+
}
116+
}
117+
118+
// TestListUserAuditsWithoutResourceFilter verifies that the list_user_audits tool works correctly without resource filtering
119+
func TestListUserAuditsWithoutResourceFilter(t *testing.T) {
120+
t.Parallel()
121+
122+
mcpClient := setupMCPClient(t)
123+
ctx := context.Background()
124+
accountID := getE2EAccountID(t)
125+
126+
// Get current time and one week ago for time range
127+
now := time.Now().UTC()
128+
threeWeeksAgo := now.AddDate(0, 0, -21)
129+
130+
// Format times in ISO 8601 format
131+
startTime := threeWeeksAgo.Format(time.RFC3339)
132+
endTime := now.Format(time.RFC3339)
133+
134+
// Call the list_user_audits tool without resource filter
135+
request := mcp.CallToolRequest{}
136+
request.Params.Name = "list_user_audits"
137+
request.Params.Arguments = map[string]any{
138+
"accountIdentifier": accountID,
139+
"orgIdentifier": getE2EOrgID(),
140+
"projectIdentifier": getE2EProjectID(),
141+
"page": 0,
142+
"size": 10,
143+
"start_time": startTime,
144+
"end_time": endTime,
145+
}
146+
147+
response, err := mcpClient.CallTool(ctx, request)
148+
require.NoError(t, err, "expected to call 'list_user_audits' tool successfully")
149+
if response.IsError {
150+
t.Logf("Error response: %v", response.Content)
151+
}
152+
require.False(t, response.IsError, "expected result not to be an error")
153+
154+
// Verify response content
155+
require.NotEmpty(t, response.Content, "expected content to not be empty")
156+
157+
// Parse the response to extract audit events
158+
textContent, ok := response.Content[0].(mcp.TextContent)
159+
require.True(t, ok, "expected content to be of type TextContent")
160+
161+
var auditResponse dto.AuditOutput[dto.AuditListItem]
162+
err = json.Unmarshal([]byte(textContent.Text), &auditResponse)
163+
require.NoError(t, err, "expected to unmarshal response successfully")
164+
165+
// Verify the response structure
166+
require.Equal(t, "SUCCESS", auditResponse.Status, "expected status to be SUCCESS")
167+
t.Logf("Found %d audit events", len(auditResponse.Data.Content))
168+
}

0 commit comments

Comments
 (0)