Skip to content

Commit d898737

Browse files
authored
feat: app sharing (now open source!) (#4378)
1 parent 19d7281 commit d898737

File tree

55 files changed

+1069
-412
lines changed

Some content is hidden

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

55 files changed

+1069
-412
lines changed

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/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/coderd.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func New(options *Options) *API {
197197
RedirectToLogin: false,
198198
Optional: true,
199199
}),
200-
httpmw.ExtractUserParam(api.Database),
200+
httpmw.ExtractUserParam(api.Database, false),
201201
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
202202
),
203203
// Build-Version is helpful for debugging.
@@ -214,8 +214,18 @@ func New(options *Options) *API {
214214
r.Use(
215215
tracing.Middleware(api.TracerProvider),
216216
httpmw.RateLimitPerMinute(options.APIRateLimit),
217-
apiKeyMiddlewareRedirect,
218-
httpmw.ExtractUserParam(api.Database),
217+
httpmw.ExtractAPIKey(httpmw.ExtractAPIKeyConfig{
218+
DB: options.Database,
219+
OAuth2Configs: oauthConfigs,
220+
// Optional is true to allow for public apps. If an
221+
// authorization check fails and the user is not authenticated,
222+
// they will be redirected to the login page by the app handler.
223+
RedirectToLogin: false,
224+
Optional: true,
225+
}),
226+
// Redirect to the login page if the user tries to open an app with
227+
// "me" as the username and they are not logged in.
228+
httpmw.ExtractUserParam(api.Database, true),
219229
// Extracts the <workspace.agent> from the url
220230
httpmw.ExtractWorkspaceAndAgentParam(api.Database),
221231
)
@@ -310,7 +320,7 @@ func New(options *Options) *API {
310320
r.Get("/roles", api.assignableOrgRoles)
311321
r.Route("/{user}", func(r chi.Router) {
312322
r.Use(
313-
httpmw.ExtractUserParam(options.Database),
323+
httpmw.ExtractUserParam(options.Database, false),
314324
httpmw.ExtractOrganizationMemberParam(options.Database),
315325
)
316326
r.Put("/roles", api.putMemberRoles)
@@ -389,7 +399,7 @@ func New(options *Options) *API {
389399
r.Get("/", api.assignableSiteRoles)
390400
})
391401
r.Route("/{user}", func(r chi.Router) {
392-
r.Use(httpmw.ExtractUserParam(options.Database))
402+
r.Use(httpmw.ExtractUserParam(options.Database, false))
393403
r.Delete("/", api.deleteUser)
394404
r.Get("/", api.userByName)
395405
r.Put("/profile", api.putUserProfile)

coderd/database/databasefake/databasefake.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2324,6 +2324,10 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
23242324
q.mutex.Lock()
23252325
defer q.mutex.Unlock()
23262326

2327+
if arg.SharingLevel == "" {
2328+
arg.SharingLevel = database.AppSharingLevelOwner
2329+
}
2330+
23272331
// nolint:gosimple
23282332
workspaceApp := database.WorkspaceApp{
23292333
ID: arg.ID,
@@ -2334,6 +2338,7 @@ func (q *fakeQuerier) InsertWorkspaceApp(_ context.Context, arg database.InsertW
23342338
Command: arg.Command,
23352339
Url: arg.Url,
23362340
Subdomain: arg.Subdomain,
2341+
SharingLevel: arg.SharingLevel,
23372342
HealthcheckUrl: arg.HealthcheckUrl,
23382343
HealthcheckInterval: arg.HealthcheckInterval,
23392344
HealthcheckThreshold: arg.HealthcheckThreshold,

coderd/database/dump.sql

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Drop column sharing_level from workspace_apps
2+
ALTER TABLE workspace_apps DROP COLUMN sharing_level;
3+
4+
-- Drop type app_sharing_level
5+
DROP TYPE app_sharing_level;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Add enum app_sharing_level
2+
CREATE TYPE app_sharing_level AS ENUM (
3+
-- only the workspace owner can access the app
4+
'owner',
5+
-- any authenticated user on the site can access the app
6+
'authenticated',
7+
-- any user can access the app even if they are not authenticated
8+
'public'
9+
);
10+
11+
-- Add sharing_level column to workspace_apps table
12+
ALTER TABLE workspace_apps ADD COLUMN sharing_level app_sharing_level NOT NULL DEFAULT 'owner'::app_sharing_level;

coderd/database/models.go

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

coderd/database/queries.sql.go

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

0 commit comments

Comments
 (0)