Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cli/exp_taskcreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (r *RootCmd) taskCreate() *serpent.Command {
taskInput string
)

return &serpent.Command{
cmd := &serpent.Command{
Use: "create [template]",
Short: "Create an experimental task",
Middleware: serpent.Chain(
Expand Down Expand Up @@ -123,4 +123,6 @@ func (r *RootCmd) taskCreate() *serpent.Command {
return nil
},
}
orgContext.AttachOptions(cmd)
return cmd
}
73 changes: 54 additions & 19 deletions cli/exp_taskcreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,28 @@ func TestTaskCreate(t *testing.T) {
taskCreatedAt = time.Now()

organizationID = uuid.New()
anotherOrganizationID = uuid.New()
templateID = uuid.New()
templateVersionID = uuid.New()
templateVersionPresetID = uuid.New()
)

templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc {
templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, orgID uuid.UUID, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc {
t.Helper()

return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
{MinimalOrganization: codersdk.MinimalOrganization{
ID: organizationID,
ID: orgID,
}},
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", organizationID):
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", orgID):
httpapi.Write(ctx, w, http.StatusOK, codersdk.TemplateVersion{
ID: templateVersionID,
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID):
case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", orgID):
httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{
ID: templateID,
ActiveVersionID: templateVersionID,
Expand Down Expand Up @@ -94,62 +95,62 @@ func TestTaskCreate(t *testing.T) {
handler func(t *testing.T, ctx context.Context) http.HandlerFunc
}{
{
args: []string{"my-template@my-template-version", "--input", "my custom prompt"},
args: []string{"my-template@my-template-version", "--input", "my custom prompt", "--org", organizationID.String()},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"--input", "my custom prompt"},
args: []string{"--input", "my custom prompt", "--org", organizationID.String()},
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt"},
env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt", "CODER_ORGANIZATION=" + organizationID.String()},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset"},
args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset", "--org", organizationID.String()},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt"},
env: []string{"CODER_TASK_PRESET_NAME=my-preset"},
expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)),
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
},
},
{
args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"},
expectError: `preset "not-real-preset" not found`,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt")
return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt")
},
},
{
Expand All @@ -173,7 +174,7 @@ func TestTaskCreate(t *testing.T) {
},
},
{
args: []string{"not-real-template", "--input", "my custom prompt"},
args: []string{"not-real-template", "--input", "my custom prompt", "--org", organizationID.String()},
expectError: httpapi.ResourceNotFoundResponse.Message,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -192,6 +193,40 @@ func TestTaskCreate(t *testing.T) {
}
},
},
{
args: []string{"template-in-different-org", "--input", "my-custom-prompt", "--org", anotherOrganizationID.String()},
expectError: httpapi.ResourceNotFoundResponse.Message,
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{
{MinimalOrganization: codersdk.MinimalOrganization{
ID: anotherOrganizationID,
}},
})
case fmt.Sprintf("/api/v2/organizations/%s/templates/template-in-different-org", anotherOrganizationID):
httpapi.ResourceNotFound(w)
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
{
args: []string{"no-org", "--input", "my-custom-prompt"},
expectError: "Must select an organization with --org=<org_name>",
handler: func(t *testing.T, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/users/me/organizations":
httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{})
default:
t.Errorf("unexpected path: %s", r.URL.Path)
}
}
},
},
}

for _, tt := range tests {
Expand Down
61 changes: 61 additions & 0 deletions coderd/httpmw/loggermw/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"

Expand All @@ -15,6 +18,59 @@ import (
"github.com/coder/coder/v2/coderd/tracing"
)

var (
safeParams = []string{"page", "limit", "offset"}
countParams = []string{"ids", "template_ids"}
)

func safeQueryParams(params url.Values) []slog.Field {
if len(params) == 0 {
return nil
}

fields := make([]slog.Field, 0, len(params))
for key, values := range params {
// Check if this parameter should be included
for _, pattern := range safeParams {
if strings.EqualFold(key, pattern) {
// Prepend query parameters in the log line to ensure we don't have issues with collisions
// in case any other internal logging fields already log fields with similar names
fieldName := "query_" + key

// Log the actual values for non-sensitive parameters
if len(values) == 1 {
fields = append(fields, slog.F(fieldName, values[0]))
continue
}
fields = append(fields, slog.F(fieldName, values))
}
}
// Some query params we just want to log the count of the params length
for _, pattern := range countParams {
if !strings.EqualFold(key, pattern) {
continue
}
count := 0

// Prepend query parameters in the log line to ensure we don't have issues with collisions
// in case any other internal logging fields already log fields with similar names
fieldName := "query_" + key

// Count comma-separated values for CSV format
for _, v := range values {
if strings.Contains(v, ",") {
count += len(strings.Split(v, ","))
continue
}
count++
}
// For logging we always want strings
fields = append(fields, slog.F(fieldName+"_count", strconv.Itoa(count)))
}
}
return fields
}

func Logger(log slog.Logger) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
Expand All @@ -39,6 +95,11 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler {
slog.F("start", start),
)

// Add safe query parameters to the log
if queryFields := safeQueryParams(r.URL.Query()); len(queryFields) > 0 {
httplog = httplog.With(queryFields...)
}

logContext := NewRequestLogger(httplog, r.Method, start)

ctx := WithRequestLogger(r.Context(), logContext)
Expand Down
71 changes: 71 additions & 0 deletions coderd/httpmw/loggermw/logger_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strings"
"sync"
Expand Down Expand Up @@ -292,6 +293,76 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) {
}
}

