Skip to content

Commit 554e837

Browse files
committed
Disable quiet hours endpoint if not entitled
1 parent 2fb3053 commit 554e837

File tree

3 files changed

+221
-98
lines changed

3 files changed

+221
-98
lines changed

enterprise/coderd/coderd.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
237237
})
238238
r.Route("/users/{user}/quiet-hours", func(r chi.Router) {
239239
r.Use(
240-
// TODO: enabled MW?
240+
api.restartRequirementEnabledMW,
241241
apiKeyMiddleware,
242242
httpmw.ExtractUserParam(options.Database, false),
243243
)
@@ -494,6 +494,10 @@ func (api *API) updateEntitlements(ctx context.Context) error {
494494
api.AGPL.UserQuietHoursScheduleStore.Store(&quietHoursStore)
495495
}
496496
} else {
497+
if api.DefaultQuietHoursSchedule != "" {
498+
api.Logger.Warn(ctx, "template restart requirements are not enabled (due to setting default quiet hours schedule) as your license is not entitled to this feature")
499+
}
500+
497501
templateStore := *(api.AGPL.TemplateScheduleStore.Load())
498502
enterpriseTemplateStore, ok := templateStore.(*schedule.EnterpriseTemplateScheduleStore)
499503
if ok {

enterprise/coderd/users.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,36 @@ import (
1111
"github.com/coder/coder/codersdk"
1212
)
1313

14+
func (api *API) restartRequirementEnabledMW(next http.Handler) http.Handler {
15+
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
16+
// The experiment must be enabled.
17+
if !api.AGPL.Experiments.Enabled(codersdk.ExperimentTemplateRestartRequirement) {
18+
httpapi.RouteNotFound(rw)
19+
return
20+
}
21+
22+
// Entitlement must be enabled.
23+
api.entitlementsMu.RLock()
24+
entitled := api.entitlements.Features[codersdk.FeatureTemplateRestartRequirement].Entitlement != codersdk.EntitlementNotEntitled
25+
enabled := api.entitlements.Features[codersdk.FeatureTemplateRestartRequirement].Enabled
26+
api.entitlementsMu.RUnlock()
27+
if !entitled {
28+
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
29+
Message: "Template restart requirement is an Enterprise feature. Contact sales!",
30+
})
31+
return
32+
}
33+
if !enabled {
34+
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
35+
Message: "Template restart requirement feature is not enabled. Please specify a default user quiet hours schedule to use this feature.",
36+
})
37+
return
38+
}
39+
40+
next.ServeHTTP(rw, r)
41+
})
42+
}
43+
1444
// @Summary Get user quiet hours schedule
1545
// @ID get-user-quiet-hours-schedule
1646
// @Security CoderSessionToken

enterprise/coderd/users_test.go

Lines changed: 186 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package coderd_test
22

