Skip to content

Commit

Permalink
feat: Move create organizations route (#1831)
Browse files Browse the repository at this point in the history
* feat: last rbac routes
- move create organization to /organizations.
  • Loading branch information
Emyrk committed May 27, 2022
1 parent d73a0f4 commit ec1fe46
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 108 deletions.
56 changes: 30 additions & 26 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,36 +140,41 @@ func New(options *Options) *API {
)
r.Get("/", api.provisionerDaemons)
})
r.Route("/organizations/{organization}", func(r chi.Router) {
r.Route("/organizations", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(options.Database),
authRolesMiddleware,
)
r.Get("/", api.organization)
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
r.Post("/", api.postTemplateByOrganization)
r.Get("/", api.templatesByOrganization)
r.Get("/{templatename}", api.templateByOrganizationAndName)
})
r.Route("/workspaces", func(r chi.Router) {
r.Post("/", api.postWorkspacesByOrganization)
r.Get("/", api.workspacesByOrganization)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
r.Get("/", api.workspacesByOwner)
r.Post("/", api.postOrganizations)
r.Route("/{organization}", func(r chi.Router) {
r.Use(
httpmw.ExtractOrganizationParam(options.Database),
)
r.Get("/", api.organization)
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
r.Route("/templates", func(r chi.Router) {
r.Post("/", api.postTemplateByOrganization)
r.Get("/", api.templatesByOrganization)
r.Get("/{templatename}", api.templateByOrganizationAndName)
})
})
r.Route("/members", func(r chi.Router) {
r.Get("/roles", api.assignableOrgRoles)
r.Route("/{user}", func(r chi.Router) {
r.Use(
httpmw.ExtractUserParam(options.Database),
httpmw.ExtractOrganizationMemberParam(options.Database),
)
r.Put("/roles", api.putMemberRoles)
r.Route("/workspaces", func(r chi.Router) {
r.Post("/", api.postWorkspacesByOrganization)
r.Get("/", api.workspacesByOrganization)
r.Route("/{user}", func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
r.Get("/{workspacename}", api.workspaceByOwnerAndName)
r.Get("/", api.workspacesByOwner)
})
})
r.Route("/members", func(r chi.Router) {
r.Get("/roles", api.assignableOrgRoles)
r.Route("/{user}", func(r chi.Router) {
r.Use(
httpmw.ExtractUserParam(options.Database),
httpmw.ExtractOrganizationMemberParam(options.Database),
)
r.Put("/roles", api.putMemberRoles)
})
})
})
})
Expand Down Expand Up @@ -252,7 +257,6 @@ func New(options *Options) *API {

r.Post("/keys", api.postAPIKey)
r.Route("/organizations", func(r chi.Router) {
r.Post("/", api.postOrganizationsByUser)
r.Get("/", api.organizationsByUser)
r.Get("/{organizationname}", api.organizationByUserAndName)
})
Expand Down
20 changes: 14 additions & 6 deletions coderd/coderd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,6 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
"GET:/api/v2/workspaceagents/{workspaceagent}/pty": {NoAuthorize: true},
"GET:/api/v2/workspaceagents/{workspaceagent}/turn": {NoAuthorize: true},

// TODO: @emyrk these need to be fixed by adding authorize calls
"POST:/api/v2/organizations/{organization}/workspaces": {NoAuthorize: true},
"POST:/api/v2/users/{user}/organizations": {NoAuthorize: true},
"GET:/api/v2/workspaces/{workspace}/watch": {NoAuthorize: true},
"POST:/api/v2/organizations/{organization}/templateversions": {NoAuthorize: true},

// These endpoints have more assertions. This is good, add more endpoints to assert if you can!
"GET:/api/v2/organizations/{organization}": {AssertObject: rbac.ResourceOrganization.InOrg(admin.OrganizationID)},
"GET:/api/v2/users/{user}/organizations": {StatusCode: http.StatusOK, AssertObject: rbac.ResourceOrganization},
Expand Down Expand Up @@ -286,11 +280,25 @@ func TestAuthorizeAllEndpoints(t *testing.T) {
AssertAction: rbac.ActionRead,
AssertObject: rbac.ResourceTemplate.InOrg(template.OrganizationID).WithID(template.ID.String()),
},
"POST:/api/v2/organizations/{organization}/workspaces": {
AssertAction: rbac.ActionCreate,
// No ID when creating
AssertObject: workspaceRBACObj.WithID(""),
},
"GET:/api/v2/workspaces/{workspace}/watch": {
AssertAction: rbac.ActionRead,
AssertObject: workspaceRBACObj,
},
"POST:/api/v2/users/{user}/organizations/": {
AssertAction: rbac.ActionCreate,
AssertObject: rbac.ResourceOrganization,
},

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

for k, v := range assertRoute {
Expand Down
4 changes: 4 additions & 0 deletions coderd/database/modelmethods.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ func (o Organization) RBACObject() rbac.Object {
func (d ProvisionerDaemon) RBACObject() rbac.Object {
return rbac.ResourceProvisionerDaemon.WithID(d.ID.String())
}

func (f File) RBACObject() rbac.Object {
return rbac.ResourceFile.WithID(f.Hash).WithOwner(f.CreatedBy.String())
}
71 changes: 71 additions & 0 deletions coderd/organizations.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package coderd

import (
"database/sql"
"errors"
"fmt"
"net/http"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
Expand All @@ -22,6 +28,71 @@ func (api *API) organization(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
}

func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) {
apiKey := httpmw.APIKey(r)
// Create organization uses the organization resource without an OrgID.
// This means you need the site wide permission to make a new organization.
if !api.Authorize(rw, r, rbac.ActionCreate,
rbac.ResourceOrganization) {
return
}

var req codersdk.CreateOrganizationRequest
if !httpapi.Read(rw, r, &req) {
return
}

_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "organization already exists with that name",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization: %s", err.Error()),
})
return
}

var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: req.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: apiKey.UserID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{
// Also assign member role incase they get demoted from admin
rbac.RoleOrgMember(organization.ID),
rbac.RoleOrgAdmin(organization.ID),
},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}

httpapi.Write(rw, http.StatusCreated, convertOrganization(organization))
}

