mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
chore: allow organization name or uuid for audit log searching (#13721)
* chore: allow organization name or uuid for audit log searching
This commit is contained in:
@ -45,7 +45,7 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queryStr := r.URL.Query().Get("q")
|
queryStr := r.URL.Query().Get("q")
|
||||||
filter, errs := searchquery.AuditLogs(queryStr)
|
filter, errs := searchquery.AuditLogs(ctx, api.Database, queryStr)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||||
Message: "Invalid audit search query.",
|
Message: "Invalid audit search query.",
|
||||||
|
@ -177,13 +177,48 @@ func TestAuditLogs(t *testing.T) {
|
|||||||
|
|
||||||
// Using the organization selector allows the org admin to fetch audit logs
|
// Using the organization selector allows the org admin to fetch audit logs
|
||||||
alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||||
SearchQuery: fmt.Sprintf("organization_id:%s", owner.OrganizationID.String()),
|
SearchQuery: fmt.Sprintf("organization:%s", owner.OrganizationID.String()),
|
||||||
Pagination: codersdk.Pagination{
|
Pagination: codersdk.Pagination{
|
||||||
Limit: 5,
|
Limit: 5,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, alogs.AuditLogs, 1)
|
require.Len(t, alogs.AuditLogs, 1)
|
||||||
|
|
||||||
|
// Also try fetching by organization name
|
||||||
|
organization, err := orgAdmin.Organization(ctx, owner.OrganizationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
alogs, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||||
|
SearchQuery: fmt.Sprintf("organization:%s", organization.Name),
|
||||||
|
Pagination: codersdk.Pagination{
|
||||||
|
Limit: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, alogs.AuditLogs, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Organization404", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
logger := slogtest.Make(t, &slogtest.Options{
|
||||||
|
IgnoreErrors: true,
|
||||||
|
})
|
||||||
|
ctx := context.Background()
|
||||||
|
client := coderdtest.New(t, &coderdtest.Options{
|
||||||
|
Logger: &logger,
|
||||||
|
})
|
||||||
|
owner := coderdtest.CreateFirstUser(t, client)
|
||||||
|
orgAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
|
||||||
|
|
||||||
|
_, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
|
||||||
|
SearchQuery: fmt.Sprintf("organization:%s", "random-name"),
|
||||||
|
Pagination: codersdk.Pagination{
|
||||||
|
Limit: 5,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package searchquery
|
package searchquery
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -16,7 +17,9 @@ import (
|
|||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) {
|
// AuditLogs requires the database to fetch an organization by name
|
||||||
|
// to convert to organization uuid.
|
||||||
|
func AuditLogs(ctx context.Context, db database.Store, query string) (database.GetAuditLogsOffsetParams, []codersdk.ValidationError) {
|
||||||
// Always lowercase for all searches.
|
// Always lowercase for all searches.
|
||||||
query = strings.ToLower(query)
|
query = strings.ToLower(query)
|
||||||
values, errors := searchTerms(query, func(term string, values url.Values) error {
|
values, errors := searchTerms(query, func(term string, values url.Values) error {
|
||||||
@ -30,7 +33,6 @@ func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.Vali
|
|||||||
const dateLayout = "2006-01-02"
|
const dateLayout = "2006-01-02"
|
||||||
parser := httpapi.NewQueryParamParser()
|
parser := httpapi.NewQueryParamParser()
|
||||||
filter := database.GetAuditLogsOffsetParams{
|
filter := database.GetAuditLogsOffsetParams{
|
||||||
OrganizationID: parser.UUID(values, uuid.Nil, "organization_id"),
|
|
||||||
ResourceID: parser.UUID(values, uuid.Nil, "resource_id"),
|
ResourceID: parser.UUID(values, uuid.Nil, "resource_id"),
|
||||||
ResourceTarget: parser.String(values, "", "resource_target"),
|
ResourceTarget: parser.String(values, "", "resource_target"),
|
||||||
Username: parser.String(values, "", "username"),
|
Username: parser.String(values, "", "username"),
|
||||||
@ -44,6 +46,28 @@ func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.Vali
|
|||||||
if !filter.DateTo.IsZero() {
|
if !filter.DateTo.IsZero() {
|
||||||
filter.DateTo = filter.DateTo.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
filter.DateTo = filter.DateTo.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert the "organization" parameter to an organization uuid. This can require
|
||||||
|
// a database lookup.
|
||||||
|
organizationArg := parser.String(values, "", "organization")
|
||||||
|
if organizationArg != "" {
|
||||||
|
organizationID, err := uuid.Parse(organizationArg)
|
||||||
|
if err == nil {
|
||||||
|
filter.OrganizationID = organizationID
|
||||||
|
} else {
|
||||||
|
// Organization could be a name
|
||||||
|
organization, err := db.GetOrganizationByName(ctx, organizationArg)
|
||||||
|
if err != nil {
|
||||||
|
parser.Errors = append(parser.Errors, codersdk.ValidationError{
|
||||||
|
Field: "organization",
|
||||||
|
Detail: fmt.Sprintf("Organization %q either does not exist, or you are unauthorized to view it", organizationArg),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
filter.OrganizationID = organization.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
parser.ErrorExcessParams(values)
|
parser.ErrorExcessParams(values)
|
||||||
return filter, parser.Errors
|
return filter, parser.Errors
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package searchquery_test
|
package searchquery_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
|
"github.com/coder/coder/v2/coderd/database/dbmem"
|
||||||
"github.com/coder/coder/v2/coderd/searchquery"
|
"github.com/coder/coder/v2/coderd/searchquery"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
)
|
)
|
||||||
@ -315,7 +317,10 @@ func TestSearchAudit(t *testing.T) {
|
|||||||
c := c
|
c := c
|
||||||
t.Run(c.Name, func(t *testing.T) {
|
t.Run(c.Name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
values, errs := searchquery.AuditLogs(c.Query)
|
// Do not use a real database, this is only used for an
|
||||||
|
// organization lookup.
|
||||||
|
db := dbmem.New()
|
||||||
|
values, errs := searchquery.AuditLogs(context.Background(), db, c.Query)
|
||||||
if c.ExpectedErrorContains != "" {
|
if c.ExpectedErrorContains != "" {
|
||||||
require.True(t, len(errs) > 0, "expect some errors")
|
require.True(t, len(errs) > 0, "expect some errors")
|
||||||
var s strings.Builder
|
var s strings.Builder
|
||||||
|
Reference in New Issue
Block a user