-
Notifications
You must be signed in to change notification settings - Fork 9
New MCP tool for retrieving user audit trails from Harness #16
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
base: master
Are you sure you want to change the base?
Changes from 4 commits
5036bab
5ba4a4d
3d82f2e
0f7928d
cc39603
17cafba
0112402
d6d50ab
5abbe91
40fda55
8cb2bc3
291fc2a
b7c60b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
params["accountIdentifier"] = scope.AccountID | ||
params["pageIndex"] = fmt.Sprintf("%d", page) | ||
params["pageSize"] = fmt.Sprintf("%d", size) | ||
|
||
addScope(scope, params) | ||
|
||
// Required fields | ||
opts.FilterType = "Audit" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what purpose does this filtertype serve ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
abhishek-singh0710 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
opts.EndTime = endTime | ||
|
||
resp := &dto.AuditOutput[dto.AuditListItem]{} | ||
err := a.Client.Post(ctx, "gateway/audit/api/audits/list", params, opts, resp) | ||
abhishek-singh0710 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to list pipeline executions by user: %w", err) | ||
abhishek-singh0710 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return resp, nil | ||
} |
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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does empty searchterm mean ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is insertId ? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"` | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. see if math package can solve your requirements There was a problem hiding this comment. Choose a reason for hiding this commentThe 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."), | ||
abhishek-singh0710 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
abhishek-singh0710 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} | ||
} |
There was a problem hiding this comment.
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
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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.