mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
feat(coderd): list workspace secrets on workspace build audit entries
This commit is contained in:
@@ -22,6 +22,9 @@ type AdditionalFields struct {
|
||||
BuildReason database.BuildReason `json:"build_reason"`
|
||||
WorkspaceOwner string `json:"workspace_owner"`
|
||||
WorkspaceID uuid.UUID `json:"workspace_id"`
|
||||
// WorkspaceSecrets lists the secrets linked to a workspace build. Set
|
||||
// only on workspace build entries.
|
||||
WorkspaceSecrets []WorkspaceBuildSecret `json:"workspace_secrets,omitempty"`
|
||||
}
|
||||
|
||||
func NewNop() Auditor {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
)
|
||||
|
||||
// WorkspaceBuildSecret is the metadata of a secret linked to a workspace
|
||||
// build. Secret values are never audited.
|
||||
type WorkspaceBuildSecret struct {
|
||||
Name string `json:"name"`
|
||||
EnvName string `json:"env_name"`
|
||||
FilePath string `json:"file_path"`
|
||||
Source database.WorkspaceSecretSource `json:"source"`
|
||||
}
|
||||
|
||||
// WorkspaceBuildFields returns the additional fields for a workspace build
|
||||
// audit entry, including the full set of secrets linked to the build.
|
||||
// Removed secrets have no row, so they are absent from the set. If the
|
||||
// secrets cannot be listed, the other fields are still returned with the
|
||||
// error.
|
||||
func WorkspaceBuildFields(ctx context.Context, db database.Store, workspace database.Workspace, build database.WorkspaceBuild) (AdditionalFields, error) {
|
||||
fields := AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10),
|
||||
BuildReason: build.Reason,
|
||||
WorkspaceID: workspace.ID,
|
||||
}
|
||||
|
||||
//nolint:gocritic // Audit entries list secret metadata for every build initiator; values are never read.
|
||||
rows, err := db.GetWorkspaceSecretsHistory(dbauthz.AsWorkspaceSecretManager(ctx), database.GetWorkspaceSecretsHistoryParams{
|
||||
WorkspaceID: workspace.ID,
|
||||
WorkspaceBuildID: build.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fields, xerrors.Errorf("list workspace build secrets: %w", err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
fields.WorkspaceSecrets = append(fields.WorkspaceSecrets, WorkspaceBuildSecret{
|
||||
Name: row.Name,
|
||||
EnvName: row.EnvName,
|
||||
FilePath: row.FilePath,
|
||||
Source: row.Source,
|
||||
})
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
@@ -1500,13 +1500,9 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
|
||||
previousBuild = database.WorkspaceBuild{}
|
||||
}
|
||||
|
||||
// We pass the below information to the Auditor so that it
|
||||
// can form a friendly string for the user to view in the UI.
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(build.BuildNumber), 10),
|
||||
BuildReason: database.BuildReason(string(build.Reason)),
|
||||
WorkspaceID: workspace.ID,
|
||||
buildResourceInfo, err := audit.WorkspaceBuildFields(ctx, s.Database, workspace, build)
|
||||
if err != nil {
|
||||
s.Logger.Error(ctx, "audit log - workspace build fields", slog.Error(err))
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(buildResourceInfo)
|
||||
@@ -2497,13 +2493,9 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
|
||||
previousBuild = database.WorkspaceBuild{}
|
||||
}
|
||||
|
||||
// We pass the below information to the Auditor so that it
|
||||
// can form a friendly string for the user to view in the UI.
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.FormatInt(int64(workspaceBuild.BuildNumber), 10),
|
||||
BuildReason: database.BuildReason(string(workspaceBuild.Reason)),
|
||||
WorkspaceID: workspace.ID,
|
||||
buildResourceInfo, err := audit.WorkspaceBuildFields(ctx, s.Database, workspace, workspaceBuild)
|
||||
if err != nil {
|
||||
s.Logger.Error(ctx, "audit log - workspace build fields", slog.Error(err))
|
||||
}
|
||||
|
||||
wriBytes, err := json.Marshal(buildResourceInfo)
|
||||
|
||||
@@ -595,13 +595,11 @@ func (api *API) postWorkspaceBuildsInternal(
|
||||
slog.F("workspace_build_id", workspaceBuild.ID),
|
||||
slog.F("provisioner_job_id", provisionerJob.ID),
|
||||
)
|
||||
buildResourceInfo := audit.AdditionalFields{
|
||||
WorkspaceName: workspace.Name,
|
||||
BuildNumber: strconv.Itoa(int(workspaceBuild.BuildNumber)),
|
||||
BuildReason: workspaceBuild.Reason,
|
||||
WorkspaceID: workspace.ID,
|
||||
WorkspaceOwner: workspace.OwnerName,
|
||||
buildResourceInfo, err := audit.WorkspaceBuildFields(ctx, api.Database, workspace, *workspaceBuild)
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to list workspace build secrets for audit", slog.Error(err))
|
||||
}
|
||||
buildResourceInfo.WorkspaceOwner = workspace.OwnerName
|
||||
briBytes, err := json.Marshal(buildResourceInfo)
|
||||
if err != nil {
|
||||
api.Logger.Error(ctx, "failed to marshal build resource info for audit", slog.Error(err))
|
||||
|
||||
@@ -7124,3 +7124,62 @@ func TestWorkspaceBuildSecretsFilePathBlocked(t *testing.T) {
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestWorkspaceSecretsAudit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auditor := audit.NewMock()
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// buildSecrets returns the secrets listed on the audit entry for a build.
|
||||
// The entry is written after the job completes, so wait for it.
|
||||
buildSecrets := func(t *testing.T, buildID uuid.UUID) []audit.WorkspaceBuildSecret {
|
||||
t.Helper()
|
||||
var entry database.AuditLog
|
||||
testutil.Eventually(ctx, t, func(context.Context) bool {
|
||||
for _, log := range auditor.AuditLogs() {
|
||||
if log.ResourceType == database.ResourceTypeWorkspaceBuild && log.ResourceID == buildID {
|
||||
entry = log
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}, testutil.IntervalFast, "audit entry for build %s", buildID)
|
||||
require.NotContains(t, string(entry.AdditionalFields), "secret-value", "values must never be audited")
|
||||
var fields audit.AdditionalFields
|
||||
require.NoError(t, json.Unmarshal(entry.AdditionalFields, &fields))
|
||||
return fields.WorkspaceSecrets
|
||||
}
|
||||
|
||||
workspace := coderdtest.CreateWorkspace(t, client, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
|
||||
req.Secrets = []codersdk.WorkspaceSecretInput{
|
||||
{Name: "api-key", Value: new("secret-value"), EnvName: "API_KEY"},
|
||||
{Name: "cert", Value: new("secret-value"), FilePath: "/home/coder/.cert"},
|
||||
}
|
||||
})
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
require.ElementsMatch(t, []audit.WorkspaceBuildSecret{
|
||||
{Name: "api-key", EnvName: "API_KEY", Source: database.WorkspaceSecretSourceRequest},
|
||||
{Name: "cert", FilePath: "/home/coder/.cert", Source: database.WorkspaceSecretSourceRequest},
|
||||
}, buildSecrets(t, workspace.LatestBuild.ID))
|
||||
|
||||
// The entry lists the build's full set. Removed secrets are absent.
|
||||
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: codersdk.WorkspaceTransitionStop,
|
||||
Secrets: []codersdk.WorkspaceSecretInput{
|
||||
{Name: "cert"},
|
||||
{Name: "token", Value: new("secret-value"), EnvName: "TOKEN"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
||||
require.ElementsMatch(t, []audit.WorkspaceBuildSecret{
|
||||
{Name: "api-key", EnvName: "API_KEY", Source: database.WorkspaceSecretSourceCarryForward},
|
||||
{Name: "token", EnvName: "TOKEN", Source: database.WorkspaceSecretSourceRequest},
|
||||
}, buildSecrets(t, build.ID))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user