func TestSafeQueryParams(t *testing.T) {
t.Parallel()

tests := []struct {
name string
params url.Values
expected map[string]interface{}
}{
{
name: "safe parameters",
params: url.Values{
"page": []string{"1"},
"limit": []string{"10"},
"filter": []string{"active"},
"sort": []string{"name"},
"offset": []string{"2"},
"ids": []string{"some-id,another-id", "second-param"},
"template_ids": []string{"some-id,another-id", "second-param"},
},
expected: map[string]interface{}{
"query_page": "1",
"query_limit": "10",
"query_offset": "2",
"query_ids_count": "3",
"query_template_ids_count": "3",
},
},
{
name: "unknown/sensitive parameters",
params: url.Values{
"token": []string{"secret-token"},
"api_key": []string{"secret-key"},
"coder_signed_app_token": []string{"jwt-token"},
"coder_application_connect_api_key": []string{"encrypted-key"},
"client_secret": []string{"oauth-secret"},
"code": []string{"auth-code"},
},
expected: map[string]interface{}{},
},
{
name: "mixed parameters",
params: url.Values{
"page": []string{"1"},
"token": []string{"secret"},
"filter": []string{"active"},
},
expected: map[string]interface{}{
"query_page": "1",
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

fields := safeQueryParams(tt.params)

// Convert fields to map for easier comparison
result := make(map[string]interface{})
for _, field := range fields {
result[field.Name] = field.Value
}

require.Equal(t, tt.expected, result)
})
}
}

type fakeSink struct {
entries []slog.SinkEntry
newEntries chan slog.SinkEntry
Expand Down
5 changes: 3 additions & 2 deletions site/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { cn } from "utils/cn";

interface SidebarProps {
children?: ReactNode;
className?: string;
}

export const Sidebar: FC<SidebarProps> = ({ children }) => {
return <nav className="w-60 flex-shrink-0">{children}</nav>;
export const Sidebar: FC<SidebarProps> = ({ className, children }) => {
return <nav className={cn("w-60 flex-shrink-0", className)}>{children}</nav>;
};

interface SidebarHeaderProps {
Expand Down
4 changes: 2 additions & 2 deletions site/src/modules/dashboard/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export const DashboardLayout: FC = () => {
{canViewDeployment && <LicenseBanner />}
<AnnouncementBanners />

<div className="flex flex-col min-h-screen">
<div className="flex flex-col h-screen justify-between">
<Navbar />

<div className="flex flex-col flex-1 min-h-0 pb-12">
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
Expand Down
6 changes: 3 additions & 3 deletions site/src/modules/management/OrganizationSettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const OrganizationSettingsLayout: FC = () => {
organizationPermissions,
}}
>
<div>
<div className="flex flex-col flex-1 min-h-0">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
Expand Down Expand Up @@ -121,8 +121,8 @@ const OrganizationSettingsLayout: FC = () => {
)}
</BreadcrumbList>
</Breadcrumb>
<hr className="h-px border-none bg-border" />
<div className="px-10 max-w-screen-2xl">
<div className="h-px border-none bg-border" />
<div className="flex flex-col flex-1 min-h-0 pl-10">
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
Expand Down
2 changes: 1 addition & 1 deletion site/src/modules/management/OrganizationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const OrganizationSidebar: FC = () => {
useOrganizationSettings();

return (
<BaseSidebar>
<BaseSidebar className="pt-10">
<OrganizationSidebarView
activeOrganization={organization}
orgPermissions={organizationPermissions}
Expand Down
Loading
Loading