// convertOrganization consumes the database representation and outputs an API friendly representation.
func convertOrganization(organization database.Organization) codersdk.Organization {
return codersdk.Organization{
Expand Down
6 changes: 3 additions & 3 deletions coderd/organizations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func TestOrganizationByUserAndName(t *testing.T) {
client := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, client)
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
org, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
org, err := client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
Name: "another",
})
require.NoError(t, err)
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
org, err := client.Organization(context.Background(), user.OrganizationID)
require.NoError(t, err)
_, err = client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
_, err = client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
Name: org.Name,
})
var apiErr *codersdk.Error
Expand All @@ -79,7 +79,7 @@ func TestPostOrganizationsByUser(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
_, err := client.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
_, err := client.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
Name: "new",
})
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion coderd/roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestListRoles(t *testing.T) {
member := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
orgAdmin := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID, rbac.RoleOrgAdmin(admin.OrganizationID))

otherOrg, err := client.CreateOrganization(ctx, admin.UserID.String(), codersdk.CreateOrganizationRequest{
otherOrg, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{
Name: "other",
})
require.NoError(t, err, "create org")
Expand Down
10 changes: 10 additions & 0 deletions coderd/templateversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
if !httpapi.Read(rw, r, &req) {
return
}

if req.TemplateID != uuid.Nil {
_, err := api.Database.GetTemplateByID(r.Context(), req.TemplateID)
if errors.Is(err, sql.ErrNoRows) {
Expand Down Expand Up @@ -340,6 +341,15 @@ func (api *API) postTemplateVersionsByOrganization(rw http.ResponseWriter, r *ht
return
}

// Making a new template version is the same permission as creating a new template.
if !api.Authorize(rw, r, rbac.ActionCreate, rbac.ResourceTemplate.InOrg(organization.ID)) {
return
}

if !api.Authorize(rw, r, rbac.ActionRead, file) {
return
}

var templateVersion database.TemplateVersion
var provisionerJob database.ProvisionerJob
err = api.Database.InTx(func(db database.Store) error {
Expand Down
65 changes: 0 additions & 65 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,71 +539,6 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
httpapi.Write(rw, http.StatusOK, convertOrganization(organization))
}

func (api *API) postOrganizationsByUser(rw http.ResponseWriter, r *http.Request) {
user := httpmw.UserParam(r)
var req codersdk.CreateOrganizationRequest
if !httpapi.Read(rw, r, &req) {
return
}

// Create organization uses the organization resource without an OrgID.
// This means you need the site wide permission to make a new organization.
if !api.Authorize(rw, r, rbac.ActionCreate,
rbac.ResourceOrganization) {
return
}

_, err := api.Database.GetOrganizationByName(r.Context(), req.Name)
if err == nil {
httpapi.Write(rw, http.StatusConflict, httpapi.Response{
Message: "organization already exists with that name",
})
return
}
if !errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get organization: %s", err.Error()),
})
return
}

var organization database.Organization
err = api.Database.InTx(func(db database.Store) error {
organization, err = api.Database.InsertOrganization(r.Context(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: req.Name,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
})
if err != nil {
return xerrors.Errorf("create organization: %w", err)
}
_, err = api.Database.InsertOrganizationMember(r.Context(), database.InsertOrganizationMemberParams{
OrganizationID: organization.ID,
UserID: user.ID,
CreatedAt: database.Now(),
UpdatedAt: database.Now(),
Roles: []string{
// Also assign member role incase they get demoted from admin
rbac.RoleOrgMember(organization.ID),
rbac.RoleOrgAdmin(organization.ID),
},
})
if err != nil {
return xerrors.Errorf("create organization member: %w", err)
}
return nil
})
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: err.Error(),
})
return
}

