Skip to content
50 changes: 50 additions & 0 deletions client/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package client

import (
"fmt"
"context"
"github.com/harness/harness-mcp/client/dto"
)

type AuditService struct {
Client *Client
}

// ListUserAuditTrail fetches the audit trail of a specific user.
func (a *AuditService) ListUserAuditTrail(ctx context.Context, scope dto.Scope, userID string, page int, size int, startTime int64, endTime int64, opts *dto.ListAuditEventsFilter) (*dto.AuditOutput[dto.AuditListItem], error) {
if opts == nil {
opts = &dto.ListAuditEventsFilter{}
}

params := make(map[string]string)
params["routingId"] = scope.AccountID

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its best to standardize routingId, accountIdentifier etc as consts across all clients

Copy link
Author

@abhishek-singh0710 abhishek-singh0710 Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the other code files, I've kept this separate in the audit file.

params["accountIdentifier"] = scope.AccountID
params["pageIndex"] = fmt.Sprintf("%d", page)
params["pageSize"] = fmt.Sprintf("%d", size)

addScope(scope, params)

// Required fields
opts.FilterType = "Audit"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what purpose does this filtertype serve ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a field as mentioned in the API Documentation.

opts.Principals = []dto.AuditPrincipal{{
Type: "USER",
Identifier: userID,
}}

opts.Scopes = []dto.AuditResourceScope{{
AccountIdentifier: scope.AccountID,
OrgIdentifier: scope.OrgID,
ProjectIdentifier: scope.ProjectID,
}}

opts.StartTime = startTime
opts.EndTime = endTime

resp := &dto.AuditOutput[dto.AuditListItem]{}
err := a.Client.Post(ctx, "gateway/audit/api/audits/list", params, opts, resp)
if err != nil {
return nil, fmt.Errorf("failed to list pipeline executions by user: %w", err)
}

return resp, nil
}
81 changes: 81 additions & 0 deletions client/dto/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dto

type AuditPrincipal struct {
Type string `json:"type"`
Identifier string `json:"identifier"`
}

type AuditPaginationOptions struct {
Page int `json:"page,omitempty"`
Size int `json:"size,omitempty"`
}

// AuditListOptions represents the options for listing pipelines
type AuditListOptions struct {
AuditPaginationOptions
SearchTerm string `json:"searchTerm,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does empty searchterm mean ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire struct has been removed.

}

type AuditResourceScope struct {
AccountIdentifier string `json:"accountIdentifier"`
OrgIdentifier string `json:"orgIdentifier,omitempty"`
ProjectIdentifier string `json:"projectIdentifier,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Audits are only available at account and org level. Is project needed for filtering ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be mentioned inside the scope field as mentioned in the API Documentation.

}

type ListAuditEventsFilter struct {
Scopes []AuditResourceScope `json:"scopes,omitempty"`
Principals []AuditPrincipal `json:"principals,omitempty"`
Actions []string `json:"actions,omitempty"`
FilterType string `json:"filterType,omitempty"`
StartTime int64 `json:"startTime,omitempty"`
EndTime int64 `json:"endTime,omitempty"`
Modules []string `json:"modules,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure to match terminology in UI . e.g. Modules is not clear to end user
Indent properly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed that since post-filtering is being done.

}


type AuditOutput[T any] struct {
Status string `json:"status,omitempty"`
Data AuditOutputData[T] `json:"data,omitempty"`
}

type AuditOutputData[T any] struct {
PageItemCount int `json:"pageItemCount,omitempty"`
PageSize int `json:"pageSize,omitempty"`
Content []T `json:"content,omitempty"`
PageIndex int `json:"pageIndex,omitempty"`
HasNext bool `json:"hasNext,omitempty"`
PageToken string `json:"pageToken,omitempty"`
TotalItems int `json:totalItems, omitempty`
TotalPages int `json:totalPages, omitempty`
}

type AuditListItem struct {
AuditID string `json:"auditId,omitempty"`
InsertId string `json:"insertId,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is insertId ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire struct has been removed.

Resource AuditResource `json:"resource,omitempty"`
Action string `json:"action,omitempty"`
Module string `json:"module,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
AuthenticationInfo AuditAuthenticationInfo `json:"authenticationInfo,omitempty"`
ResourceScope AuditResourceScope `json:"resourceScope,omitempty"`
}

type AuditResource struct {
Type string `json:"type"`
Identifier string `json:"identifier"`
Labels AuditResourceLabels `json:"labels,omitempty"`
}

type AuditResourceLabels struct {
ResourceName string `json:"resourceName,omitempty"`
}

type AuditAuthenticationInfo struct {
Principal AuditPrincipal `json:"principal"`
Labels AuditAuthenticationInfoLabels `json:"labels,omitempty"`
}

type AuditAuthenticationInfoLabels struct {
UserID string `json:"userId,omitempty"`
Username string `json:"username,omitempty"`
}
85 changes: 85 additions & 0 deletions pkg/harness/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package harness

import (
"context"
"encoding/json"
"fmt"
"github.com/harness/harness-mcp/client"
// "github.com/harness/harness-mcp/client/dto"
"github.com/harness/harness-mcp/cmd/harness-mcp-server/config"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"time"
)

const (
minPage = 0
maxPage = 100
minSize = 1
maxSize = 100
)

func maxMin(val, min, max int) int {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see if math package can solve your requirements

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used the math package to clamp the values.

if(val < min) {
return min
}
if(val > max) {
return max
}
return val
}

// ListUserAuditTrail creates a tool for listing the audit trail of a specific user.
func ListUserAuditTrail(config *config.Config, auditClient *client.AuditService) (tool mcp.Tool, handler server.ToolHandlerFunc) {
return mcp.NewTool("list_user_audits",
mcp.WithDescription("List the audit trail of the user."),
mcp.WithString("user_id",
mcp.Required(),
mcp.Description("The user id used to retrieve the audit trail."),
),
mcp.WithNumber("start_time",
mcp.Description("Optional start time in milliseconds"),
mcp.DefaultNumber(0),
),
mcp.WithNumber("end_time",
mcp.Description("Optional end time in milliseconds"),
mcp.DefaultNumber(float64(time.Now().UnixMilli())),
),
WithScope(config, true),
WithPagination(),
),
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
userID, err := requiredParam[string](request, "user_id")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

scope, err := fetchScope(config, request, true)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

page, size, err := fetchPagination(request)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

page = maxMin(page, minPage, maxPage)
size = maxMin(size, minSize, maxSize)

startTime, _ := OptionalParam[int64](request, "start_time")
endTime, _ := OptionalParam[int64](request, "end_time")

data, err := auditClient.ListUserAuditTrail(ctx, scope, userID, page, size, startTime, endTime, nil)
if err != nil {
return nil, fmt.Errorf("failed to list the audit logs: %w", err)
}

r, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("failed to marshal the audit logs: %w", err)
}

return mcp.NewToolResultText(string(r)), nil
}
}