mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
85984ff142
Users can now disable a secret to stop it from being injected into workspaces without deleting it, and re-enable it later. Disabled secrets stay visible and editable everywhere they already appear. An enabled secret must have at least one injection target; a secret with no target can be stored only while disabled. Existing target-less secrets are migrated to disabled to preserve current behavior. Support spans the REST API, SDK, CLI, dashboard, and audit log.
194 lines
5.8 KiB
Go
194 lines
5.8 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
//nolint:paralleltest,tparallel // Subtests share one coderdtest.New server and run sequentially.
|
|
func TestUserSecretAudit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
auditor := audit.NewMock()
|
|
client := coderdtest.New(t, &coderdtest.Options{Auditor: auditor})
|
|
_ = coderdtest.CreateFirstUser(t, client)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
genSecretName := func(t *testing.T) string {
|
|
// Use test name derived secret names so subtests cannot
|
|
// collide in the shared user's secret namespace.
|
|
return strings.ReplaceAll(t.Name(), "/", "-")
|
|
}
|
|
genEnvName := func(t *testing.T) string {
|
|
// Same derivation as genSecretName, but in the
|
|
// SCREAMING_SNAKE_CASE shape env names require. Every
|
|
// secret needs at least one of env_name or file_path,
|
|
// and the per-user UNIQUE index makes empty-injection
|
|
// not an option.
|
|
name := strings.ReplaceAll(t.Name(), "/", "_")
|
|
name = strings.ReplaceAll(name, "-", "_")
|
|
return strings.ToUpper(name)
|
|
}
|
|
|
|
t.Run("CreateEmitsLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
secret, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: genSecretName(t),
|
|
Value: "ghp_xxxxxxxxxxxx",
|
|
EnvName: genEnvName(t),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
logs := auditor.AuditLogs()
|
|
require.Len(t, logs, 1)
|
|
assert.Equal(t, database.AuditActionCreate, logs[0].Action)
|
|
assert.Equal(t, secret.ID, logs[0].ResourceID)
|
|
assert.Equal(t, secret.Name, logs[0].ResourceTarget)
|
|
assert.EqualValues(t, http.StatusCreated, logs[0].StatusCode)
|
|
})
|
|
|
|
t.Run("UpdateEmitsLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
secret, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: genSecretName(t),
|
|
Value: "old",
|
|
EnvName: genEnvName(t),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
newDescription := "rotated"
|
|
newValue := "new-value"
|
|
_, err = client.UpdateUserSecret(ctx, codersdk.Me, secret.Name, codersdk.UpdateUserSecretRequest{
|
|
Description: &newDescription,
|
|
Value: &newValue,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
logs := auditor.AuditLogs()
|
|
require.Len(t, logs, 2)
|
|
assert.Equal(t, database.AuditActionCreate, logs[0].Action)
|
|
assert.Equal(t, database.AuditActionWrite, logs[1].Action)
|
|
assert.Equal(t, secret.ID, logs[1].ResourceID)
|
|
assert.Equal(t, secret.Name, logs[1].ResourceTarget)
|
|
assert.EqualValues(t, http.StatusOK, logs[1].StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteEmitsLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
secret, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: genSecretName(t),
|
|
Value: "value",
|
|
EnvName: genEnvName(t),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, client.DeleteUserSecret(ctx, codersdk.Me, secret.Name))
|
|
|
|
logs := auditor.AuditLogs()
|
|
require.Len(t, logs, 2)
|
|
assert.Equal(t, database.AuditActionCreate, logs[0].Action)
|
|
assert.Equal(t, database.AuditActionDelete, logs[1].Action)
|
|
assert.Equal(t, secret.ID, logs[1].ResourceID)
|
|
assert.Equal(t, secret.Name, logs[1].ResourceTarget)
|
|
assert.EqualValues(t, http.StatusNoContent, logs[1].StatusCode)
|
|
})
|
|
|
|
t.Run("DeleteOfMissingWritesNoLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
err := client.DeleteUserSecret(ctx, codersdk.Me, "does-not-exist")
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
assert.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
|
|
require.Empty(t, auditor.AuditLogs())
|
|
})
|
|
|
|
t.Run("UpdateOfMissingWritesNoLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
desc := "anything"
|
|
_, err := client.UpdateUserSecret(ctx, codersdk.Me, "does-not-exist", codersdk.UpdateUserSecretRequest{
|
|
Description: &desc,
|
|
})
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
assert.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
|
|
|
require.Empty(t, auditor.AuditLogs())
|
|
})
|
|
|
|
t.Run("ValidationFailureWritesNoLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
|
|
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: genSecretName(t),
|
|
Value: "value",
|
|
EnvName: "1invalid",
|
|
})
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
|
|
require.Empty(t, auditor.AuditLogs())
|
|
})
|
|
|
|
t.Run("EmptyUpdateWritesNoLog", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
name := genSecretName(t)
|
|
|
|
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: name,
|
|
Value: "value",
|
|
EnvName: genEnvName(t),
|
|
})
|
|
require.NoError(t, err)
|
|
// Reset to ignore the created log. We are only testing that the
|
|
// no-op update does not add a new log.
|
|
auditor.ResetLogs()
|
|
|
|
_, err = client.UpdateUserSecret(ctx, codersdk.Me, name, codersdk.UpdateUserSecretRequest{})
|
|
var sdkErr *codersdk.Error
|
|
require.ErrorAs(t, err, &sdkErr)
|
|
assert.Equal(t, http.StatusBadRequest, sdkErr.StatusCode())
|
|
|
|
require.Empty(t, auditor.AuditLogs())
|
|
})
|
|
|
|
t.Run("ReadsDoNotAudit", func(t *testing.T) {
|
|
auditor.ResetLogs()
|
|
secretName := genSecretName(t)
|
|
|
|
_, err := client.CreateUserSecret(ctx, codersdk.Me, codersdk.CreateUserSecretRequest{
|
|
Name: secretName,
|
|
Value: "value",
|
|
EnvName: genEnvName(t),
|
|
})
|
|
require.NoError(t, err)
|
|
// Discard the create log so the assertion below only sees audit entries
|
|
// produced by later reads.
|
|
auditor.ResetLogs()
|
|
|
|
_, err = client.UserSecrets(ctx, codersdk.Me)
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.UserSecretByName(ctx, codersdk.Me, secretName)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, auditor.AuditLogs())
|
|
})
|
|
}
|