httpapi.Write(rw, http.StatusCreated, convertOrganization(organization))
}

// Authenticates the user with an email and password.
func (api *API) postLogin(rw http.ResponseWriter, r *http.Request) {
var loginWithPassword codersdk.LoginWithPasswordRequest
Expand Down
2 changes: 1 addition & 1 deletion coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ func TestPostUsers(t *testing.T) {
first := coderdtest.CreateFirstUser(t, client)
notInOrg := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
other := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleAdmin(), rbac.RoleMember())
org, err := other.CreateOrganization(context.Background(), codersdk.Me, codersdk.CreateOrganizationRequest{
org, err := other.CreateOrganization(context.Background(), codersdk.CreateOrganizationRequest{
Name: "another",
})
require.NoError(t, err)
Expand Down
14 changes: 12 additions & 2 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,18 @@ func (api *API) workspaceByOwnerAndName(rw http.ResponseWriter, r *http.Request)

// Create a new workspace for the currently authenticated user.
func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Request) {
organization := httpmw.OrganizationParam(r)
apiKey := httpmw.APIKey(r)
if !api.Authorize(rw, r, rbac.ActionCreate,
rbac.ResourceWorkspace.InOrg(organization.ID).WithOwner(apiKey.UserID.String())) {
return
}

var createWorkspace codersdk.CreateWorkspaceRequest
if !httpapi.Read(rw, r, &createWorkspace) {
return
}
apiKey := httpmw.APIKey(r)

template, err := api.Database.GetTemplateByID(r.Context(), createWorkspace.TemplateID)
if errors.Is(err, sql.ErrNoRows) {
httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{
Expand All @@ -303,7 +310,7 @@ func (api *API) postWorkspacesByOrganization(rw http.ResponseWriter, r *http.Req
})
return
}
organization := httpmw.OrganizationParam(r)

if organization.ID != template.OrganizationID {
httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{
Message: fmt.Sprintf("template is not in organization %q", organization.Name),
Expand Down Expand Up @@ -636,6 +643,9 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {

func (api *API) watchWorkspace(rw http.ResponseWriter, r *http.Request) {
workspace := httpmw.WorkspaceParam(r)
if !api.Authorize(rw, r, rbac.ActionRead, workspace) {
return
}

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

0 comments on commit ec1fe46

Please sign in to comment.