33
import (
4+
"net/http"
45
"testing"
56
"time"
67

@@ -17,112 +18,200 @@ import (
1718
func TestUserQuietHours(t *testing.T) {
1819
t.Parallel()
1920

20-
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 0 * * *"
21-
defaultScheduleParsed, err := schedule.Daily(defaultQuietHoursSchedule)
22-
require.NoError(t, err)
23-
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
24-
if time.Until(nextTime) < time.Hour {
25-
// Use a different default schedule instead, because we want to avoid
26-
// the schedule "ticking over" during this test run.
27-
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 12 * * *"
28-
defaultScheduleParsed, err = schedule.Daily(defaultQuietHoursSchedule)
21+
t.Run("OK", func(t *testing.T) {
22+
t.Parallel()
23+
24+
defaultQuietHoursSchedule := "CRON_TZ=America/Chicago 0 0 * * *"
25+
defaultScheduleParsed, err := schedule.Daily(defaultQuietHoursSchedule)
2926
require.NoError(t, err)
30-
}
31-
32-
dv := coderdtest.DeploymentValues(t)
33-
dv.UserQuietHoursSchedule.DefaultSchedule.Set(defaultQuietHoursSchedule)
34-
dv.UserQuietHoursSchedule.WindowDuration.Set("8h") // default is 4h
35-
36-
client, user := coderdenttest.New(t, &coderdenttest.Options{
37-
Options: &coderdtest.Options{
38-
DeploymentValues: dv,
39-
},
40-
LicenseOptions: &coderdenttest.LicenseOptions{
41-
Features: license.Features{
42-
codersdk.FeatureAdvancedTemplateScheduling: 1,
43-
codersdk.FeatureTemplateRestartRequirement: 1,
27+
nextTime := defaultScheduleParsed.Next(time.Now().In(defaultScheduleParsed.Location()))
28+
if time.Until(nextTime) < time.Hour {
29+
// Use a different default schedule instead, because we want to avoid
30+
// the schedule "ticking over" during this test run.
31+
defaultQuietHoursSchedule = "CRON_TZ=America/Chicago 0 12 * * *"
32+
defaultScheduleParsed, err = schedule.Daily(defaultQuietHoursSchedule)
33+
require.NoError(t, err)
34+
}
35+
36+
dv := coderdtest.DeploymentValues(t)
37+
dv.UserQuietHoursSchedule.DefaultSchedule.Set(defaultQuietHoursSchedule)
38+
dv.UserQuietHoursSchedule.WindowDuration.Set("8h") // default is 4h
39+
dv.Experiments.Set(string(codersdk.ExperimentTemplateRestartRequirement))
40+
41+
client, user := coderdenttest.New(t, &coderdenttest.Options{
42+
Options: &coderdtest.Options{
43+
DeploymentValues: dv,
4444
},
45-
},
46-
})
45+
LicenseOptions: &coderdenttest.LicenseOptions{
46+
Features: license.Features{
47+
codersdk.FeatureAdvancedTemplateScheduling: 1,
48+
codersdk.FeatureTemplateRestartRequirement: 1,
49+
},
50+
},
51+
})
4752

48-
// Get quiet hours for a user that doesn't have them set.
49-
ctx := testutil.Context(t, testutil.WaitLong)
50-
sched1, err := client.UserQuietHoursSchedule(ctx, codersdk.Me)
51-
require.NoError(t, err)
52-
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
53-
require.False(t, sched1.UserSet)
54-
require.Equal(t, defaultScheduleParsed.Time(), sched1.Time)
55-
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
56-
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched1.Duration)
57-
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
58-
59-
// Set their quiet hours.
60-
customQuietHoursSchedule := "CRON_TZ=Australia/Sydney 0 0 * * *"
61-
customScheduleParsed, err := schedule.Daily(customQuietHoursSchedule)
62-
require.NoError(t, err)
63-
nextTime = customScheduleParsed.Next(time.Now().In(customScheduleParsed.Location()))
64-
if time.Until(nextTime) < time.Hour {
65-
// Use a different default schedule instead, because we want to avoid
66-
// the schedule "ticking over" during this test run.
67-
customQuietHoursSchedule = "CRON_TZ=Australia/Sydney 0 12 * * *"
68-
customScheduleParsed, err = schedule.Daily(customQuietHoursSchedule)
53+
// Get quiet hours for a user that doesn't have them set.
54+
ctx := testutil.Context(t, testutil.WaitLong)
55+
sched1, err := client.UserQuietHoursSchedule(ctx, codersdk.Me)
6956
require.NoError(t, err)
70-
}
71-
72-
sched2, err := client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
73-
Schedule: customQuietHoursSchedule,
74-
})
75-
require.NoError(t, err)
76-
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
77-
require.True(t, sched2.UserSet)
78-
require.Equal(t, customScheduleParsed.Time(), sched2.Time)
79-
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
80-
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched2.Duration)
81-
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
82-
83-
// Get quiet hours for a user that has them set.
84-
sched3, err := client.UserQuietHoursSchedule(ctx, user.UserID.String())
85-
require.NoError(t, err)
86-
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
87-
require.True(t, sched3.UserSet)
88-
require.Equal(t, customScheduleParsed.Time(), sched3.Time)
89-
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
90-
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched3.Duration)
91-
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)
92-
93-
// Try setting a garbage schedule.
94-
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
95-
Schedule: "garbage",
57+
require.Equal(t, defaultScheduleParsed.String(), sched1.RawSchedule)
58+
require.False(t, sched1.UserSet)
59+
require.Equal(t, defaultScheduleParsed.Time(), sched1.Time)
60+
require.Equal(t, defaultScheduleParsed.Location().String(), sched1.Timezone)
61+
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched1.Duration)
62+
require.WithinDuration(t, defaultScheduleParsed.Next(time.Now()), sched1.Next, 15*time.Second)
63+
64+
// Set their quiet hours.
65+
customQuietHoursSchedule := "CRON_TZ=Australia/Sydney 0 0 * * *"
66+
customScheduleParsed, err := schedule.Daily(customQuietHoursSchedule)
67+
require.NoError(t, err)
68+
nextTime = customScheduleParsed.Next(time.Now().In(customScheduleParsed.Location()))
69+
if time.Until(nextTime) < time.Hour {
70+
// Use a different default schedule instead, because we want to avoid
71+
// the schedule "ticking over" during this test run.
72+
customQuietHoursSchedule = "CRON_TZ=Australia/Sydney 0 12 * * *"
73+
customScheduleParsed, err = schedule.Daily(customQuietHoursSchedule)
74+
require.NoError(t, err)
75+
}
76+
77+
sched2, err := client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
78+
Schedule: customQuietHoursSchedule,
79+
})
80+
require.NoError(t, err)
81+
require.Equal(t, customScheduleParsed.String(), sched2.RawSchedule)
82+
require.True(t, sched2.UserSet)
83+
require.Equal(t, customScheduleParsed.Time(), sched2.Time)
84+
require.Equal(t, customScheduleParsed.Location().String(), sched2.Timezone)
85+
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched2.Duration)
86+
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched2.Next, 15*time.Second)
87+
88+
// Get quiet hours for a user that has them set.
89+
sched3, err := client.UserQuietHoursSchedule(ctx, user.UserID.String())
90+
require.NoError(t, err)
91+
require.Equal(t, customScheduleParsed.String(), sched3.RawSchedule)
92+
require.True(t, sched3.UserSet)
93+
require.Equal(t, customScheduleParsed.Time(), sched3.Time)
94+
require.Equal(t, customScheduleParsed.Location().String(), sched3.Timezone)
95+
require.Equal(t, dv.UserQuietHoursSchedule.WindowDuration.Value(), sched3.Duration)
96+
require.WithinDuration(t, customScheduleParsed.Next(time.Now()), sched3.Next, 15*time.Second)
97+
98+
// Try setting a garbage schedule.
99+
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
100+
Schedule: "garbage",
101+
})
102+
require.Error(t, err)
103+
require.ErrorContains(t, err, "parse daily schedule")
104+
105+
// Try setting a non-daily schedule.
106+
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
107+
Schedule: "CRON_TZ=America/Chicago 0 0 * * 1",
108+
})
109+
require.Error(t, err)
110+
require.ErrorContains(t, err, "parse daily schedule")
111+
112+
// Try setting a schedule with a timezone that doesn't exist.
113+
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
114+
Schedule: "CRON_TZ=Deans/House 0 0 * * *",
115+
})
116+
require.Error(t, err)
117+
require.ErrorContains(t, err, "parse daily schedule")
118+
119+
// Try setting a schedule with more than one time.
120+
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
121+
Schedule: "CRON_TZ=America/Chicago 0 0,12 * * *",
122+
})
123+
require.Error(t, err)
124+
require.ErrorContains(t, err, "more than one time")
125+
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
126+
Schedule: "CRON_TZ=America/Chicago 0-30 0 * * *",
127+
})
128+
require.Error(t, err)
129+
require.ErrorContains(t, err, "more than one time")
130+
131+
// We don't allow unsetting the custom schedule so we don't need to worry
132+
// about it in this test.
96133
})
97-
require.Error(t, err)
98-
require.ErrorContains(t, err, "parse daily schedule")
99134

