Skip to content

Commit b6f5d78

Browse files
committed
Merge branch 'main' into colin/auditgitsshkey
2 parents 5870456 + dd8ebf1 commit b6f5d78

File tree

117 files changed

+2235
-797
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

117 files changed

+2235
-797
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"drpcserver",
2626
"Dsts",
2727
"enablements",
28+
"eventsourcemock",
2829
"fatih",
2930
"Formik",
3031
"gitsshkey",

cli/deployment/flags.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func Flags() *codersdk.DeploymentFlags {
3232
Name: "Wildcard Address URL",
3333
Flag: "wildcard-access-url",
3434
EnvVar: "CODER_WILDCARD_ACCESS_URL",
35-
Description: `Specifies the wildcard hostname to use for workspace applications in the form "*.example.com".`,
35+
Description: `Specifies the wildcard hostname to use for workspace applications in the form "*.example.com" or "*-suffix.example.com". Ports or schemes should not be included. The scheme will be copied from the access URL.`,
3636
},
3737
Address: &codersdk.StringFlag{
3838
Name: "Bind Address",

cli/server.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"os/signal"
1818
"os/user"
1919
"path/filepath"
20+
"regexp"
2021
"strconv"
2122
"strings"
2223
"sync"
@@ -53,6 +54,7 @@ import (
5354
"github.com/coder/coder/coderd/database/migrations"
5455
"github.com/coder/coder/coderd/devtunnel"
5556
"github.com/coder/coder/coderd/gitsshkey"
57+
"github.com/coder/coder/coderd/httpapi"
5658
"github.com/coder/coder/coderd/prometheusmetrics"
5759
"github.com/coder/coder/coderd/telemetry"
5860
"github.com/coder/coder/coderd/tracing"
@@ -297,13 +299,19 @@ func Server(dflags *codersdk.DeploymentFlags, newAPI func(context.Context, *code
297299
return xerrors.Errorf("create derp map: %w", err)
298300
}
299301

300-
appHostname := strings.TrimPrefix(dflags.WildcardAccessURL.Value, "http://")
301-
appHostname = strings.TrimPrefix(appHostname, "https://")
302-
appHostname = strings.TrimPrefix(appHostname, "*.")
302+
appHostname := strings.TrimSpace(dflags.WildcardAccessURL.Value)
303+
var appHostnameRegex *regexp.Regexp
304+
if appHostname != "" {
305+
appHostnameRegex, err = httpapi.CompileHostnamePattern(appHostname)
306+
if err != nil {
307+
return xerrors.Errorf("parse wildcard access URL %q: %w", appHostname, err)
308+
}
309+
}
303310

304311
options := &coderd.Options{
305312
AccessURL: accessURLParsed,
306313
AppHostname: appHostname,
314+
AppHostnameRegex: appHostnameRegex,
307315
Logger: logger.Named("coderd"),
308316
Database: databasefake.New(),
309317
DERPMap: derpMap,

cli/templatecreate.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"unicode/utf8"
1111

1212
"github.com/briandowns/spinner"
13+
"github.com/google/uuid"
1314
"github.com/spf13/cobra"
1415
"golang.org/x/xerrors"
1516

@@ -91,7 +92,7 @@ func templateCreate() *cobra.Command {
9192
Client: client,
9293
Organization: organization,
9394
Provisioner: database.ProvisionerType(provisioner),
94-
FileHash: resp.Hash,
95+
FileID: resp.ID,
9596
ParameterFile: parameterFile,
9697
})
9798
if err != nil {
@@ -148,7 +149,7 @@ type createValidTemplateVersionArgs struct {
148149
Client *codersdk.Client
149150
Organization codersdk.Organization
150151
Provisioner database.ProvisionerType
151-
FileHash string
152+
FileID uuid.UUID
152153
ParameterFile string
153154
// Template is only required if updating a template's active version.
154155
Template *codersdk.Template
@@ -165,7 +166,7 @@ func createValidTemplateVersion(cmd *cobra.Command, args createValidTemplateVers
165166
req := codersdk.CreateTemplateVersionRequest{
166167
Name: args.Name,
167168
StorageMethod: codersdk.ProvisionerStorageMethodFile,
168-
StorageSource: args.FileHash,
169+
FileID: args.FileID,
169170
Provisioner: codersdk.ProvisionerType(args.Provisioner),
170171
ParameterValues: parameters,
171172
}

cli/templatepull.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func templatePull() *cobra.Command {
6666
latest := versions[0]
6767

6868
// Download the tar archive.
69-
raw, ctype, err := client.Download(ctx, latest.Job.StorageSource)
69+
raw, ctype, err := client.Download(ctx, latest.Job.FileID)
7070
if err != nil {
7171
return xerrors.Errorf("download template: %w", err)
7272
}

cli/templatepush.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func templatePush() *cobra.Command {
8080
Client: client,
8181
Organization: organization,
8282
Provisioner: database.ProvisionerType(provisioner),
83-
FileHash: resp.Hash,
83+
FileID: resp.ID,
8484
ParameterFile: parameterFile,
8585
Template: &template,
8686
ReuseParameters: !alwaysPrompt,

cli/tokens.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func createToken() *cobra.Command {
5555
return xerrors.Errorf("create codersdk client: %w", err)
5656
}
5757

58-
res, err := client.CreateToken(cmd.Context(), codersdk.Me)
58+
res, err := client.CreateToken(cmd.Context(), codersdk.Me, codersdk.CreateTokenRequest{})
5959
if err != nil {
6060
return xerrors.Errorf("create tokens: %w", err)
6161
}

coderd/activitybump_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ func TestWorkspaceActivityBump(t *testing.T) {
2323
setupActivityTest := func(t *testing.T) (client *codersdk.Client, workspace codersdk.Workspace, assertBumped func(want bool)) {
2424
var ttlMillis int64 = 60 * 1000
2525

26-
client, _, workspace, _ = setupProxyTest(t, func(cwr *codersdk.CreateWorkspaceRequest) {
26+
client = coderdtest.New(t, &coderdtest.Options{
27+
AppHostname: proxyTestSubdomainRaw,
28+
IncludeProvisionerDaemon: true,
29+
AgentStatsRefreshInterval: time.Millisecond * 100,
30+
MetricsCacheRefreshInterval: time.Millisecond * 100,
31+
})
32+
user := coderdtest.CreateFirstUser(t, client)
33+
34+
workspace = createWorkspaceWithApps(t, client, user.OrganizationID, 1234, func(cwr *codersdk.CreateWorkspaceRequest) {
2735
cwr.TTLMillis = &ttlMillis
2836
})
2937

coderd/apikey.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,23 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
3434
return
3535
}
3636

37+
var createToken codersdk.CreateTokenRequest
38+
if !httpapi.Read(ctx, rw, r, &createToken) {
39+
return
40+
}
41+
42+
scope := database.APIKeyScopeAll
43+
if scope != "" {
44+
scope = database.APIKeyScope(createToken.Scope)
45+
}
46+
3747
// tokens last 100 years
3848
lifeTime := time.Hour * 876000
3949
cookie, err := api.createAPIKey(ctx, createAPIKeyParams{
4050
UserID: user.ID,
4151
LoginType: database.LoginTypeToken,
4252
ExpiresAt: database.Now().Add(lifeTime),
53+
Scope: scope,
4354
LifetimeSeconds: int64(lifeTime.Seconds()),
4455
})
4556
if err != nil {
@@ -54,6 +65,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
5465
}
5566

5667
// Creates a new session key, used for logging in via the CLI.
68+
// DEPRECATED: use postToken instead.
5769
func (api *API) postAPIKey(rw http.ResponseWriter, r *http.Request) {
5870
ctx := r.Context()
5971
user := httpmw.UserParam(r)
@@ -229,6 +241,11 @@ func (api *API) createAPIKey(ctx context.Context, params createAPIKeyParams) (*h
229241
if params.Scope != "" {
230242
scope = params.Scope
231243
}
244+
switch scope {
245+
case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect:
246+
default:
247+
return nil, xerrors.Errorf("invalid API key scope: %q", scope)
248+
}
232249

233250
key, err := api.Database.InsertAPIKey(ctx, database.InsertAPIKeyParams{
234251
ID: keyID,

coderd/apikey_test.go

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,61 @@ import (
1414

1515
func TestTokens(t *testing.T) {
1616
t.Parallel()
17-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
18-
defer cancel()
19-
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
20-
_ = coderdtest.CreateFirstUser(t, client)
21-
keys, err := client.GetTokens(ctx, codersdk.Me)
22-
require.NoError(t, err)
23-
require.Empty(t, keys)
2417

25-
res, err := client.CreateToken(ctx, codersdk.Me)
26-
require.NoError(t, err)
27-
require.Greater(t, len(res.Key), 2)
18+
t.Run("CRUD", func(t *testing.T) {
19+
t.Parallel()
2820

29-
keys, err = client.GetTokens(ctx, codersdk.Me)
30-
require.NoError(t, err)
31-
require.EqualValues(t, len(keys), 1)
32-
require.Contains(t, res.Key, keys[0].ID)
33-
// expires_at must be greater than 50 years
34-
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
21+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
22+
defer cancel()
23+
client := coderdtest.New(t, nil)
24+
_ = coderdtest.CreateFirstUser(t, client)
25+
keys, err := client.GetTokens(ctx, codersdk.Me)
26+
require.NoError(t, err)
27+
require.Empty(t, keys)
3528

36-
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
37-
require.NoError(t, err)
38-
keys, err = client.GetTokens(ctx, codersdk.Me)
39-
require.NoError(t, err)
40-
require.Empty(t, keys)
29+
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{})
30+
require.NoError(t, err)
31+
require.Greater(t, len(res.Key), 2)
32+
33+
keys, err = client.GetTokens(ctx, codersdk.Me)
34+
require.NoError(t, err)
35+
require.EqualValues(t, len(keys), 1)
36+
require.Contains(t, res.Key, keys[0].ID)
37+
// expires_at must be greater than 50 years
38+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
39+
require.Equal(t, codersdk.APIKeyScopeAll, keys[0].Scope)
40+
41+
// no update
42+
43+
err = client.DeleteAPIKey(ctx, codersdk.Me, keys[0].ID)
44+
require.NoError(t, err)
45+
keys, err = client.GetTokens(ctx, codersdk.Me)
46+
require.NoError(t, err)
47+
require.Empty(t, keys)
48+
})
49+
50+
t.Run("Scoped", func(t *testing.T) {
51+
t.Parallel()
52+
53+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
54+
defer cancel()
55+
client := coderdtest.New(t, nil)
56+
_ = coderdtest.CreateFirstUser(t, client)
57+
58+
res, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
59+
Scope: codersdk.APIKeyScopeApplicationConnect,
60+
})
61+
require.NoError(t, err)
62+
require.Greater(t, len(res.Key), 2)
63+
64+
keys, err := client.GetTokens(ctx, codersdk.Me)
65+
require.NoError(t, err)
66+
require.EqualValues(t, len(keys), 1)
67+
require.Contains(t, res.Key, keys[0].ID)
68+
// expires_at must be greater than 50 years
69+
require.Greater(t, keys[0].ExpiresAt, time.Now().Add(time.Hour*438300))
70+
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
71+
})
4172
}
4273

4374
func TestAPIKey(t *testing.T) {

coderd/audit/audit.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ func (nop) Export(context.Context, database.AuditLog) error {
2121
return nil
2222
}
2323

24-
func (nop) diff(any, any) Map { return Map{} }
24+
func (nop) diff(any, any) Map {
25+
return Map{}
26+
}
2527

2628
func NewMock() *MockAuditor {
2729
return &MockAuditor{}
@@ -36,4 +38,6 @@ func (a *MockAuditor) Export(_ context.Context, alog database.AuditLog) error {
3638
return nil
3739
}
3840

39-
func (*MockAuditor) diff(any, any) Map { return Map{} }
41+
func (*MockAuditor) diff(any, any) Map {
42+
return Map{}
43+
}

coderd/autobuild/executor/lifecycle_executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ func build(ctx context.Context, store database.Store, workspace database.Workspa
276276
Provisioner: template.Provisioner,
277277
Type: database.ProvisionerJobTypeWorkspaceBuild,
278278
StorageMethod: priorJob.StorageMethod,
279-
StorageSource: priorJob.StorageSource,
279+
FileID: priorJob.FileID,
280280
Input: input,
281281
})
282282
if err != nil {

coderd/coderd.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/url"
88
"path/filepath"
9+
"regexp"
910
"sync"
1011
"sync/atomic"
1112
"time"
@@ -46,11 +47,16 @@ import (
4647
type Options struct {
4748
AccessURL *url.URL
4849
// AppHostname should be the wildcard hostname to use for workspace
49-
// applications without the asterisk or leading dot. E.g. "apps.coder.com".
50+
// applications INCLUDING the asterisk, (optional) suffix and leading dot.
51+
// E.g. "*.apps.coder.com" or "*-apps.coder.com".
5052
AppHostname string
51-
Logger slog.Logger
52-
Database database.Store
53-
Pubsub database.Pubsub
53+
// AppHostnameRegex contains the regex version of options.AppHostname as
54+
// generated by httpapi.CompileHostnamePattern(). It MUST be set if
55+
// options.AppHostname is set.
56+
AppHostnameRegex *regexp.Regexp
57+
Logger slog.Logger
58+
Database database.Store
59+
Pubsub database.Pubsub
5460

5561
// CacheDir is used for caching files served by the API.
5662
CacheDir string
@@ -90,6 +96,9 @@ func New(options *Options) *API {
9096
if options == nil {
9197
options = &Options{}
9298
}
99+
if options.AppHostname != "" && options.AppHostnameRegex == nil || options.AppHostname == "" && options.AppHostnameRegex != nil {
100+
panic("coderd: both AppHostname and AppHostnameRegex must be set or unset")
101+
}
93102
if options.AgentConnectionUpdateFrequency == 0 {
94103
options.AgentConnectionUpdateFrequency = 3 * time.Second
95104
}
@@ -197,7 +206,7 @@ func New(options *Options) *API {
197206
RedirectToLogin: false,
198207
Optional: true,
199208
}),
200-
httpmw.ExtractUserParam(api.Database),
209+
httpmw.ExtractUserParam(api.Database, false),
201210
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
202211
),
203212
// Build-Version is helpful for debugging.
@@ -214,8 +223,18 @@ func New(options *Options) *API {
214223
r.Use(
215224
tracing.Middleware(api.TracerProvider),
216225
httpmw.RateLimitPerMinute(options.APIRateLimit),
217-
apiKeyMiddlewareRedirect,
218-
httpmw.ExtractUserParam(api.Database),
226+
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
227+
DB: options.Database,
228+
OAuth2Configs: oauthConfigs,
229+
// Optional is true to allow for public apps. If an
230+
// authorization check fails and the user is not authenticated,
231+
// they will be redirected to the login page by the app handler.
232+
RedirectToLogin: false,
233+
Optional: true,
234+
}),
235+
// Redirect to the login page if the user tries to open an app with
236+
// "me" as the username and they are not logged in.
237+
httpmw.ExtractUserParam(api.Database, true),
219238
// Extracts the <workspace.agent> from the url
220239
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
221240
)
@@ -280,7 +299,7 @@ func New(options *Options) *API {
280299
// file content is expensive so it should be small.
281300
httpmw.RateLimitPerMinute(12),
282301
)
283-
r.Get("/{hash}", api.fileByHash)
302+
r.Get("/{fileID}", api.fileByID)
284303
r.Post("/", api.postFile)
285304
})
286305

@@ -310,7 +329,7 @@ func New(options *Options) *API {
310329
r.Get("/roles", api.assignableOrgRoles)
311330
r.Route("/{user}", func(r chi.Router) {
312331
r.Use(
313-
httpmw.ExtractUserParam(options.Database),
332+
httpmw.ExtractUserParam(options.Database, false),
314333
httpmw.ExtractOrganizationMemberParam(options.Database),
315334
)
316335
r.Put("/roles", api.putMemberRoles)
@@ -389,7 +408,7 @@ func New(options *Options) *API {
389408
r.Get("/", api.assignableSiteRoles)
390409
})
391410
r.Route("/{user}", func(r chi.Router) {
392-
r.Use(httpmw.ExtractUserParam(options.Database))
411+
r.Use(httpmw.ExtractUserParam(options.Database, false))
393412
r.Delete("/", api.deleteUser)
394413
r.Get("/", api.userByName)
395414
r.Put("/profile", api.putUserProfile)

0 commit comments

Comments
 (0)