fix!: enforce regex for agent names (#16641)

Underscores and double hyphens are now blocked. The regex is almost the
exact same as the `coder_app` `slug` regex, but uppercase characters are
still permitted.
This commit is contained in:
Dean Sheather
2025-02-20 05:09:26 +00:00
committed by GitHub
parent 92870f0642
commit 9469b78290
17 changed files with 368 additions and 104 deletions
+2 -1
View File
@@ -3918,7 +3918,8 @@ func (s *MethodTestSuite) TestSystemFunctions() {
s.Run("InsertWorkspaceAgent", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
check.Args(database.InsertWorkspaceAgentParams{
ID: uuid.New(),
ID: uuid.New(),
Name: "dev",
}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
}))
s.Run("InsertWorkspaceApp", s.Subtest(func(db database.Store, check *expects) {
+2 -1
View File
@@ -91,7 +91,8 @@ func (b WorkspaceBuildBuilder) WithAgent(mutations ...func([]*sdkproto.Agent) []
//nolint: revive // returns modified struct
b.agentToken = uuid.NewString()
agents := []*sdkproto.Agent{{
Id: uuid.NewString(),
Id: uuid.NewString(),
Name: "dev",
Auth: &sdkproto.Agent_Token{
Token: b.agentToken,
},
@@ -1891,6 +1891,19 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
appSlugs = make(map[string]struct{})
)
for _, prAgent := range protoResource.Agents {
// Similar logic is duplicated in terraform/resources.go.
if prAgent.Name == "" {
return xerrors.Errorf("agent name cannot be empty")
}
// In 2025-02 we removed support for underscores in agent names. To
// provide a nicer error message, we check the regex first and check
// for underscores if it fails.
if !provisioner.AgentNameRegex.MatchString(prAgent.Name) {
if strings.Contains(prAgent.Name, "_") {
return xerrors.Errorf("agent name %q contains underscores which are no longer supported, please use hyphens instead (regex: %q)", prAgent.Name, provisioner.AgentNameRegex.String())
}
return xerrors.Errorf("agent name %q does not match regex %q", prAgent.Name, provisioner.AgentNameRegex.String())
}
// Agent names must be case-insensitive-unique, to be unambiguous in
// `coder_app`s and CoderVPN DNS names.
if _, ok := agentNames[strings.ToLower(prAgent.Name)]; ok {
@@ -2070,10 +2083,13 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.
}
for _, app := range prAgent.Apps {
// Similar logic is duplicated in terraform/resources.go.
slug := app.Slug
if slug == "" {
return xerrors.Errorf("app must have a slug or name set")
}
// Contrary to agent names above, app slugs were never permitted to
// contain uppercase letters or underscores.
if !provisioner.AppSlugRegex.MatchString(slug) {
return xerrors.Errorf("app slug %q does not match regex %q", slug, provisioner.AppSlugRegex.String())
}
@@ -1883,6 +1883,7 @@ func TestInsertWorkspaceResource(t *testing.T) {
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Auth: &sdkproto.Agent_Token{
Token: "bananas",
},
@@ -1896,6 +1897,7 @@ func TestInsertWorkspaceResource(t *testing.T) {
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Apps: []*sdkproto.App{{
Slug: "a",
}, {
@@ -1903,7 +1905,61 @@ func TestInsertWorkspaceResource(t *testing.T) {
}},
}},
})
require.ErrorContains(t, err, "duplicate app slug")
require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`)
err = insert(dbmem.New(), uuid.New(), &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev1",
Apps: []*sdkproto.App{{
Slug: "a",
}},
}, {
Name: "dev2",
Apps: []*sdkproto.App{{
Slug: "a",
}},
}},
})
require.ErrorContains(t, err, `duplicate app slug, must be unique per template: "a"`)
})
t.Run("AppSlugInvalid", func(t *testing.T) {
t.Parallel()
db := dbmem.New()
job := uuid.New()
err := insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Apps: []*sdkproto.App{{
Slug: "dev_1",
}},
}},
})
require.ErrorContains(t, err, `app slug "dev_1" does not match regex`)
err = insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Apps: []*sdkproto.App{{
Slug: "dev--1",
}},
}},
})
require.ErrorContains(t, err, `app slug "dev--1" does not match regex`)
err = insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
Apps: []*sdkproto.App{{
Slug: "Dev",
}},
}},
})
require.ErrorContains(t, err, `app slug "Dev" does not match regex`)
})
t.Run("DuplicateAgentNames", func(t *testing.T) {
t.Parallel()
@@ -1931,6 +1987,35 @@ func TestInsertWorkspaceResource(t *testing.T) {
})
require.ErrorContains(t, err, "duplicate agent name")
})
t.Run("AgentNameInvalid", func(t *testing.T) {
t.Parallel()
db := dbmem.New()
job := uuid.New()
err := insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "Dev",
}},
})
require.NoError(t, err) // uppercase is still allowed
err = insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev_1",
}},
})
require.ErrorContains(t, err, `agent name "dev_1" contains underscores`) // custom error for underscores
err = insert(db, job, &sdkproto.Resource{
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev--1",
}},
})
require.ErrorContains(t, err, `agent name "dev--1" does not match regex`)
})
t.Run("Success", func(t *testing.T) {
t.Parallel()
db := dbmem.New()
@@ -2007,6 +2092,7 @@ func TestInsertWorkspaceResource(t *testing.T) {
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
DisplayApps: &sdkproto.DisplayApps{
Vscode: true,
VscodeInsiders: true,
@@ -2035,6 +2121,7 @@ func TestInsertWorkspaceResource(t *testing.T) {
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
DisplayApps: &sdkproto.DisplayApps{},
}},
})
@@ -2059,6 +2146,7 @@ func TestInsertWorkspaceResource(t *testing.T) {
Name: "something",
Type: "aws_instance",
Agents: []*sdkproto.Agent{{
Name: "dev",
DisplayApps: &sdkproto.DisplayApps{},
ResourcesMonitoring: &sdkproto.ResourcesMonitoring{
Memory: &sdkproto.MemoryResourceMonitor{
+3 -1
View File
@@ -829,6 +829,7 @@ func TestTemplateVersionResources(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{},
}},
}, {
@@ -875,7 +876,8 @@ func TestTemplateVersionLogs(t *testing.T) {
Name: "some",
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{
Token: uuid.NewString(),
},
+2 -1
View File
@@ -393,7 +393,8 @@ func TestWorkspaceAgentConnectRPC(t *testing.T) {
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Id: uuid.NewString(),
Name: "dev",
Auth: &proto.Agent_Token{
Token: uuid.NewString(),
},
+1
View File
@@ -720,6 +720,7 @@ func TestWorkspaceBuildLogs(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{},
}},
}, {
+3
View File
@@ -33,6 +33,7 @@ func TestPostWorkspaceAuthAzureInstanceIdentity(t *testing.T) {
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Name: "dev",
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
@@ -78,6 +79,7 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) {
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Name: "dev",
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
@@ -164,6 +166,7 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) {
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Name: "dev",
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
+9 -2
View File
@@ -219,6 +219,7 @@ func TestWorkspace(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "dev",
Auth: &proto.Agent_Token{},
}},
}},
@@ -259,6 +260,7 @@ func TestWorkspace(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "dev",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 1,
}},
@@ -1722,7 +1724,8 @@ func TestWorkspaceFilterManual(t *testing.T) {
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Id: uuid.NewString(),
Name: "dev",
Auth: &proto.Agent_Token{
Token: authToken,
},
@@ -2729,7 +2732,8 @@ func TestWorkspaceWatcher(t *testing.T) {
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Id: uuid.NewString(),
Name: "dev",
Auth: &proto.Agent_Token{
Token: authToken,
},
@@ -2951,6 +2955,7 @@ func TestWorkspaceResource(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{},
Apps: apps,
}},
@@ -3025,6 +3030,7 @@ func TestWorkspaceResource(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{},
Apps: apps,
}},
@@ -3068,6 +3074,7 @@ func TestWorkspaceResource(t *testing.T) {
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Name: "dev",
Auth: &proto.Agent_Token{},
}},
Metadata: []*proto.Resource_Metadata{{