100-
// Try setting a non-daily schedule.
101-
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
102-
Schedule: "CRON_TZ=America/Chicago 0 0 * * 1",
103-
})
104-
require.Error(t, err)
105-
require.ErrorContains(t, err, "parse daily schedule")
135+
t.Run("NotEntitled", func(t *testing.T) {
136+
t.Parallel()
106137

107-
// Try setting a schedule with a timezone that doesn't exist.
108-
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
109-
Schedule: "CRON_TZ=Deans/House 0 0 * * *",
110-
})
111-
require.Error(t, err)
112-
require.ErrorContains(t, err, "parse daily schedule")
138+
dv := coderdtest.DeploymentValues(t)
139+
dv.UserQuietHoursSchedule.DefaultSchedule.Set("CRON_TZ=America/Chicago 0 0 * * *")
140+
dv.Experiments.Set(string(codersdk.ExperimentTemplateRestartRequirement))
113141

114-
// Try setting a schedule with more than one time.
115-
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
116-
Schedule: "CRON_TZ=America/Chicago 0 0,12 * * *",
142+
client, user := coderdenttest.New(t, &coderdenttest.Options{
143+
Options: &coderdtest.Options{
144+
DeploymentValues: dv,
145+
},
146+
LicenseOptions: &coderdenttest.LicenseOptions{
147+
Features: license.Features{
148+
codersdk.FeatureAdvancedTemplateScheduling: 1,
149+
// Not entitled.
150+
// codersdk.FeatureTemplateRestartRequirement: 1,
151+
},
152+
},
153+
})
154+
155+
ctx := testutil.Context(t, testutil.WaitLong)
156+
_, err := client.UserQuietHoursSchedule(ctx, user.UserID.String())
157+
require.Error(t, err)
158+
var sdkErr *codersdk.Error
159+
require.ErrorAs(t, err, &sdkErr)
160+
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
117161
})
118-
require.Error(t, err)
119-
require.ErrorContains(t, err, "more than one time")
120-
_, err = client.UpdateUserQuietHoursSchedule(ctx, user.UserID.String(), codersdk.UpdateUserQuietHoursScheduleRequest{
121-
Schedule: "CRON_TZ=America/Chicago 0-30 0 * * *",
162+
163+
t.Run("NotEnabled", func(t *testing.T) {
164+
t.Parallel()
165+
166+
dv := coderdtest.DeploymentValues(t)
167+
dv.UserQuietHoursSchedule.DefaultSchedule.Set("")
168+
dv.Experiments.Set(string(codersdk.ExperimentTemplateRestartRequirement))
169+
170+
client, user := coderdenttest.New(t, &coderdenttest.Options{
171+
NoDefaultQuietHoursSchedule: true,
172+
Options: &coderdtest.Options{
173+
DeploymentValues: dv,
174+
},
175+
LicenseOptions: &coderdenttest.LicenseOptions{
176+
Features: license.Features{
177+
codersdk.FeatureAdvancedTemplateScheduling: 1,
178+
codersdk.FeatureTemplateRestartRequirement: 1,
179+
},
180+
},
181+
})
182+
183+
ctx := testutil.Context(t, testutil.WaitLong)
184+
_, err := client.UserQuietHoursSchedule(ctx, user.UserID.String())
185+
require.Error(t, err)
186+
var sdkErr *codersdk.Error
187+
require.ErrorAs(t, err, &sdkErr)
188+
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
122189
})
123-
require.Error(t, err)
124-
require.ErrorContains(t, err, "more than one time")
125190

126-
// We don't allow unsetting the custom schedule so we don't need to worry
127-
// about it in this test.
191+
t.Run("NoFeatureFlag", func(t *testing.T) {
192+
t.Parallel()
193+
194+
dv := coderdtest.DeploymentValues(t)
195+
dv.UserQuietHoursSchedule.DefaultSchedule.Set("CRON_TZ=America/Chicago 0 0 * * *")
196+
dv.UserQuietHoursSchedule.DefaultSchedule.Set("")
197+
198+
client, user := coderdenttest.New(t, &coderdenttest.Options{
199+
Options: &coderdtest.Options{
200+
DeploymentValues: dv,
201+
},
202+
LicenseOptions: &coderdenttest.LicenseOptions{
203+
Features: license.Features{
204+
codersdk.FeatureAdvancedTemplateScheduling: 1,
205+
codersdk.FeatureTemplateRestartRequirement: 1,
206+
},
207+
},
208+
})
209+
210+
ctx := testutil.Context(t, testutil.WaitLong)
211+
_, err := client.UserQuietHoursSchedule(ctx, user.UserID.String())
212+
require.Error(t, err)
213+
var sdkErr *codersdk.Error
214+
require.ErrorAs(t, err, &sdkErr)
215+
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
216+
})
128217
}

0 commit comments

Comments
 (0)