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

Skip to content

Commit e079c38

Browse files
authored
Merge branch 'main' into david-fraley/fix-typo
2 parents 87c2c53 + 3074547 commit e079c38

File tree

14 files changed

+297
-79
lines changed

14 files changed

+297
-79
lines changed

coderd/aitasks.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,29 @@ func (api *API) tasksCreate(rw http.ResponseWriter, r *http.Request) {
113113
return
114114
}
115115

116-
taskName := taskname.GenerateFallback()
117-
if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" {
118-
anthropicModel := taskname.GetAnthropicModelFromEnv()
116+
taskName := req.Name
117+
if taskName != "" {
118+
if err := codersdk.NameValid(taskName); err != nil {
119+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
120+
Message: "Unable to create a Task with the provided name.",
121+
Detail: err.Error(),
122+
})
123+
return
124+
}
125+
}
119126

120-
generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel))
121-
if err != nil {
122-
api.Logger.Error(ctx, "unable to generate task name", slog.Error(err))
123-
} else {
124-
taskName = generatedName
127+
if taskName == "" {
128+
taskName = taskname.GenerateFallback()
129+
130+
if anthropicAPIKey := taskname.GetAnthropicAPIKeyFromEnv(); anthropicAPIKey != "" {
131+
anthropicModel := taskname.GetAnthropicModelFromEnv()
132+
133+
generatedName, err := taskname.Generate(ctx, req.Prompt, taskname.WithAPIKey(anthropicAPIKey), taskname.WithModel(anthropicModel))
134+
if err != nil {
135+
api.Logger.Error(ctx, "unable to generate task name", slog.Error(err))
136+
} else {
137+
taskName = generatedName
138+
}
125139
}
126140
}
127141

coderd/aitasks_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,80 @@ func TestTasksCreate(t *testing.T) {
441441
assert.Equal(t, taskPrompt, parameters[0].Value)
442442
})
443443

