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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy