diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1e2f8fa7b1d22..56031db76d535 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8716,6 +8716,10 @@ const docTemplate = `{ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c58e0a9de10f5..06c30c1044c98 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7759,6 +7759,10 @@ } ] }, + "organization_id": { + "type": "string", + "format": "uuid" + }, "resource_id": { "type": "string", "format": "uuid" diff --git a/coderd/audit.go b/coderd/audit.go index a818f681038ed..37966ead7e916 100644 --- a/coderd/audit.go +++ b/coderd/audit.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/db2sdk" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/searchquery" @@ -61,6 +62,10 @@ func (api *API) auditLogs(rw http.ResponseWriter, r *http.Request) { } dblogs, err := api.Database.GetAuditLogsOffset(ctx, filter) + if dbauthz.IsNotAuthorizedError(err) { + httpapi.Forbidden(rw) + return + } if err != nil { httpapi.InternalServerError(rw, err) return @@ -139,6 +144,9 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { if len(params.AdditionalFields) == 0 { params.AdditionalFields = json.RawMessage("{}") } + if params.OrganizationID == uuid.Nil { + params.OrganizationID = uuid.New() + } _, err = api.Database.InsertAuditLog(ctx, database.InsertAuditLogParams{ ID: uuid.New(), @@ -155,7 +163,7 @@ func (api *API) generateFakeAuditLog(rw http.ResponseWriter, r *http.Request) { AdditionalFields: params.AdditionalFields, RequestID: uuid.Nil, // no request ID to attach this to ResourceIcon: "", - OrganizationID: uuid.New(), + OrganizationID: params.OrganizationID, }) if err != nil { httpapi.InternalServerError(rw, err) diff --git a/coderd/audit_test.go b/coderd/audit_test.go index 9de6f65071a02..198b792c44ba8 100644 --- a/coderd/audit_test.go +++ b/coderd/audit_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net/http" "strconv" "testing" "time" @@ -11,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/require" + "cdr.dev/slog/sloggers/slogtest" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" @@ -135,6 +137,54 @@ func TestAuditLogs(t *testing.T) { require.Equal(t, auditLogs.AuditLogs[0].ResourceLink, fmt.Sprintf("/@%s/%s/builds/%s", workspace.OwnerName, workspace.Name, buildNumberString)) }) + + t.Run("Organization", 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 := client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: owner.OrganizationID, + }) + require.NoError(t, err) + + // Add an extra audit log in another organization + err = client.CreateTestAuditLog(ctx, codersdk.CreateTestAuditLogRequest{ + ResourceID: owner.UserID, + OrganizationID: uuid.New(), + }) + require.NoError(t, err) + + // Fetching audit logs without an organization selector should fail + _, 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()) + + // Using the organization selector allows the org admin to fetch audit logs + alogs, err := orgAdmin.AuditLogs(ctx, codersdk.AuditLogsRequest{ + SearchQuery: fmt.Sprintf("organization_id:%s", owner.OrganizationID.String()), + Pagination: codersdk.Pagination{ + Limit: 5, + }, + }) + require.NoError(t, err) + require.Len(t, alogs.AuditLogs, 1) + }) } func TestAuditLogsFilter(t *testing.T) { diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index f32e176754fa1..ef5a3bc3185db 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -1200,12 +1200,21 @@ func (q *querier) GetApplicationName(ctx context.Context) (string, error) { } func (q *querier) GetAuditLogsOffset(ctx context.Context, arg database.GetAuditLogsOffsetParams) ([]database.GetAuditLogsOffsetRow, error) { - // To optimize audit logs, we only check the global audit log permission once. - // This is because we expect a large unbounded set of audit logs, and applying a SQL - // filter would slow down the query for no benefit. - if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil { + // 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 } + return q.db.GetAuditLogsOffset(ctx, arg) } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 04eecd5d86355..1b019cf94eccf 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1927,6 +1927,9 @@ func (q *FakeQuerier) GetAuditLogsOffset(_ context.Context, arg database.GetAudi arg.Offset-- continue } + if arg.OrganizationID != uuid.Nil && arg.OrganizationID != alog.OrganizationID { + continue + } if arg.Action != "" && !strings.Contains(string(alog.Action), arg.Action) { continue } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 4f113323a024f..84e3439250db7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -500,52 +500,58 @@ WHERE resource_id = $4 ELSE true END + -- Filter organization_id + AND CASE + WHEN $5 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = $5 + ELSE true + END -- Filter by resource_target AND CASE - WHEN $5 :: text != '' THEN - resource_target = $5 + WHEN $6 :: text != '' THEN + resource_target = $6 ELSE true END -- Filter action AND CASE - WHEN $6 :: text != '' THEN - action = $6 :: audit_action + WHEN $7 :: text != '' THEN + action = $7 :: audit_action ELSE true END -- Filter by user_id AND CASE - WHEN $7 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN - user_id = $7 + WHEN $8 :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + user_id = $8 ELSE true END -- Filter by username AND CASE - WHEN $8 :: text != '' THEN - user_id = (SELECT id FROM users WHERE lower(username) = lower($8) AND deleted = false) + WHEN $9 :: text != '' THEN + user_id = (SELECT id FROM users WHERE lower(username) = lower($9) AND deleted = false) ELSE true END -- Filter by user_email AND CASE - WHEN $9 :: text != '' THEN - users.email = $9 + WHEN $10 :: text != '' THEN + users.email = $10 ELSE true END -- Filter by date_from AND CASE - WHEN $10 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" >= $10 + WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" >= $11 ELSE true END -- Filter by date_to AND CASE - WHEN $11 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN - "time" <= $11 + WHEN $12 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + "time" <= $12 ELSE true END -- Filter by build_reason AND CASE - WHEN $12::text != '' THEN - workspace_builds.reason::text = $12 + WHEN $13::text != '' THEN + workspace_builds.reason::text = $13 ELSE true END ORDER BY @@ -561,6 +567,7 @@ type GetAuditLogsOffsetParams struct { Offset int32 `db:"offset" json:"offset"` ResourceType string `db:"resource_type" json:"resource_type"` ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` + OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` ResourceTarget string `db:"resource_target" json:"resource_target"` Action string `db:"action" json:"action"` UserID uuid.UUID `db:"user_id" json:"user_id"` @@ -611,6 +618,7 @@ func (q *sqlQuerier) GetAuditLogsOffset(ctx context.Context, arg GetAuditLogsOff arg.Offset, arg.ResourceType, arg.ResourceID, + arg.OrganizationID, arg.ResourceTarget, arg.Action, arg.UserID, diff --git a/coderd/database/queries/auditlogs.sql b/coderd/database/queries/auditlogs.sql index d05b5bbe371e0..653082bd69fee 100644 --- a/coderd/database/queries/auditlogs.sql +++ b/coderd/database/queries/auditlogs.sql @@ -59,6 +59,12 @@ WHERE resource_id = @resource_id ELSE true END + -- Filter organization_id + AND CASE + WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN + audit_logs.organization_id = @organization_id + ELSE true + END -- Filter by resource_target AND CASE WHEN @resource_target :: text != '' THEN diff --git a/coderd/members.go b/coderd/members.go index e15aa9d4821f9..24f712b8154c7 100644 --- a/coderd/members.go +++ b/coderd/members.go @@ -176,10 +176,11 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) { apiKey = httpmw.APIKey(r) auditor = api.Auditor.Load() aReq, commitAudit = audit.InitRequest[database.AuditableOrganizationMember](rw, &audit.RequestParams{ - Audit: *auditor, - Log: api.Logger, - Request: r, - Action: database.AuditActionWrite, + OrganizationID: organization.ID, + Audit: *auditor, + Log: api.Logger, + Request: r, + Action: database.AuditActionWrite, }) ) aReq.Old = member.OrganizationMember.Auditable(member.Username) diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index cef971a731cbd..ab44530cc7c4c 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -30,6 +30,7 @@ func AuditLogs(query string) (database.GetAuditLogsOffsetParams, []codersdk.Vali const dateLayout = "2006-01-02" parser := httpapi.NewQueryParamParser() filter := database.GetAuditLogsOffsetParams{ + OrganizationID: parser.UUID(values, uuid.Nil, "organization_id"), ResourceID: parser.UUID(values, uuid.Nil, "resource_id"), ResourceTarget: parser.String(values, "", "resource_target"), Username: parser.String(values, "", "username"), diff --git a/codersdk/audit.go b/codersdk/audit.go index b1d525d69179f..683db5406c13f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -161,6 +161,7 @@ type CreateTestAuditLogRequest struct { AdditionalFields json.RawMessage `json:"additional_fields,omitempty"` Time time.Time `json:"time,omitempty" format:"date-time"` BuildReason BuildReason `json:"build_reason,omitempty" enums:"autostart,autostop,initiator"` + OrganizationID uuid.UUID `json:"organization_id,omitempty" format:"uuid"` } // AuditLogs retrieves audit logs from the given page. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 6329b49ee820e..76da4e3d71196 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1179,6 +1179,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "action": "create", "additional_fields": [0], "build_reason": "autostart", + "organization_id": "7c60d51f-b44e-4682-87d6-449835ea4de6", "resource_id": "4d5215ed-38bb-48ed-879a-fdb9ca58522f", "resource_type": "template", "time": "2019-08-24T14:15:22Z" @@ -1192,6 +1193,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `action` | [codersdk.AuditAction](#codersdkauditaction) | false | | | | `additional_fields` | array of integer | false | | | | `build_reason` | [codersdk.BuildReason](#codersdkbuildreason) | false | | | +| `organization_id` | string | false | | | | `resource_id` | string | false | | | | `resource_type` | [codersdk.ResourceType](#codersdkresourcetype) | false | | | | `time` | string | false | | | diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index ef3e997f1a1e4..167b7d28f8edf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -281,6 +281,7 @@ export interface CreateTestAuditLogRequest { readonly additional_fields?: Record; readonly time?: string; readonly build_reason?: BuildReason; + readonly organization_id?: string; } // From codersdk/apikey.go pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy