Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ec1fe46

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

12 files changed

+150
-108
lines changed

coderd/coderd.go

+30-26
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

+14-6
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

+4
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

+71
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

+3-3
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

+1-1
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

+10
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

-65
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

+1-1
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

+12-2
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)