Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore: add sql filter to fetching audit logs #14070

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 8 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 43 additions & 43 deletions coderd/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,17 @@ func (api *API) convertAuditLogs(ctx context.Context, dblogs []database.GetAudit
}

func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogsOffsetRow) codersdk.AuditLog {
ip, _ := netip.AddrFromSlice(dblog.Ip.IPNet.IP)
ip, _ := netip.AddrFromSlice(dblog.AuditLog.Ip.IPNet.IP)

diff := codersdk.AuditDiff{}
_ = json.Unmarshal(dblog.Diff, &diff)
_ = json.Unmarshal(dblog.AuditLog.Diff, &diff)

var user *codersdk.User
if dblog.UserUsername.Valid {
// Leaving the organization IDs blank for now; not sure they are useful for
// the audit query anyway?
sdkUser := db2sdk.User(database.User{
ID: dblog.UserID,
ID: dblog.AuditLog.UserID,
Email: dblog.UserEmail.String,
Username: dblog.UserUsername.String,
CreatedAt: dblog.UserCreatedAt.Time,
Expand All @@ -211,7 +211,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
}

var (
additionalFieldsBytes = []byte(dblog.AdditionalFields)
additionalFieldsBytes = []byte(dblog.AuditLog.AdditionalFields)
additionalFields audit.AdditionalFields
err = json.Unmarshal(additionalFieldsBytes, &additionalFields)
)
Expand All @@ -224,7 +224,7 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
WorkspaceOwner: "unknown",
}

dblog.AdditionalFields, err = json.Marshal(resourceInfo)
dblog.AuditLog.AdditionalFields, err = json.Marshal(resourceInfo)
api.Logger.Error(ctx, "marshal additional fields", slog.Error(err))
}

Expand All @@ -239,30 +239,30 @@ func (api *API) convertAuditLog(ctx context.Context, dblog database.GetAuditLogs
}

alog := codersdk.AuditLog{
ID: dblog.ID,
RequestID: dblog.RequestID,
Time: dblog.Time,
ID: dblog.AuditLog.ID,
RequestID: dblog.AuditLog.RequestID,
Time: dblog.AuditLog.Time,
// OrganizationID is deprecated.
OrganizationID: dblog.OrganizationID,
OrganizationID: dblog.AuditLog.OrganizationID,
IP: ip,
UserAgent: dblog.UserAgent.String,
ResourceType: codersdk.ResourceType(dblog.ResourceType),
ResourceID: dblog.ResourceID,
ResourceTarget: dblog.ResourceTarget,
ResourceIcon: dblog.ResourceIcon,
Action: codersdk.AuditAction(dblog.Action),
UserAgent: dblog.AuditLog.UserAgent.String,
ResourceType: codersdk.ResourceType(dblog.AuditLog.ResourceType),
ResourceID: dblog.AuditLog.ResourceID,
ResourceTarget: dblog.AuditLog.ResourceTarget,
ResourceIcon: dblog.AuditLog.ResourceIcon,
Action: codersdk.AuditAction(dblog.AuditLog.Action),
Diff: diff,
StatusCode: dblog.StatusCode,
AdditionalFields: dblog.AdditionalFields,
StatusCode: dblog.AuditLog.StatusCode,
AdditionalFields: dblog.AuditLog.AdditionalFields,
User: user,
Description: auditLogDescription(dblog),
ResourceLink: resourceLink,
IsDeleted: isDeleted,
}

if dblog.OrganizationID != uuid.Nil {
if dblog.AuditLog.OrganizationID != uuid.Nil {
alog.Organization = &codersdk.MinimalOrganization{
ID: dblog.OrganizationID,
ID: dblog.AuditLog.OrganizationID,
Name: dblog.OrganizationName,
DisplayName: dblog.OrganizationDisplayName,
Icon: dblog.OrganizationIcon,
Expand All @@ -276,32 +276,32 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
b := strings.Builder{}
// NOTE: WriteString always returns a nil error, so we never check it
_, _ = b.WriteString("{user} ")
if alog.StatusCode >= 400 {
if alog.AuditLog.StatusCode >= 400 {
_, _ = b.WriteString("unsuccessfully attempted to ")
_, _ = b.WriteString(string(alog.Action))
_, _ = b.WriteString(string(alog.AuditLog.Action))
} else {
_, _ = b.WriteString(codersdk.AuditAction(alog.Action).Friendly())
_, _ = b.WriteString(codersdk.AuditAction(alog.AuditLog.Action).Friendly())
}

// API Key resources (used for authentication) do not have targets and follow the below format:
// "User {logged in | logged out | registered}"
if alog.ResourceType == database.ResourceTypeApiKey &&
(alog.Action == database.AuditActionLogin || alog.Action == database.AuditActionLogout || alog.Action == database.AuditActionRegister) {
if alog.AuditLog.ResourceType == database.ResourceTypeApiKey &&
(alog.AuditLog.Action == database.AuditActionLogin || alog.AuditLog.Action == database.AuditActionLogout || alog.AuditLog.Action == database.AuditActionRegister) {
return b.String()
}

// We don't display the name (target) for git ssh keys. It's fairly long and doesn't
// make too much sense to display.
if alog.ResourceType == database.ResourceTypeGitSshKey {
if alog.AuditLog.ResourceType == database.ResourceTypeGitSshKey {
_, _ = b.WriteString(" the ")
_, _ = b.WriteString(codersdk.ResourceType(alog.ResourceType).FriendlyString())
_, _ = b.WriteString(codersdk.ResourceType(alog.AuditLog.ResourceType).FriendlyString())
return b.String()
}

_, _ = b.WriteString(" ")
_, _ = b.WriteString(codersdk.ResourceType(alog.ResourceType).FriendlyString())
_, _ = b.WriteString(codersdk.ResourceType(alog.AuditLog.ResourceType).FriendlyString())

if alog.ResourceType == database.ResourceTypeConvertLogin {
if alog.AuditLog.ResourceType == database.ResourceTypeConvertLogin {
_, _ = b.WriteString(" to")
}

Expand All @@ -311,9 +311,9 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
}

func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.GetAuditLogsOffsetRow) bool {
switch alog.ResourceType {
switch alog.AuditLog.ResourceType {
case database.ResourceTypeTemplate:
template, err := api.Database.GetTemplateByID(ctx, alog.ResourceID)
template, err := api.Database.GetTemplateByID(ctx, alog.AuditLog.ResourceID)
if err != nil {
if xerrors.Is(err, sql.ErrNoRows) {
return true
Expand All @@ -322,7 +322,7 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
}
return template.Deleted
case database.ResourceTypeUser:
user, err := api.Database.GetUserByID(ctx, alog.ResourceID)
user, err := api.Database.GetUserByID(ctx, alog.AuditLog.ResourceID)
if err != nil {
if xerrors.Is(err, sql.ErrNoRows) {
return true
Expand All @@ -331,7 +331,7 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
}
return user.Deleted
case database.ResourceTypeWorkspace:
workspace, err := api.Database.GetWorkspaceByID(ctx, alog.ResourceID)
workspace, err := api.Database.GetWorkspaceByID(ctx, alog.AuditLog.ResourceID)
if err != nil {
if xerrors.Is(err, sql.ErrNoRows) {
return true
Expand All @@ -340,7 +340,7 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
}
return workspace.Deleted
case database.ResourceTypeWorkspaceBuild:
workspaceBuild, err := api.Database.GetWorkspaceBuildByID(ctx, alog.ResourceID)
workspaceBuild, err := api.Database.GetWorkspaceBuildByID(ctx, alog.AuditLog.ResourceID)
if err != nil {
if xerrors.Is(err, sql.ErrNoRows) {
return true
Expand All @@ -357,15 +357,15 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
}
return workspace.Deleted
case database.ResourceTypeOauth2ProviderApp:
_, err := api.Database.GetOAuth2ProviderAppByID(ctx, alog.ResourceID)
_, err := api.Database.GetOAuth2ProviderAppByID(ctx, alog.AuditLog.ResourceID)
if xerrors.Is(err, sql.ErrNoRows) {
return true
} else if err != nil {
api.Logger.Error(ctx, "unable to fetch oauth2 app", slog.Error(err))
}
return false
case database.ResourceTypeOauth2ProviderAppSecret:
_, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.ResourceID)
_, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.AuditLog.ResourceID)
if xerrors.Is(err, sql.ErrNoRows) {
return true
} else if err != nil {
Expand All @@ -378,17 +378,17 @@ func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.Get
}

func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAuditLogsOffsetRow, additionalFields audit.AdditionalFields) string {
switch alog.ResourceType {
switch alog.AuditLog.ResourceType {
case database.ResourceTypeTemplate:
return fmt.Sprintf("/templates/%s",
alog.ResourceTarget)
alog.AuditLog.ResourceTarget)

case database.ResourceTypeUser:
return fmt.Sprintf("/users?filter=%s",
alog.ResourceTarget)
alog.AuditLog.ResourceTarget)

case database.ResourceTypeWorkspace:
workspace, getWorkspaceErr := api.Database.GetWorkspaceByID(ctx, alog.ResourceID)
workspace, getWorkspaceErr := api.Database.GetWorkspaceByID(ctx, alog.AuditLog.ResourceID)
if getWorkspaceErr != nil {
return ""
}
Expand All @@ -397,13 +397,13 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
return ""
}
return fmt.Sprintf("/@%s/%s",
workspaceOwner.Username, alog.ResourceTarget)
workspaceOwner.Username, alog.AuditLog.ResourceTarget)

case database.ResourceTypeWorkspaceBuild:
if len(additionalFields.WorkspaceName) == 0 || len(additionalFields.BuildNumber) == 0 {
return ""
}
workspaceBuild, getWorkspaceBuildErr := api.Database.GetWorkspaceBuildByID(ctx, alog.ResourceID)
workspaceBuild, getWorkspaceBuildErr := api.Database.GetWorkspaceBuildByID(ctx, alog.AuditLog.ResourceID)
if getWorkspaceBuildErr != nil {
return ""
}
Expand All @@ -419,10 +419,10 @@ func (api *API) auditLogResourceLink(ctx context.Context, alog database.GetAudit
workspaceOwner.Username, additionalFields.WorkspaceName, additionalFields.BuildNumber)

case database.ResourceTypeOauth2ProviderApp:
return fmt.Sprintf("/deployment/oauth2-provider/apps/%s", alog.ResourceID)
return fmt.Sprintf("/deployment/oauth2-provider/apps/%s", alog.AuditLog.ResourceID)

case database.ResourceTypeOauth2ProviderAppSecret:
secret, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.ResourceID)
secret, err := api.Database.GetOAuth2ProviderAppSecretByID(ctx, alog.AuditLog.ResourceID)
if err != nil {
return ""
}
Expand Down
40 changes: 25 additions & 15 deletions coderd/audit_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,55 @@ func TestAuditLogDescription(t *testing.T) {
{
name: "mainline",
alog: database.GetAuditLogsOffsetRow{
Action: database.AuditActionCreate,
StatusCode: 200,
ResourceType: database.ResourceTypeWorkspace,
AuditLog: database.AuditLog{
Action: database.AuditActionCreate,
StatusCode: 200,
ResourceType: database.ResourceTypeWorkspace,
},
},
want: "{user} created workspace {target}",
},
{
name: "unsuccessful",
alog: database.GetAuditLogsOffsetRow{
Action: database.AuditActionCreate,
StatusCode: 400,
ResourceType: database.ResourceTypeWorkspace,
AuditLog: database.AuditLog{
Action: database.AuditActionCreate,
StatusCode: 400,
ResourceType: database.ResourceTypeWorkspace,
},
},
want: "{user} unsuccessfully attempted to create workspace {target}",
},
{
name: "login",
alog: database.GetAuditLogsOffsetRow{
Action: database.AuditActionLogin,
StatusCode: 200,
ResourceType: database.ResourceTypeApiKey,
AuditLog: database.AuditLog{
Action: database.AuditActionLogin,
StatusCode: 200,
ResourceType: database.ResourceTypeApiKey,
},
},
want: "{user} logged in",
},
{
name: "unsuccessful_login",
alog: database.GetAuditLogsOffsetRow{
Action: database.AuditActionLogin,
StatusCode: 401,
ResourceType: database.ResourceTypeApiKey,
AuditLog: database.AuditLog{
Action: database.AuditActionLogin,
StatusCode: 401,
ResourceType: database.ResourceTypeApiKey,
},
},
want: "{user} unsuccessfully attempted to login",
},
{
name: "gitsshkey",
alog: database.GetAuditLogsOffsetRow{
Action: database.AuditActionDelete,
StatusCode: 200,
ResourceType: database.ResourceTypeGitSshKey,
AuditLog: database.AuditLog{
Action: database.AuditActionDelete,
StatusCode: 200,
ResourceType: database.ResourceTypeGitSshKey,
},
},
want: "{user} deleted the git ssh key",
},
Expand Down
14 changes: 6 additions & 8 deletions coderd/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"testing"
"time"
Expand Down Expand Up @@ -163,19 +162,18 @@ func TestAuditLogs(t *testing.T) {
})
require.NoError(t, err)

// Fetching audit logs without an organization selector should fail
_, err = orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
// Fetching audit logs without an organization selector should only
// return organization audit logs the org admin is an admin of.
alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{
Pagination: codersdk.Pagination{
Limit: 5,
},
})
var sdkError *codersdk.Error
require.Error(t, err)
require.ErrorAsf(t, err, &sdkError, "error should be of type *codersdk.Error")
require.Equal(t, http.StatusForbidden, sdkError.StatusCode())
require.NoError(t, err)
require.Len(t, alogs.AuditLogs, 1)

// 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:%s", owner.OrganizationID.String()),
Pagination: codersdk.Pagination{
Limit: 5,
Expand Down
22 changes: 8 additions & 14 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1248,22 +1248,12 @@ func (q *querier) GetApplicationName(ctx context.Context) (string, error) {
}

func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) {
// To optimize the authz checks for audit logs, do not run an authorize
// check on each individual audit log row. In practice, audit logs are either
// fetched from a global or an organization scope.
// Applying a SQL filter would slow down the query for no benefit on how this query is
// actually used.

object := rbac.ResourceAuditLog
if arg.OrganizationID != uuid.Nil {
object = object.InOrg(arg.OrganizationID)
}

if err := q.authorizeContext(ctx, policy.ActionRead, object); err != nil {
return nil, err
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAuditLog.Type)
if err != nil {
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
}

return q.db.GetAuditLogsOffset(ctx, arg)
return q.db.GetAuthorizedAuditLogsOffset(ctx, arg, prep)
}

func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (database.GetAuthorizationUserRolesRow, error) {
Expand Down Expand Up @@ -3852,3 +3842,7 @@ func (q *querier) GetAuthorizedUsers(ctx context.Context, arg database.GetUsersP
// GetUsers is authenticated.
return q.GetUsers(ctx, arg)
}

func (q *querier) GetAuthorizedAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams, _ rbac.PreparedAuthorized) ([]database.GetAuditLogsOffsetRow, error) {
return q.GetAuditLogsOffset(ctx, arg)
}
9 changes: 8 additions & 1 deletion coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,14 @@ func (s *MethodTestSuite) TestAuditLogs() {
_ = dbgen.AuditLog(s.T(), db, database.AuditLog{})
check.Args(database.GetAuditLogsOffsetParams{
LimitOpt: 10,
}).Asserts(rbac.ResourceAuditLog, policy.ActionRead)
}).Asserts()
}))
s.Run("GetAuthorizedAuditLogsOffset", s.Subtest(func(db database.Store, check *expects) {
_ = dbgen.AuditLog(s.T(), db, database.AuditLog{})
_ = dbgen.AuditLog(s.T(), db, database.AuditLog{})
check.Args(database.GetAuditLogsOffsetParams{
LimitOpt: 10,
}, emptyPreparedAuthorized{}).Asserts()
}))
}

Expand Down
Loading
Loading