444+
t.Run("CustomNames", func(t *testing.T) {
445+
t.Parallel()
446+
447+
tests := []struct {
448+
name string
449+
taskName string
450+
expectFallbackName bool
451+
expectError string
452+
}{
453+
{
454+
name: "ValidName",
455+
taskName: "a-valid-task-name",
456+
},
457+
{
458+
name: "NotValidName",
459+
taskName: "this is not a valid task name",
460+
expectError: "Unable to create a Task with the provided name.",
461+
},
462+
{
463+
name: "NoNameProvided",
464+
taskName: "",
465+
expectFallbackName: true,
466+
},
467+
}
468+
469+
for _, tt := range tests {
470+
t.Run(tt.name, func(t *testing.T) {
471+
t.Parallel()
472+
473+
var (
474+
ctx = testutil.Context(t, testutil.WaitShort)
475+
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
476+
expClient = codersdk.NewExperimentalClient(client)
477+
user = coderdtest.CreateFirstUser(t, client)
478+
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
479+
Parse: echo.ParseComplete,
480+
ProvisionApply: echo.ApplyComplete,
481+
ProvisionPlan: []*proto.Response{
482+
{Type: &proto.Response_Plan{Plan: &proto.PlanComplete{
483+
Parameters: []*proto.RichParameter{{Name: "AI Prompt", Type: "string"}},
484+
HasAiTasks: true,
485+
}}},
486+
},
487+
})
488+
template = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
489+
)
490+
491+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
492+
493+
// When: We attempt to create a Task.
494+
task, err := expClient.CreateTask(ctx, "me", codersdk.CreateTaskRequest{
495+
TemplateVersionID: template.ActiveVersionID,
496+
Prompt: "Some prompt",
497+
Name: tt.taskName,
498+
})
499+
if tt.expectError == "" {
500+
require.NoError(t, err)
501+
require.True(t, task.WorkspaceID.Valid)
502+
503+
// Then: We expect the correct name to have been picked.
504+
err = codersdk.NameValid(task.Name)
505+
require.NoError(t, err, "Generated task name should be valid")
506+
507+
require.NotEmpty(t, task.Name)
508+
if !tt.expectFallbackName {
509+
require.Equal(t, tt.taskName, task.Name)
510+
}
511+
} else {
512+
require.ErrorContains(t, err, tt.expectError)
513+
}
514+
})
515+
}
516+
})
517+
444518
t.Run("FailsOnNonTaskTemplate", func(t *testing.T) {
445519
t.Parallel()
446520

coderd/coderdtest/coderdtest.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,9 +1097,17 @@ func AwaitWorkspaceBuildJobCompleted(t testing.TB, client *codersdk.Client, buil
10971097
require.Eventually(t, func() bool {
10981098
var err error
10991099
workspaceBuild, err = client.WorkspaceBuild(ctx, build)
1100-
return assert.NoError(t, err) && workspaceBuild.Job.CompletedAt != nil
1100+
if err != nil {
1101+
t.Logf("failed to get workspace build %s: %v", build, err)
1102+
return false
1103+
}
1104+
if workspaceBuild.Job.CompletedAt == nil {
1105+
t.Logf("workspace build job %s still running (status: %s)", build, workspaceBuild.Job.Status)
1106+
return false
1107+
}
1108+
return true
11011109
}, testutil.WaitMedium, testutil.IntervalMedium)
1102-
t.Logf("got workspace build job %s", build)
1110+
t.Logf("got workspace build job %s (status: %s)", build, workspaceBuild.Job.Status)
11031111
return workspaceBuild
11041112
}
11051113

coderd/database/modelqueries.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ func (q *sqlQuerier) GetAuthorizedTemplates(ctx context.Context, arg GetTemplate
7878
arg.Deleted,
7979
arg.OrganizationID,
8080
arg.ExactName,
81+
arg.ExactDisplayName,
8182
arg.FuzzyName,
83+
arg.FuzzyDisplayName,
8284
pq.Array(arg.IDs),
8385
arg.Deprecated,
8486
arg.HasAITask,

coderd/database/queries.sql.go

Lines changed: 36 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/templates.sql

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,30 @@ WHERE
3030
LOWER(t.name) = LOWER(@exact_name)
3131
ELSE true
3232
END
33+
-- Filter by exact display name
34+
AND CASE
35+
WHEN @exact_display_name :: text != '' THEN
36+
LOWER(t.display_name) = LOWER(@exact_display_name)
37+
ELSE true
38+
END
3339
-- Filter by name, matching on substring
3440
AND CASE
3541
WHEN @fuzzy_name :: text != '' THEN
3642
lower(t.name) ILIKE '%' || lower(@fuzzy_name) || '%'
3743
ELSE true
3844
END
45+
-- Filter by display_name, matching on substring (fallback to name if display_name is empty)
46+
AND CASE
47+
WHEN @fuzzy_display_name :: text != '' THEN
48+
CASE
49+
WHEN t.display_name IS NOT NULL AND t.display_name != '' THEN
50+
lower(t.display_name) ILIKE '%' || lower(@fuzzy_display_name) || '%'
51+
ELSE
52+
-- Remove spaces if present since 't.name' cannot have any spaces
53+
lower(t.name) ILIKE '%' || REPLACE(lower(@fuzzy_display_name), ' ', '') || '%'
54+
END
55+
ELSE true
56+
END
3957
-- Filter by ids
4058
AND CASE
4159
WHEN array_length(@ids :: uuid[], 1) > 0 THEN

coderd/rbac/authz_internal_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,57 @@ func TestAuthorizeScope(t *testing.T) {
11101110
{resource: ResourceOrganization.WithID(defOrg)},
11111111
}),
11121112
)
1113+
1114+
// Test setting a scope on the org and the user level
1115+
// This is a bit of a contrived example that would not exist in practice.
1116+
// It combines a specific organization scope with a user scope to verify
1117+
// that both are applied.
1118+
// The test uses the `Owner` role, so by default the user can do everything.
1119+
user = Subject{
1120+
ID: "me",
1121+
Roles: Roles{
1122+
must(RoleByName(RoleOwner())),
1123+
// TODO: There is a __bug__ in the policy.rego. If the user is not a
1124+
// member of the organization, the org_scope fails. This happens because
1125+
// the org_allow_set uses "org_members".
1126+
// This is odd behavior, as without this membership role, the test for
1127+
// the workspace fails. Maybe scopes should just assume the user
1128+
// is a member.
1129+
must(RoleByName(ScopedRoleOrgMember(defOrg))),
1130+
},
1131+
Scope: Scope{
1132+
Role: Role{
1133+
Identifier: RoleIdentifier{
1134+
Name: "org-and-user-scope",
1135+
OrganizationID: defOrg,
1136+
},
1137+
DisplayName: "OrgAndUserScope",
1138+
Site: nil,
1139+
Org: map[string][]Permission{
1140+
defOrg.String(): Permissions(map[string][]policy.Action{
1141+
ResourceWorkspace.Type: {policy.ActionRead},
1142+
}),
1143+
},
1144+
User: Permissions(map[string][]policy.Action{
1145+
ResourceUser.Type: {policy.ActionRead},
1146+
}),
1147+
},
1148+
AllowIDList: []string{policy.WildcardSymbol},
1149+
},
1150+
}
1151+
1152+
testAuthorize(t, "OrgAndUserScope", user,
1153+
// Allowed by scope:
1154+
[]authTestCase{
1155+
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: true, actions: []policy.Action{policy.ActionRead}},
1156+
{resource: ResourceUser.WithOwner(user.ID), allow: true, actions: []policy.Action{policy.ActionRead}},
1157+
},
1158+
// Not allowed by scope:
1159+
[]authTestCase{
1160+
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: false, actions: []policy.Action{policy.ActionCreate}},
1161+
{resource: ResourceUser.WithOwner(user.ID), allow: false, actions: []policy.Action{policy.ActionUpdate}},
1162+
},
1163+
)
11131164
}
11141165

11151166
// cases applies a given function to all test cases. This makes generalities easier to create.

coderd/rbac/policy.rego

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ site_allow(roles) := num if {
106106
# -------------------
107107

108108
# org_members is the list of organizations the actor is apart of.
109+
# TODO: Should there be an org_members for the scope too? Without it,
110+
# the membership is determined by the user's roles, not their scope permissions.
111+
# So if an owner (who is not an org member) has an org scope, that org scope
112+
# will fail to return '1'. Since we assume all non members return '-1' for org
113+
# level permissions.
114+
# Adding a second org_members set might affect the partial evaluation.
115+
# This is being left until org scopes are used.
109116
org_members := {orgID |
110117
input.subject.roles[_].org[orgID]
111118
}
@@ -116,7 +123,7 @@ default org := 0
116123
org := org_allow(input.subject.roles)
117124

118125
default scope_org := 0
119-
scope_org := org_allow([input.scope])
126+
scope_org := org_allow([input.subject.scope])
120127

121128
# org_allow_set is a helper function that iterates over all orgs that the actor
122129
# is a member of. For each organization it sets the numerical allow value
@@ -221,7 +228,7 @@ default user := 0
221228
user := user_allow(input.subject.roles)
222229

223230
default scope_user := 0
224-
scope_user := user_allow([input.scope])
231+
scope_user := user_allow([input.subject.scope])
225232

226233
user_allow(roles) := num if {
227234
input.object.owner != ""

0 commit comments

Comments
 (0)