Skip to content

Commit ec1fe46

Browse files
authored
feat: Move create organizations route (#1831)
* feat: last rbac routes - move create organization to /organizations.
1 parent d73a0f4 commit ec1fe46

File tree

12 files changed

+150
-108
lines changed

12 files changed

+150
-108
lines changed

coderd/coderd.go

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -140,36 +140,41 @@ func New(options *Options) *API {
140140
)
141141
r.Get("/", api.provisionerDaemons)
142142
})
143-
r.Route("/organizations/{organization}", func(r chi.Router) {
143+
r.Route("/organizations", func(r chi.Router) {
144144
r.Use(
145145
apiKeyMiddleware,
146-
httpmw.ExtractOrganizationParam(options.Database),
147146
authRolesMiddleware,
148147
)
149-
r.Get("/", api.organization)
150-
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
151-
r.Route("/templates", func(r chi.Router) {
152-
r.Post("/", api.postTemplateByOrganization)
153-
r.Get("/", api.templatesByOrganization)
154-
r.Get("/{templatename}", api.templateByOrganizationAndName)
155-
})
156-
r.Route("/workspaces", func(r chi.Router) {
157-
r.Post("/", api.postWorkspacesByOrganization)
158-
r.Get("/", api.workspacesByOrganization)
159-
r.Route("/{user}", func(r chi.Router) {
160-
r.Use(httpmw.ExtractUserParam(options.Database))
161-
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
162-
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)
163159
})
164-
})
165-
r.Route("/members", func(r chi.Router) {
166-
r.Get("/roles", api.assignableOrgRoles)
167-
r.Route("/{user}", func(r chi.Router) {
168-
r.Use(
169-
httpmw.ExtractUserParam(options.Database),
170-
httpmw.ExtractOrganizationMemberParam(options.Database),
171-
)
172-
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+
})
173178
})
174179
})
175180
})
@@ -252,7 +257,6 @@ func New(options *Options) *API {
252257

253258
r.Post("/keys", api.postAPIKey)
254259
r.Route("/organizations", func(r chi.Router) {
255-
r.Post("/", api.postOrganizationsByUser)
256260
r.Get("/", api.organizationsByUser)
257261
r.Get("/{organizationname}", api.organizationByUserAndName)
258262
})

coderd/coderd_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
139139
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
140140
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},
141141

142-
// TODO: @emyrk these need to be fixed by adding authorize calls
143-
"POST:/api/v2/organizations/{organization}/workspaces": {NoAuthorize: true},
144-
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
145-
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
146-
"POST:/api/v2/organizations/{organization}/templateversions": {NoAuthorize: true},
147-
148142
// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
149143
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
150144
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
@@ -286,11 +280,25 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
286280
AssertAction: rbac.ActionRead,
287281
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
288282
},
283+
"POST:/api/v2/organizations/{organization}/workspaces": {
284+
AssertAction: rbac.ActionCreate,
285+
// No ID when creating
286+
AssertObject: workspaceRBACObj.WithID(""),
287+
},
288+
"GET:/api/v2/workspaces/{workspace}/watch": {
289+
AssertAction: rbac.ActionRead,
290+
AssertObject: workspaceRBACObj,
291+
},
292+
"POST:/api/v2/users/{user}/organizations/": {
293+
AssertAction: rbac.ActionCreate,
294+
AssertObject: rbac.ResourceOrganization,
295+
},
289296

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

296304
for k, v := range assertRoute {

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 {

coderd/users.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -539,71 +539,6 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
539539
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
540540
}
541541

542-
func (api *API) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
543-
user := httpmw.UserParam(r)
544-
var req codersdk.CreateOrganizationRequest
545-
if !httpapi.Read(rw, r, &req) {
546-
return
547-
}
548-
549-
// Create organization uses the organization resource without an OrgID.
550-
// This means you need the site wide permission to make a new organization.
551-
if !api.Authorize(rw, r, rbac.ActionCreate,
552-
rbac.ResourceOrganization) {
553-
return
554-
}
555-
556-
_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
557-
if err == nil {
558-
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
559-
Message: "organization already exists with that name",
560-
})
561-
return
562-
}
563-
if !errors.Is(err, sql.ErrNoRows) {
564-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
565-
Message: fmt.Sprintf("get organization: %s", err.Error()),
566-
})
567-
return
568-
}
569-
570-
var organization database.Organization
571-
err = api.Database.InTx(func(db database.Store) error {
572-
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
573-
ID: uuid.New(),
574-
Name: req.Name,
575-
CreatedAt: database.Now(),
576-
UpdatedAt: database.Now(),
577-
})
578-
if err != nil {
579-
return xerrors.Errorf("create organization: %w", err)
580-
}
581-
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
582-
OrganizationID: organization.ID,
583-
UserID: user.ID,
584-
CreatedAt: database.Now(),
585-
UpdatedAt: database.Now(),
586-
Roles: []string{
587-
// Also assign member role incase they get demoted from admin
588-
rbac.RoleOrgMember(organization.ID),
589-
rbac.RoleOrgAdmin(organization.ID),
590-
},
591-
})
592-
if err != nil {
593-
return xerrors.Errorf("create organization member: %w", err)
594-
}
595-
return nil
596-
})
597-
if err != nil {
598-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
599-
Message: err.Error(),
600-
})
601-
return
602-
}
603-
604-
httpapi.Write(rw, http.StatusCreated, convertOrganization(organization))
605-
}
606-
607542
// Authenticates the user with an email and password.
608543
func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
609544
var loginWithPassword codersdk.LoginWithPasswordRequest

coderd/users_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func TestPostUsers(t *testing.T) {
174174
first := coderdtest.CreateFirstUser(t, client)
175175
notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
176176
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember())
177-
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
177+
org, err := other.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
178178
Name: "another",
179179
})
180180
require.NoError(t, err)

coderd/workspaces.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,18 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)
281281

282282
// Create a new workspace for the currently authenticated user.
283283
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
284+
organization := httpmw.OrganizationParam(r)
285+
apiKey := httpmw.APIKey(r)
286+
if !api.Authorize(rw, r, rbac.ActionCreate,
287+
rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(apiKey.UserID.String())) {
288+
return
289+
}
290+
284291
var createWorkspace codersdk.CreateWorkspaceRequest
285292
if !httpapi.Read(rw, r, &createWorkspace) {
286293
return
287294
}
288-
apiKey := httpmw.APIKey(r)
295+
289296
template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
290297
if errors.Is(err, sql.ErrNoRows) {
291298
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
@@ -303,7 +310,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
303310
})
304311
return
305312
}
306-
organization := httpmw.OrganizationParam(r)
313+
307314
if organization.ID != template.OrganizationID {
308315
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
309316
Message: fmt.Sprintf("template is not in organization %q", organization.Name),
@@ -636,6 +643,9 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
636643

637644
func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
638645
workspace := httpmw.WorkspaceParam(r)
646+
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
647+
return
648+
}
639649

640650
c, err := websocket.Accept(rw, r, &websocket.AcceptOptions{
641651
// Fix for Safari 15.1:

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