Skip to content

Commit a76d136

Browse files
committed
Merge branch 'main' into abhineetjain/delete-session-token-api
2 parents c82c661 + ec1fe46 commit a76d136

20 files changed

+218
-115
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=soc
1010

1111
Coder creates remote development machines so you can develop your code from anywhere.
1212

13-
**Coder is in an alpha state.** But, any serious bugs are P1 for us so please report them.
13+
> **Note**:
14+
> Coder is in an alpha state, but any serious bugs are P1 for us so please report them.
1415
1516
<p align="center">
1617
<img src="./docs/images/hero-image.png">

coderd/coderd.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ func New(options *Options) *API {
101101
Message: "Route not found.",
102102
})
103103
})
104-
105104
r.Use(
106105
// Specific routes can specify smaller limits.
107106
httpmw.RateLimitPerMinute(options.APIRateLimit),
@@ -112,6 +111,9 @@ func New(options *Options) *API {
112111
Message: "👋",
113112
})
114113
})
114+
// All CSP errors will be logged
115+
r.Post("/csp/reports", api.logReportCSPViolations)
116+
115117
r.Route("/buildinfo", func(r chi.Router) {
116118
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
117119
httpapi.Write(rw, http.StatusOK, codersdk.BuildInfoResponse{
@@ -138,36 +140,41 @@ func New(options *Options) *API {
138140
)
139141
r.Get("/", api.provisionerDaemons)
140142
})
141-
r.Route("/organizations/{organization}", func(r chi.Router) {
143+
r.Route("/organizations", func(r chi.Router) {
142144
r.Use(
143145
apiKeyMiddleware,
144-
httpmw.ExtractOrganizationParam(options.Database),
145146
authRolesMiddleware,
146147
)
147-
r.Get("/", api.organization)
148-
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
149-
r.Route("/templates", func(r chi.Router) {
150-
r.Post("/", api.postTemplateByOrganization)
151-
r.Get("/", api.templatesByOrganization)
152-
r.Get("/{templatename}", api.templateByOrganizationAndName)
153-
})
154-
r.Route("/workspaces", func(r chi.Router) {
155-
r.Post("/", api.postWorkspacesByOrganization)
156-
r.Get("/", api.workspacesByOrganization)
157-
r.Route("/{user}", func(r chi.Router) {
158-
r.Use(httpmw.ExtractUserParam(options.Database))
159-
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
160-
r.Get("/", api.workspacesByOwner)
148+
r.Post("/", api.postOrganizations)
149+
r.Route("/{organization}", func(r chi.Router) {
150+
r.Use(
151+
httpmw.ExtractOrganizationParam(options.Database),
152+
)
153+
r.Get("/", api.organization)
154+
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
155+
r.Route("/templates", func(r chi.Router) {
156+
r.Post("/", api.postTemplateByOrganization)
157+
r.Get("/", api.templatesByOrganization)
158+
r.Get("/{templatename}", api.templateByOrganizationAndName)
161159
})
162-
})
163-
r.Route("/members", func(r chi.Router) {
164-
r.Get("/roles", api.assignableOrgRoles)
165-
r.Route("/{user}", func(r chi.Router) {
166-
r.Use(
167-
httpmw.ExtractUserParam(options.Database),
168-
httpmw.ExtractOrganizationMemberParam(options.Database),
169-
)
170-
r.Put("/roles", api.putMemberRoles)
160+
r.Route("/workspaces", func(r chi.Router) {
161+
r.Post("/", api.postWorkspacesByOrganization)
162+
r.Get("/", api.workspacesByOrganization)
163+
r.Route("/{user}", func(r chi.Router) {
164+
r.Use(httpmw.ExtractUserParam(options.Database))
165+
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
166+
r.Get("/", api.workspacesByOwner)
167+
})
168+
})
169+
r.Route("/members", func(r chi.Router) {
170+
r.Get("/roles", api.assignableOrgRoles)
171+
r.Route("/{user}", func(r chi.Router) {
172+
r.Use(
173+
httpmw.ExtractUserParam(options.Database),
174+
httpmw.ExtractOrganizationMemberParam(options.Database),
175+
)
176+
r.Put("/roles", api.putMemberRoles)
177+
})
171178
})
172179
})
173180
})
@@ -250,7 +257,6 @@ func New(options *Options) *API {
250257

251258
r.Post("/keys", api.postAPIKey)
252259
r.Route("/organizations", func(r chi.Router) {
253-
r.Post("/", api.postOrganizationsByUser)
254260
r.Get("/", api.organizationsByUser)
255261
r.Get("/{organizationname}", api.organizationByUserAndName)
256262
})

coderd/coderd_test.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
122122
"POST:/api/v2/users/first": {NoAuthorize: true},
123123
"POST:/api/v2/users/login": {NoAuthorize: true},
124124
"GET:/api/v2/users/authmethods": {NoAuthorize: true},
125+
"POST:/api/v2/csp/reports": {NoAuthorize: true},
125126

126127
// Has it's own auth
127128
"GET:/api/v2/users/oauth2/github/callback": {NoAuthorize: true},
@@ -141,12 +142,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
141142
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
142143
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
143144

144-
// TODO: @emyrk these need to be fixed by adding authorize calls
145-
"POST:/api/v2/organizations/{organization}/workspaces": {NoAuthorize: true},
146-
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
147-
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
148-
"POST:/api/v2/organizations/{organization}/templateversions": {NoAuthorize: true},
149-
150145
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
151146
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
152147
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
@@ -288,11 +283,25 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
288283
AssertAction: rbac.ActionRead,
289284
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
290285
},
286+
"POST:/api/v2/organizations/{organization}/workspaces": {
287+
AssertAction: rbac.ActionCreate,
288+
// No ID when creating
289+
AssertObject: workspaceRBACObj.WithID(""),
290+
},
291+
"GET:/api/v2/workspaces/{workspace}/watch": {
292+
AssertAction: rbac.ActionRead,
293+
AssertObject: workspaceRBACObj,
294+
},
295+
"POST:/api/v2/users/{user}/organizations/": {
296+
AssertAction: rbac.ActionCreate,
297+
AssertObject: rbac.ResourceOrganization,
298+
},
291299

292300
// These endpoints need payloads to get to the auth part. Payloads will be required
293301
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
294302
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
295303
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
304+
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
296305
}
297306

298307
for k, v := range assertRoute {

coderd/csp.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package coderd
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
7+
"github.com/coder/coder/coderd/httpapi"
8+
9+
"cdr.dev/slog"
10+
)
11+
12+
type cspViolation struct {
13+
Report map[string]interface{} `json:"csp-report"`
14+
}
15+
16+
// logReportCSPViolations will log all reported csp violations.
17+
func (api *API) logReportCSPViolations(rw http.ResponseWriter, r *http.Request) {
18+
ctx := r.Context()
19+
var v cspViolation
20+
21+
dec := json.NewDecoder(r.Body)
22+
err := dec.Decode(&v)
23+
if err != nil {
24+
api.Logger.Warn(ctx, "csp violation", slog.Error(err))
25+
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
26+
Message: "failed to read body",
27+
})
28+
return
29+
}
30+
31+
fields := make([]slog.Field, 0, len(v.Report))
32+
for k, v := range v.Report {
33+
fields = append(fields, slog.F(k, v))
34+
}
35+
api.Logger.Warn(ctx, "csp violation", fields...)
36+
37+
httpapi.Write(rw, http.StatusOK, "ok")
38+
}

coderd/database/modelmethods.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@ func (o Organization) RBACObject() rbac.Object {
2626
func (d ProvisionerDaemon) RBACObject() rbac.Object {
2727
return rbac.ResourceProvisionerDaemon.WithID(d.ID.String())
2828
}
29+
30+
func (f File) RBACObject() rbac.Object {
31+
return rbac.ResourceFile.WithID(f.Hash).WithOwner(f.CreatedBy.String())
32+
}

coderd/organizations.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
package coderd
22

33
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
47
"net/http"
58

9+
"github.com/google/uuid"
10+
"golang.org/x/xerrors"
11+
612
"github.com/coder/coder/coderd/database"
713
"github.com/coder/coder/coderd/httpapi"
814
"github.com/coder/coder/coderd/httpmw"
@@ -22,6 +28,71 @@ func (api *API) organization(rw http.ResponseWriter, r *http.Request) {
2228
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
2329
}
2430

31+
func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
32+
apiKey := httpmw.APIKey(r)
33+
// Create organization uses the organization resource without an OrgID.
34+
// This means you need the site wide permission to make a new organization.
35+
if !api.Authorize(rw, r, rbac.ActionCreate,
36+
rbac.ResourceOrganization) {
37+
return
38+
}
39+
40+
var req codersdk.CreateOrganizationRequest
41+
if !httpapi.Read(rw, r, &req) {
42+
return
43+
}
44+
45+
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
46+
if err == nil {
47+
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
48+
Message: "organization already exists with that name",
49+
})
50+
return
51+
}
52+
if !errors.Is(err, sql.ErrNoRows) {
53+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
54+
Message: fmt.Sprintf("get organization: %s", err.Error()),
55+
})
56+
return
57+
}
58+
59+
var organization database.Organization
60+
err = api.Database.InTx(func(db database.Store) error {
61+
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
62+
ID: uuid.New(),
63+
Name: req.Name,
64+
CreatedAt: database.Now(),
65+
UpdatedAt: database.Now(),
66+
})
67+
if err != nil {
68+
return xerrors.Errorf("create organization: %w", err)
69+
}
70+
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
71+
OrganizationID: organization.ID,
72+
UserID: apiKey.UserID,
73+
CreatedAt: database.Now(),
74+
UpdatedAt: database.Now(),
75+
Roles: []string{
76+
// Also assign member role incase they get demoted from admin
77+
rbac.RoleOrgMember(organization.ID),
78+
rbac.RoleOrgAdmin(organization.ID),
79+
},
80+
})
81+
if err != nil {
82+
return xerrors.Errorf("create organization member: %w", err)
83+
}
84+
return nil
85+
})
86+
if err != nil {
87+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
88+
Message: err.Error(),
89+
})
90+
return
91+
}
92+
93+
httpapi.Write(rw, http.StatusCreated, convertOrganization(organization))
94+
}
95+
2596
// convertOrganization consumes the database representation and outputs an API friendly representation.
2697
func convertOrganization(organization database.Organization) codersdk.Organization {
2798
return codersdk.Organization{

coderd/organizations_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
3838
client := coderdtest.New(t, nil)
3939
first := coderdtest.CreateFirstUser(t, client)
4040
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
41-
org, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
41+
org, err := client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
4242
Name: "another",
4343
})
4444
require.NoError(t, err)
@@ -67,7 +67,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
6767
user := coderdtest.CreateFirstUser(t, client)
6868
org, err := client.Organization(context.Background(), user.OrganizationID)
6969
require.NoError(t, err)
70-
_, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
70+
_, err = client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
7171
Name: org.Name,
7272
})
7373
var apiErr *codersdk.Error
@@ -79,7 +79,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
7979
t.Parallel()
8080
client := coderdtest.New(t, nil)
8181
_ = coderdtest.CreateFirstUser(t, client)
82-
_, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
82+
_, err := client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
8383
Name: "new",
8484
})
8585
require.NoError(t, err)

coderd/roles_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func TestListRoles(t *testing.T) {
107107
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
108108
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID))
109109

110-
otherOrg, err := client.CreateOrganization(ctx, admin.UserID.String(), codersdk.CreateOrganizationRequest{
110+
otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
111111
Name: "other",
112112
})
113113
require.NoError(t, err, "create org")

coderd/templateversions.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
310310
if !httpapi.Read(rw, r, &req) {
311311
return
312312
}
313+
313314
if req.TemplateID != uuid.Nil {
314315
_, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID)
315316
if errors.Is(err, sql.ErrNoRows) {
@@ -340,6 +341,15 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
340341
return
341342
}
342343

344+
// Making a new template version is the same permission as creating a new template.
345+
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
346+
return
347+
}
348+
349+
if !api.Authorize(rw, r, rbac.ActionRead, file) {
350+
return
351+
}
352+
343353
var templateVersion database.TemplateVersion
344354
var provisionerJob database.ProvisionerJob
345355
err = api.Database.InTx(func(db database.Store) error {

0 commit comments

Comments
 (0)