From 7d40b8b2f830bedb2b0baf138845c21c39d6b321 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 15 May 2024 20:02:08 +0000 Subject: [PATCH 01/23] patchOrganization --- coderd/database/dbauthz/dbauthz.go | 4 ++ coderd/database/dbmem/dbmem.go | 9 ++++ coderd/database/dbmetrics/dbmetrics.go | 7 +++ coderd/database/dbmock/dbmock.go | 15 ++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 31 ++++++++++++ coderd/database/queries/organizations.sql | 11 +++++ coderd/organizations.go | 58 +++++++++++++++++++++++ codersdk/users.go | 4 ++ 9 files changed, 140 insertions(+) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index aaf623c7a70b5..2fc09d9f72686 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2777,6 +2777,10 @@ func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg dat return q.db.UpdateOAuth2ProviderAppSecretByID(ctx, arg) } +func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { + panic("not implemented") +} + func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { if err := q.authorizeContext(ctx, rbac.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil { return err diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 8a2ce25b34367..716f105d012d8 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7143,6 +7143,15 @@ func (q *FakeQuerier) UpdateOAuth2ProviderAppSecretByID(_ context.Context, arg d return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows } +func (q *FakeQuerier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { + err := validateDatabaseType(arg) + if err != nil { + return database.Organization{}, err + } + + panic("not implemented") +} + func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { err := validateDatabaseType(arg) if err != nil { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index d92c60e8db09a..591033d47eb3f 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -1822,6 +1822,13 @@ func (m metricsStore) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg return r0, r1 } +func (m metricsStore) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { + start := time.Now() + r0, r1 := m.s.UpdateOrganization(ctx, arg) + m.queryLatencies.WithLabelValues("UpdateOrganization").Observe(time.Since(start).Seconds()) + return r0, r1 +} + func (m metricsStore) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { start := time.Now() r0 := m.s.UpdateProvisionerDaemonLastSeenAt(ctx, arg) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index e651c8301c933..8c80a2ccefb39 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -3836,6 +3836,21 @@ func (mr *MockStoreMockRecorder) UpdateOAuth2ProviderAppSecretByID(arg0, arg1 an return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOAuth2ProviderAppSecretByID", reflect.TypeOf((*MockStore)(nil).UpdateOAuth2ProviderAppSecretByID), arg0, arg1) } +// UpdateOrganization mocks base method. +func (m *MockStore) UpdateOrganization(arg0 context.Context, arg1 database.UpdateOrganizationParams) (database.Organization, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateOrganization", arg0, arg1) + ret0, _ := ret[0].(database.Organization) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateOrganization indicates an expected call of UpdateOrganization. +func (mr *MockStoreMockRecorder) UpdateOrganization(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOrganization", reflect.TypeOf((*MockStore)(nil).UpdateOrganization), arg0, arg1) +} + // UpdateProvisionerDaemonLastSeenAt mocks base method. func (m *MockStore) UpdateProvisionerDaemonLastSeenAt(arg0 context.Context, arg1 database.UpdateProvisionerDaemonLastSeenAtParams) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 405f86bf47688..c277f4f67e9e4 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -365,6 +365,7 @@ type sqlcQuerier interface { UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error) UpdateOAuth2ProviderAppByID(ctx context.Context, arg UpdateOAuth2ProviderAppByIDParams) (OAuth2ProviderApp, error) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg UpdateOAuth2ProviderAppSecretByIDParams) (OAuth2ProviderAppSecret, error) + UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg UpdateProvisionerDaemonLastSeenAtParams) error UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error UpdateProvisionerJobWithCancelByID(ctx context.Context, arg UpdateProvisionerJobWithCancelByIDParams) error diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index e0fba2dad35bd..93e05d8456777 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4092,6 +4092,37 @@ func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizat return i, err } +const updateOrganization = `-- name: UpdateOrganization :one +UPDATE + organizations +SET + updated_at = $2, + name = $3 +WHERE + id = $1 +RETURNING id, name, description, created_at, updated_at, is_default +` + +type UpdateOrganizationParams struct { + ID uuid.UUID `db:"id" json:"id"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` +} + +func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) { + row := q.db.QueryRowContext(ctx, updateOrganization, arg.ID, arg.UpdatedAt, arg.Name) + var i Organization + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.CreatedAt, + &i.UpdatedAt, + &i.IsDefault, + ) + return i, err +} + const getParameterSchemasByJobID = `-- name: GetParameterSchemasByJobID :many SELECT id, created_at, job_id, name, description, default_source_scheme, default_source_value, allow_override_source, default_destination_scheme, allow_override_destination, default_refresh, redisplay_value, validation_error, validation_condition, validation_type_system, validation_value_type, index diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index e809b386926a3..9b39da128c2b5 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -53,3 +53,14 @@ INSERT INTO VALUES -- If no organizations exist, and this is the first, make it the default. ($1, $2, $3, $4, $5, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING *; + + +-- name: UpdateOrganization :one +UPDATE + organizations +SET + updated_at = $2, + name = $3 +WHERE + id = $1 +RETURNING *; diff --git a/coderd/organizations.go b/coderd/organizations.go index e5098a9697caf..323937a165888 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -118,6 +118,64 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusCreated, convertOrganization(organization)) } +// @Summary Update organization +// @ID update-organization +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Organizations +// @Param request body codersdk.PatchOrganizationRequest true "Patch organization request" +// @Success 201 {object} codersdk.Organization +// @Router /organizations/{organization} [patch] +func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + apiKey := httpmw.APIKey(r) + + var req codersdk.PatchOrganizationRequest + if !httpapi.Read(ctx, rw, r, &req) { + return + } + + if req.Name == codersdk.DefaultOrganization { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return + } + + _, err := api.Database.GetOrganizationByName(ctx, req.Name) + if err == nil { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: "Organization already exists with that name.", + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), + Detail: err.Error(), + }) + return + } + + organization, err := api.Database.InsertOrganization(ctx, database.InsertOrganizationParams{ + ID: uuid.New(), + Name: req.Name, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + Description: "", + }) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error inserting organization member.", + Detail: fmt.Sprintf("update organization: %w", err), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization)) +} + // convertOrganization consumes the database representation and outputs an API friendly representation. func convertOrganization(organization database.Organization) codersdk.Organization { return codersdk.Organization{ diff --git a/codersdk/users.go b/codersdk/users.go index 7eb7604fc57b7..350dd0635545f 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -207,6 +207,10 @@ type CreateOrganizationRequest struct { Name string `json:"name" validate:"required,username"` } +type PatchOrganizationRequest struct { + Name string `json:"name" validate:"required,username"` +} + // AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc. type AuthMethods struct { TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` From 06378e4851828d217d15e85359644c03d244c8eb Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 15 May 2024 22:59:48 +0000 Subject: [PATCH 02/23] dbauthz and dbmem impls --- coderd/database/dbauthz/dbauthz.go | 9 ++++++++- coderd/database/dbmem/dbmem.go | 19 ++++++++++++++++++- coderd/database/dbmetrics/dbmetrics.go | 7 +++++++ coderd/database/dbmock/dbmock.go | 14 ++++++++++++++ coderd/database/querier.go | 1 + coderd/database/queries.sql.go | 13 +++++++++++++ coderd/database/queries/organizations.sql | 8 +++++++- coderd/organizations.go | 1 - coderd/rbac/object.go | 4 ++-- coderd/rbac/roles_test.go | 2 +- 10 files changed, 71 insertions(+), 7 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index fe224c6d111ce..096168c713ab3 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -914,6 +914,10 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { return q.db.DeleteOldWorkspaceAgentStats(ctx) } +func (q *querier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + return deleteQ[database.Organization](q.log, q.auth, q.db.GetOrganizationByID, q.db.DeleteOrganization)(ctx, id) +} + func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { return err @@ -2754,7 +2758,10 @@ func (q *querier) UpdateOAuth2ProviderAppSecretByID(ctx context.Context, arg dat } func (q *querier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { - panic("not implemented") + fetch := func(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { + return q.db.GetOrganizationByID(ctx, arg.ID) + } + return updateWithReturn(q.log, q.auth, fetch, q.db.UpdateOrganization)(ctx, arg) } func (q *querier) UpdateProvisionerDaemonLastSeenAt(ctx context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 716f105d012d8..7850ea0c79ee7 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1573,6 +1573,16 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { return nil } +func (q *FakeQuerier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + for i, org := range q.organizations { + if org.ID == id { + q.organizations = append(q.organizations[:i], q.organizations[i+1:]...) + return nil + } + } + return sql.ErrNoRows +} + func (q *FakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error { q.mutex.Lock() defer q.mutex.Unlock() @@ -7149,7 +7159,14 @@ func (q *FakeQuerier) UpdateOrganization(ctx context.Context, arg database.Updat return database.Organization{}, err } - panic("not implemented") + for i, org := range q.organizations { + if org.ID == arg.ID { + org.Name = arg.Name + q.organizations[i] = org + return org, nil + } + } + return database.Organization{}, sql.ErrNoRows } func (q *FakeQuerier) UpdateProvisionerDaemonLastSeenAt(_ context.Context, arg database.UpdateProvisionerDaemonLastSeenAtParams) error { diff --git a/coderd/database/dbmetrics/dbmetrics.go b/coderd/database/dbmetrics/dbmetrics.go index 2ddc976e78710..c9db99260c1dc 100644 --- a/coderd/database/dbmetrics/dbmetrics.go +++ b/coderd/database/dbmetrics/dbmetrics.go @@ -277,6 +277,13 @@ func (m metricsStore) DeleteOldWorkspaceAgentStats(ctx context.Context) error { return err } +func (m metricsStore) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + start := time.Now() + r0 := m.s.DeleteOrganization(ctx, id) + m.queryLatencies.WithLabelValues("DeleteOrganization").Observe(time.Since(start).Seconds()) + return r0 +} + func (m metricsStore) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { start := time.Now() err := m.s.DeleteReplicasUpdatedBefore(ctx, updatedAt) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 8c80a2ccefb39..8c8c461be3fe9 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -440,6 +440,20 @@ func (mr *MockStoreMockRecorder) DeleteOldWorkspaceAgentStats(arg0 any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOldWorkspaceAgentStats", reflect.TypeOf((*MockStore)(nil).DeleteOldWorkspaceAgentStats), arg0) } +// DeleteOrganization mocks base method. +func (m *MockStore) DeleteOrganization(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOrganization", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOrganization indicates an expected call of DeleteOrganization. +func (mr *MockStoreMockRecorder) DeleteOrganization(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganization", reflect.TypeOf((*MockStore)(nil).DeleteOrganization), arg0, arg1) +} + // DeleteReplicasUpdatedBefore mocks base method. func (m *MockStore) DeleteReplicasUpdatedBefore(arg0 context.Context, arg1 time.Time) error { m.ctrl.T.Helper() diff --git a/coderd/database/querier.go b/coderd/database/querier.go index c277f4f67e9e4..c2e75abf5dc40 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -73,6 +73,7 @@ type sqlcQuerier interface { // Logs can take up a lot of space, so it's important we clean up frequently. DeleteOldWorkspaceAgentLogs(ctx context.Context) error DeleteOldWorkspaceAgentStats(ctx context.Context) error + DeleteOrganization(ctx context.Context, id uuid.UUID) error DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error DeleteTailnetAgent(ctx context.Context, arg DeleteTailnetAgentParams) (DeleteTailnetAgentRow, error) DeleteTailnetClient(ctx context.Context, arg DeleteTailnetClientParams) (DeleteTailnetClientRow, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 93e05d8456777..34bc41bc456df 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3900,6 +3900,19 @@ func (q *sqlQuerier) UpdateMemberRoles(ctx context.Context, arg UpdateMemberRole return i, err } +const deleteOrganization = `-- name: DeleteOrganization :exec +DELETE FROM + organizations +WHERE + id = $1 AND + is_default = false +` + +func (q *sqlQuerier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, deleteOrganization, id) + return err +} + const getDefaultOrganization = `-- name: GetDefaultOrganization :one SELECT id, name, description, created_at, updated_at, is_default diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index 9b39da128c2b5..aaa262b939caa 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -54,7 +54,6 @@ VALUES -- If no organizations exist, and this is the first, make it the default. ($1, $2, $3, $4, $5, (SELECT TRUE FROM organizations LIMIT 1) IS NULL) RETURNING *; - -- name: UpdateOrganization :one UPDATE organizations @@ -64,3 +63,10 @@ SET WHERE id = $1 RETURNING *; + +-- name: DeleteOrganization :exec +DELETE FROM + organizations +WHERE + id = $1 AND + is_default = false; diff --git a/coderd/organizations.go b/coderd/organizations.go index 323937a165888..2a9503f552542 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -129,7 +129,6 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { // @Router /organizations/{organization} [patch] func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - apiKey := httpmw.APIKey(r) var req codersdk.PatchOrganizationRequest if !httpapi.Read(ctx, rw, r, &req) { diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 30a74e4f825dd..91df14e0ee481 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -36,10 +36,10 @@ type Object struct { func (z Object) ValidAction(action policy.Action) error { perms, ok := policy.RBACPermissions[z.Type] if !ok { - return fmt.Errorf("invalid type %q", z.Type) + return xerrors.Errorf("invalid type %q", z.Type) } if _, ok := perms.Actions[action]; !ok { - return fmt.Errorf("invalid action %q for type %q", action, z.Type) + return xerrors.Errorf("invalid action %q for type %q", action, z.Type) } return nil diff --git a/coderd/rbac/roles_test.go b/coderd/rbac/roles_test.go index 44ef83b74cd20..28bcdeb35e447 100644 --- a/coderd/rbac/roles_test.go +++ b/coderd/rbac/roles_test.go @@ -359,7 +359,7 @@ func TestRolePermissions(t *testing.T) { }, // Some admin style resources { - Name: "Licences", + Name: "Licenses", Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete}, Resource: rbac.ResourceLicense, AuthorizeMap: map[bool][]authSubject{ From 10224ad55b78c34e94605e57513cfb98ca590b05 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 15 May 2024 23:07:00 +0000 Subject: [PATCH 03/23] gen --- coderd/apidoc/docs.go | 48 ++++++++++++++++++++++++++++++++ coderd/apidoc/swagger.json | 40 +++++++++++++++++++++++++++ coderd/organizations.go | 10 +++---- coderd/rbac/object.go | 3 +- docs/api/organizations.md | 50 ++++++++++++++++++++++++++++++++++ docs/api/schemas.md | 14 ++++++++++ site/src/api/typesGenerated.ts | 5 ++++ 7 files changed, 162 insertions(+), 8 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0a22d84d13642..341881ae3042d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1987,6 +1987,43 @@ const docTemplate = `{ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Organizations" + ], + "summary": "Update organization", + "operationId": "update-organization", + "parameters": [ + { + "description": "Patch organization request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/codersdk.Organization" + } + } + } } }, "/organizations/{organization}/groups": { @@ -10414,6 +10451,17 @@ const docTemplate = `{ } } }, + "codersdk.PatchOrganizationRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 331b1512393f7..74858f6fff941 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1732,6 +1732,37 @@ } } } + }, + "patch": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "tags": ["Organizations"], + "summary": "Update organization", + "operationId": "update-organization", + "parameters": [ + { + "description": "Patch organization request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/codersdk.PatchOrganizationRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/codersdk.Organization" + } + } + } } }, "/organizations/{organization}/groups": { @@ -9358,6 +9389,15 @@ } } }, + "codersdk.PatchOrganizationRequest": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { diff --git a/coderd/organizations.go b/coderd/organizations.go index 2a9503f552542..260747f000862 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -157,12 +157,10 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { return } - organization, err := api.Database.InsertOrganization(ctx, database.InsertOrganizationParams{ - ID: uuid.New(), - Name: req.Name, - CreatedAt: dbtime.Now(), - UpdatedAt: dbtime.Now(), - Description: "", + organization, err := api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ + ID: uuid.New(), + Name: req.Name, + UpdatedAt: dbtime.Now(), }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ diff --git a/coderd/rbac/object.go b/coderd/rbac/object.go index 91df14e0ee481..dfd8ab6b55b23 100644 --- a/coderd/rbac/object.go +++ b/coderd/rbac/object.go @@ -1,9 +1,8 @@ package rbac import ( - "fmt" - "github.com/google/uuid" + "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/rbac/policy" ) diff --git a/docs/api/organizations.md b/docs/api/organizations.md index 478c8aba56648..f87baab4956ed 100644 --- a/docs/api/organizations.md +++ b/docs/api/organizations.md @@ -177,3 +177,53 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization} \ | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Organization](schemas.md#codersdkorganization) | To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Update organization + +### Code samples + +```shell +# Example request using curl +curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` + +`PATCH /organizations/{organization}` + +> Body parameter + +```json +{ + "name": "string" +} +``` + +### Parameters + +| Name | In | Type | Required | Description | +| ------ | ---- | -------------------------------------------------------------------------------- | -------- | -------------------------- | +| `body` | body | [codersdk.PatchOrganizationRequest](schemas.md#codersdkpatchorganizationrequest) | true | Patch organization request | + +### Example responses + +> 201 Response + +```json +{ + "created_at": "2019-08-24T14:15:22Z", + "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", + "is_default": true, + "name": "string", + "updated_at": "2019-08-24T14:15:22Z" +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------------ | ----------- | -------------------------------------------------------- | +| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Organization](schemas.md#codersdkorganization) | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 42f8f43517233..a22f9eef74320 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3611,6 +3611,20 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `quota_allowance` | integer | false | | | | `remove_users` | array of string | false | | | +## codersdk.PatchOrganizationRequest + +```json +{ + "name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ----------- | +| `name` | string | true | | | + ## codersdk.PatchTemplateVersionRequest ```json diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 9331339ed1aa1..7bc17b1be7edc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -806,6 +806,11 @@ export interface PatchGroupRequest { readonly quota_allowance?: number; } +// From codersdk/users.go +export interface PatchOrganizationRequest { + readonly name: string; +} + // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: string; From 265a895c1c36c09049e5f300802c9940817145f7 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 16:41:54 +0000 Subject: [PATCH 04/23] =?UTF-8?q?=F0=9F=AA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/coderd.go | 2 ++ coderd/database/dbmem/dbmem.go | 4 +-- coderd/organizations.go | 48 +++++++++++++++++++++++++++++----- scripts/rbacgen/main.go | 8 +++--- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index c0631c0752c0c..e1256ba24ebfb 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -810,6 +810,8 @@ func New(options *Options) *API { httpmw.ExtractOrganizationParam(options.Database), ) r.Get("/", api.organization) + r.Patch("/", api.patchOrganization) + r.Delete("/", api.deleteOrganization) r.Post("/templateversions", api.postTemplateVersionsByOrganization) r.Route("/templates", func(r chi.Router) { r.Post("/", api.postTemplateByOrganization) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7850ea0c79ee7..abae80f4db6c5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1573,7 +1573,7 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { return nil } -func (q *FakeQuerier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { +func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error { for i, org := range q.organizations { if org.ID == id { q.organizations = append(q.organizations[:i], q.organizations[i+1:]...) @@ -7153,7 +7153,7 @@ func (q *FakeQuerier) UpdateOAuth2ProviderAppSecretByID(_ context.Context, arg d return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows } -func (q *FakeQuerier) UpdateOrganization(ctx context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { +func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) { err := validateDatabaseType(arg) if err != nil { return database.Organization{}, err diff --git a/coderd/organizations.go b/coderd/organizations.go index 260747f000862..65189418350b1 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -125,17 +125,18 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Organizations // @Param request body codersdk.PatchOrganizationRequest true "Patch organization request" -// @Success 201 {object} codersdk.Organization +// @Success 200 {object} codersdk.Organization // @Router /organizations/{organization} [patch] func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() + organization := httpmw.OrganizationParam(r) var req codersdk.PatchOrganizationRequest if !httpapi.Read(ctx, rw, r, &req) { return } - if req.Name == codersdk.DefaultOrganization { + if req.Name == codersdk.DefaultOrganization && !organization.IsDefault { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), }) @@ -157,15 +158,15 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { return } - organization, err := api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ - ID: uuid.New(), - Name: req.Name, + organization, err = api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ + ID: organization.ID, UpdatedAt: dbtime.Now(), + Name: req.Name, }) if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error inserting organization member.", - Detail: fmt.Sprintf("update organization: %w", err), + Message: "Internal error updating organization.", + Detail: fmt.Sprintf("update organization: %s", err.Error()), }) return } @@ -173,6 +174,39 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, convertOrganization(organization)) } +// @Summary Delete organization +// @ID delete-organization +// @Security CoderSessionToken +// @Accept json +// @Produce json +// @Tags Organizations +// @Success 200 +// @Router /organizations/{organization} [delete] +func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + organization := httpmw.OrganizationParam(r) + + if organization.IsDefault { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return + } + + err := api.Database.DeleteOrganization(ctx, organization.ID) + if err != nil { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: "Internal error deleting organization.", + Detail: fmt.Sprintf("delete organization: %s", err.Error()), + }) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.Response{ + Message: "Organization has been deleted.", + }) +} + // convertOrganization consumes the database representation and outputs an API friendly representation. func convertOrganization(organization database.Organization) codersdk.Organization { return codersdk.Organization{ diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 38f13434c77e4..67b35ae57c037 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -148,7 +148,7 @@ func generateRbacObjects(templateSource string) ([]byte, error) { // Parse the policy.go file for the action enums f, err := parser.ParseFile(token.NewFileSet(), "./coderd/rbac/policy/policy.go", nil, parser.ParseComments) if err != nil { - return nil, fmt.Errorf("parsing policy.go: %w", err) + return nil, xerrors.Errorf("parsing policy.go: %w", err) } actionMap := fileActions(f) actionList := make([]ActionDetails, 0) @@ -176,14 +176,14 @@ func generateRbacObjects(templateSource string) ([]byte, error) { x++ v, ok := actionMap[string(action)] if !ok { - errorList = append(errorList, fmt.Errorf("action value %q does not have a constant a matching enum constant", action)) + errorList = append(errorList, xerrors.Errorf("action value %q does not have a constant a matching enum constant", action)) } return v }, "concat": func(strs ...string) string { return strings.Join(strs, "") }, }).Parse(templateSource) if err != nil { - return nil, fmt.Errorf("parse template: %w", err) + return nil, xerrors.Errorf("parse template: %w", err) } // Convert to sorted list for autogen consistency. @@ -203,7 +203,7 @@ func generateRbacObjects(templateSource string) ([]byte, error) { err = tpl.Execute(&out, list) if err != nil { - return nil, fmt.Errorf("execute template: %w", err) + return nil, xerrors.Errorf("execute template: %w", err) } if len(errorList) > 0 { From dea7c0a40966b2d22a516ab8b2558ef499a6288d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 16:48:37 +0000 Subject: [PATCH 05/23] fix message --- coderd/organizations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index 65189418350b1..0baaf3bd4ecaf 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -188,7 +188,7 @@ func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { if organization.IsDefault { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + Message: "Default organization cannot be deleted.", }) return } From f641a49dc530aba6f54ecb19f496ea32f9d9e391 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 16:49:14 +0000 Subject: [PATCH 06/23] no body --- coderd/organizations.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index 0baaf3bd4ecaf..5c7b50978aff6 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -177,7 +177,6 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { // @Summary Delete organization // @ID delete-organization // @Security CoderSessionToken -// @Accept json // @Produce json // @Tags Organizations // @Success 200 From 0ea9a126feee75b5e6c380884831a9c7bd300359 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 16:52:32 +0000 Subject: [PATCH 07/23] it's fine now I guess --- coderd/database/dbauthz/dbauthz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 096168c713ab3..ff73dc760d481 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -915,7 +915,7 @@ func (q *querier) DeleteOldWorkspaceAgentStats(ctx context.Context) error { } func (q *querier) DeleteOrganization(ctx context.Context, id uuid.UUID) error { - return deleteQ[database.Organization](q.log, q.auth, q.db.GetOrganizationByID, q.db.DeleteOrganization)(ctx, id) + return deleteQ(q.log, q.auth, q.db.GetOrganizationByID, q.db.DeleteOrganization)(ctx, id) } func (q *querier) DeleteReplicasUpdatedBefore(ctx context.Context, updatedAt time.Time) error { From e6a0ec02a10b04e3cd92afe61f51baf04e54ae55 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 17:41:12 +0000 Subject: [PATCH 08/23] codersdk client functions, tests --- coderd/database/dbauthz/dbauthz_test.go | 25 ++++++++- coderd/organizations.go | 4 +- coderd/organizations_test.go | 67 +++++++++++++++++++++++++ codersdk/organizations.go | 57 +++++++++++++++++++++ codersdk/users.go | 24 --------- 5 files changed, 150 insertions(+), 27 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index e8dcb2f8ee5bc..2367fbf698bfc 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -624,7 +624,7 @@ func (s *MethodTestSuite) TestOrganization() { s.Run("InsertOrganization", s.Subtest(func(db database.Store, check *expects) { check.Args(database.InsertOrganizationParams{ ID: uuid.New(), - Name: "random", + Name: "new-org", }).Asserts(rbac.ResourceOrganization, policy.ActionCreate) })) s.Run("InsertOrganizationMember", s.Subtest(func(db database.Store, check *expects) { @@ -639,6 +639,29 @@ func (s *MethodTestSuite) TestOrganization() { rbac.ResourceAssignRole.InOrg(o.ID), policy.ActionAssign, rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate) })) + s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) { + ctx := testutil.Context(s.T(), testutil.WaitShort) + o, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{ + ID: uuid.New(), + Name: "something-unique", + }) + require.NoError(s.T(), err) + check.Args(database.UpdateOrganizationParams{ + ID: o.ID, + Name: "something-different", + }).Asserts(rbac.ResourceOrganization, policy.ActionUpdate) + })) + s.Run("DeleteOrganization", s.Subtest(func(db database.Store, check *expects) { + ctx := testutil.Context(s.T(), testutil.WaitShort) + o, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{ + ID: uuid.New(), + Name: "doomed", + }) + require.NoError(s.T(), err) + check.Args( + o.ID, + ).Asserts(rbac.ResourceOrganization, policy.ActionDelete) + })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) u := dbgen.User(s.T(), db, database.User{}) diff --git a/coderd/organizations.go b/coderd/organizations.go index 5c7b50978aff6..8256987da2b7f 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -124,14 +124,14 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { // @Accept json // @Produce json // @Tags Organizations -// @Param request body codersdk.PatchOrganizationRequest true "Patch organization request" +// @Param request body codersdk.UpdateOrganizationRequest true "Patch organization request" // @Success 200 {object} codersdk.Organization // @Router /organizations/{organization} [patch] func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() organization := httpmw.OrganizationParam(r) - var req codersdk.PatchOrganizationRequest + var req codersdk.UpdateOrganizationRequest if !httpapi.Read(ctx, rw, r, &req) { return } diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index e176c7a6d858c..c23862ae7c937 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -140,3 +140,70 @@ func TestPostOrganizationsByUser(t *testing.T) { require.NoError(t, err) }) } + +func TestPatchOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Conflict", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + originalOrg, err := client.Organization(ctx, user.OrganizationID) + require.NoError(t, err) + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "something-unique", + }) + require.NoError(t, err) + + _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: originalOrg.Name, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusConflict, apiErr.StatusCode()) + }) + + t.Run("ReservedName", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "something-unique", + }) + require.NoError(t, err) + + _, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: codersdk.DefaultOrganization, + }) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("Update", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "new", + }) + require.NoError(t, err) + + o, err = client.UpdateOrganization(ctx, o.Name, codersdk.UpdateOrganizationRequest{ + Name: "new-new", + }) + require.NoError(t, err) + require.Equal(t, "new-new", o.Name) + }) +} diff --git a/codersdk/organizations.go b/codersdk/organizations.go index 441f4774f2441..f26d17dce749f 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -55,6 +55,14 @@ type OrganizationMember struct { Roles []Role `db:"roles" json:"roles"` } +type CreateOrganizationRequest struct { + Name string `json:"name" validate:"required,username"` +} + +type UpdateOrganizationRequest struct { + Name string `json:"name" validate:"required,username"` +} + // CreateTemplateVersionRequest enables callers to create a new Template Version. type CreateTemplateVersionRequest struct { Name string `json:"name,omitempty" validate:"omitempty,template_version_name"` @@ -187,6 +195,55 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, return c.OrganizationByName(ctx, id.String()) } +// CreateOrganization creates an organization and adds the provided user as an admin. +func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (Organization, error) { + res, err := c.Request(ctx, http.MethodPost, "/api/v2/organizations", req) + if err != nil { + return Organization{}, err + } + defer res.Body.Close() + + if res.StatusCode != http.StatusCreated { + return Organization{}, ReadBodyAsError(res) + } + + var org Organization + return org, json.NewDecoder(res.Body).Decode(&org) +} + +// UpdateOrganization will update information about the corresponding organization, based on +// the UUID/name provided as `orgID`. +func (c *Client) UpdateOrganization(ctx context.Context, orgID string, req UpdateOrganizationRequest) (Organization, error) { + res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/organizations/%s", orgID), req) + if err != nil { + return Organization{}, xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return Organization{}, ReadBodyAsError(res) + } + + var organization Organization + return organization, json.NewDecoder(res.Body).Decode(&organization) +} + +// DeleteOrganization will remove the corresponding organization from the deployment, based on +// the UUID/name provided as `orgID`. +func (c *Client) DeleteOrganization(ctx context.Context, orgID string) error { + res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s", orgID), nil) + if err != nil { + return xerrors.Errorf("execute request: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return ReadBodyAsError(res) + } + + return nil +} + // ProvisionerDaemons returns provisioner daemons available. func (c *Client) ProvisionerDaemons(ctx context.Context) ([]ProvisionerDaemon, error) { res, err := c.Request(ctx, http.MethodGet, diff --git a/codersdk/users.go b/codersdk/users.go index 350dd0635545f..863668bf983be 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -203,14 +203,6 @@ type OAuthConversionResponse struct { UserID uuid.UUID `json:"user_id" format:"uuid"` } -type CreateOrganizationRequest struct { - Name string `json:"name" validate:"required,username"` -} - -type PatchOrganizationRequest struct { - Name string `json:"name" validate:"required,username"` -} - // AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc. type AuthMethods struct { TermsOfServiceURL string `json:"terms_of_service_url,omitempty"` @@ -591,22 +583,6 @@ func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, nam return org, json.NewDecoder(res.Body).Decode(&org) } -// CreateOrganization creates an organization and adds the provided user as an admin. -func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (Organization, error) { - res, err := c.Request(ctx, http.MethodPost, "/api/v2/organizations", req) - if err != nil { - return Organization{}, err - } - defer res.Body.Close() - - if res.StatusCode != http.StatusCreated { - return Organization{}, ReadBodyAsError(res) - } - - var org Organization - return org, json.NewDecoder(res.Body).Decode(&org) -} - // AuthMethods returns types of authentication available to the user. func (c *Client) AuthMethods(ctx context.Context) (AuthMethods, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/authmethods", nil) From 72ebaee2cdc840326e6fe671f5a332715904babf Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:10:30 +0000 Subject: [PATCH 09/23] something --- .vscode/settings.json | 1 - coderd/apidoc/docs.go | 48 ++++++++++++++++++++++++---------- coderd/apidoc/swagger.json | 40 +++++++++++++++++++--------- codersdk/organizations.go | 2 +- docs/api/organizations.md | 34 +++++++++++++++++++----- docs/api/schemas.md | 28 ++++++++++---------- scripts/rbacgen/main.go | 2 ++ site/src/api/typesGenerated.ts | 12 ++++----- 8 files changed, 112 insertions(+), 55 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c95554245cab5..c824ea4edb783 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -195,7 +195,6 @@ "**.pb.go": true, "**/*.gen.json": true, "**/testdata/*": true, - "**Generated.ts": true, "coderd/apidoc/**": true, "docs/api/*.md": true, "docs/templates/*.md": true, diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 341881ae3042d..7a2b1108d85ed 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -1988,6 +1988,26 @@ const docTemplate = `{ } } }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Organizations" + ], + "summary": "Delete organization", + "operationId": "delete-organization", + "responses": { + "200": { + "description": "OK" + } + } + }, "patch": { "security": [ { @@ -2012,13 +2032,13 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PatchOrganizationRequest" + "$ref": "#/definitions/codersdk.UpdateOrganizationRequest" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { "$ref": "#/definitions/codersdk.Organization" } @@ -10451,17 +10471,6 @@ const docTemplate = `{ } } }, - "codersdk.PatchOrganizationRequest": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { @@ -12040,6 +12049,17 @@ const docTemplate = `{ } } }, + "codersdk.UpdateOrganizationRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.UpdateRoles": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 74858f6fff941..f04038ed3c6d3 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1733,6 +1733,22 @@ } } }, + "delete": { + "security": [ + { + "CoderSessionToken": [] + } + ], + "produces": ["application/json"], + "tags": ["Organizations"], + "summary": "Delete organization", + "operationId": "delete-organization", + "responses": { + "200": { + "description": "OK" + } + } + }, "patch": { "security": [ { @@ -1751,13 +1767,13 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/codersdk.PatchOrganizationRequest" + "$ref": "#/definitions/codersdk.UpdateOrganizationRequest" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { "$ref": "#/definitions/codersdk.Organization" } @@ -9389,15 +9405,6 @@ } } }, - "codersdk.PatchOrganizationRequest": { - "type": "object", - "required": ["name"], - "properties": { - "name": { - "type": "string" - } - } - }, "codersdk.PatchTemplateVersionRequest": { "type": "object", "properties": { @@ -10891,6 +10898,15 @@ } } }, + "codersdk.UpdateOrganizationRequest": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string" + } + } + }, "codersdk.UpdateRoles": { "type": "object", "properties": { diff --git a/codersdk/organizations.go b/codersdk/organizations.go index f26d17dce749f..05a8aad2da853 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -195,7 +195,7 @@ func (c *Client) Organization(ctx context.Context, id uuid.UUID) (Organization, return c.OrganizationByName(ctx, id.String()) } -// CreateOrganization creates an organization and adds the provided user as an admin. +// CreateOrganization creates an organization and adds the user making the request as an owner. func (c *Client) CreateOrganization(ctx context.Context, req CreateOrganizationRequest) (Organization, error) { res, err := c.Request(ctx, http.MethodPost, "/api/v2/organizations", req) if err != nil { diff --git a/docs/api/organizations.md b/docs/api/organizations.md index f87baab4956ed..dc5e2e9c72ccd 100644 --- a/docs/api/organizations.md +++ b/docs/api/organizations.md @@ -178,6 +178,26 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization} \ To perform this operation, you must be authenticated. [Learn more](authentication.md). +## Delete organization + +### Code samples + +```shell +# Example request using curl +curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \ + -H 'Coder-Session-Token: API_KEY' +``` + +`DELETE /organizations/{organization}` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + ## Update organization ### Code samples @@ -202,13 +222,13 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \ ### Parameters -| Name | In | Type | Required | Description | -| ------ | ---- | -------------------------------------------------------------------------------- | -------- | -------------------------- | -| `body` | body | [codersdk.PatchOrganizationRequest](schemas.md#codersdkpatchorganizationrequest) | true | Patch organization request | +| Name | In | Type | Required | Description | +| ------ | ---- | ---------------------------------------------------------------------------------- | -------- | -------------------------- | +| `body` | body | [codersdk.UpdateOrganizationRequest](schemas.md#codersdkupdateorganizationrequest) | true | Patch organization request | ### Example responses -> 201 Response +> 200 Response ```json { @@ -222,8 +242,8 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \ ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------------ | ----------- | -------------------------------------------------------- | -| 201 | [Created](https://tools.ietf.org/html/rfc7231#section-6.3.2) | Created | [codersdk.Organization](schemas.md#codersdkorganization) | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Organization](schemas.md#codersdkorganization) | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/docs/api/schemas.md b/docs/api/schemas.md index a22f9eef74320..6d728c1d54b95 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -3611,20 +3611,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `quota_allowance` | integer | false | | | | `remove_users` | array of string | false | | | -## codersdk.PatchOrganizationRequest - -```json -{ - "name": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ------ | ------ | -------- | ------------ | ----------- | -| `name` | string | true | | | - ## codersdk.PatchTemplateVersionRequest ```json @@ -5266,6 +5252,20 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `url` | string | false | | URL to download the latest release of Coder. | | `version` | string | false | | Version is the semantic version for the latest release of Coder. | +## codersdk.UpdateOrganizationRequest + +```json +{ + "name": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| ------ | ------ | -------- | ------------ | ----------- | +| `name` | string | true | | | + ## codersdk.UpdateRoles ```json diff --git a/scripts/rbacgen/main.go b/scripts/rbacgen/main.go index 67b35ae57c037..1eb186c1b5ce4 100644 --- a/scripts/rbacgen/main.go +++ b/scripts/rbacgen/main.go @@ -16,6 +16,8 @@ import ( "slices" "strings" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/coderd/rbac/policy" ) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7bc17b1be7edc..fc40bc3b095bf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -222,7 +222,7 @@ export interface CreateGroupRequest { readonly quota_allowance: number; } -// From codersdk/users.go +// From codersdk/organizations.go export interface CreateOrganizationRequest { readonly name: string; } @@ -806,11 +806,6 @@ export interface PatchGroupRequest { readonly quota_allowance?: number; } -// From codersdk/users.go -export interface PatchOrganizationRequest { - readonly name: string; -} - // From codersdk/templateversions.go export interface PatchTemplateVersionRequest { readonly name: string; @@ -1305,6 +1300,11 @@ export interface UpdateCheckResponse { readonly url: string; } +// From codersdk/organizations.go +export interface UpdateOrganizationRequest { + readonly name: string; +} + // From codersdk/users.go export interface UpdateRoles { readonly roles: readonly string[]; From 8902a857d1aea447cb5161acead4015b239f7dd3 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:15:12 +0000 Subject: [PATCH 10/23] =?UTF-8?q?=F0=9F=A7=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/database/dbauthz/dbauthz_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 2367fbf698bfc..cea02538950ef 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -649,7 +649,7 @@ func (s *MethodTestSuite) TestOrganization() { check.Args(database.UpdateOrganizationParams{ ID: o.ID, Name: "something-different", - }).Asserts(rbac.ResourceOrganization, policy.ActionUpdate) + }).Asserts(o, policy.ActionUpdate) })) s.Run("DeleteOrganization", s.Subtest(func(db database.Store, check *expects) { ctx := testutil.Context(s.T(), testutil.WaitShort) @@ -660,7 +660,7 @@ func (s *MethodTestSuite) TestOrganization() { require.NoError(s.T(), err) check.Args( o.ID, - ).Asserts(rbac.ResourceOrganization, policy.ActionDelete) + ).Asserts(o, policy.ActionDelete) })) s.Run("UpdateMemberRoles", s.Subtest(func(db database.Store, check *expects) { o := dbgen.Organization(s.T(), db, database.Organization{}) From 8ec39049207b0164b8a4a6a2c7926b5afca562ef Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:25:09 +0000 Subject: [PATCH 11/23] fix `@Param` annotations --- coderd/organizations.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/organizations.go b/coderd/organizations.go index 8256987da2b7f..1cac07c475fa8 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -124,6 +124,7 @@ func (api *API) postOrganizations(rw http.ResponseWriter, r *http.Request) { // @Accept json // @Produce json // @Tags Organizations +// @Param organization path string true "Organization ID or name" // @Param request body codersdk.UpdateOrganizationRequest true "Patch organization request" // @Success 200 {object} codersdk.Organization // @Router /organizations/{organization} [patch] @@ -179,6 +180,7 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { // @Security CoderSessionToken // @Produce json // @Tags Organizations +// @Param organization path string true "Organization ID or name" // @Success 200 // @Router /organizations/{organization} [delete] func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { From ee44145ff92dcdcd79bfd3385f3c42c92d943b91 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:37:41 +0000 Subject: [PATCH 12/23] match query --- coderd/database/dbmem/dbmem.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index abae80f4db6c5..5d8d930c51f20 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1575,7 +1575,7 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error { for i, org := range q.organizations { - if org.ID == id { + if org.ID == id && !org.IsDefault { q.organizations = append(q.organizations[:i], q.organizations[i+1:]...) return nil } From f30665cb619e1037b4be01e4453797dae738afd1 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:40:06 +0000 Subject: [PATCH 13/23] only validate name if it's different --- coderd/organizations.go | 42 +++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index 1cac07c475fa8..a7bbb24240fa8 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -137,29 +137,31 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { return } - if req.Name == codersdk.DefaultOrganization && !organization.IsDefault { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), - }) - return - } + if req.Name != organization.Name { + if req.Name == codersdk.DefaultOrganization && !organization.IsDefault { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return + } - _, err := api.Database.GetOrganizationByName(ctx, req.Name) - if err == nil { - httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: "Organization already exists with that name.", - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), - Detail: err.Error(), - }) - return + _, err := api.Database.GetOrganizationByName(ctx, req.Name) + if err == nil { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: "Organization already exists with that name.", + }) + return + } + if !errors.Is(err, sql.ErrNoRows) { + httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ + Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), + Detail: err.Error(), + }) + return + } } - organization, err = api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ + organization, err := api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ ID: organization.ID, UpdatedAt: dbtime.Now(), Name: req.Name, From 8342b26747dcec4283e1e263d68dc77ad5072c0e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 18:47:56 +0000 Subject: [PATCH 14/23] =?UTF-8?q?=E2=9A=97=EF=B8=8F=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/apidoc/docs.go | 16 ++++++++ coderd/apidoc/swagger.json | 16 ++++++++ coderd/organizations.go | 2 +- coderd/organizations_test.go | 76 +++++++++++++++++++++++++++++++++++- docs/api/organizations.md | 13 ++++-- 5 files changed, 118 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 7a2b1108d85ed..893c04bc19211 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2002,6 +2002,15 @@ const docTemplate = `{ ], "summary": "Delete organization", "operationId": "delete-organization", + "parameters": [ + { + "type": "string", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK" @@ -2026,6 +2035,13 @@ const docTemplate = `{ "summary": "Update organization", "operationId": "update-organization", "parameters": [ + { + "type": "string", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, { "description": "Patch organization request", "name": "request", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index f04038ed3c6d3..c083b0e8efa9b 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1743,6 +1743,15 @@ "tags": ["Organizations"], "summary": "Delete organization", "operationId": "delete-organization", + "parameters": [ + { + "type": "string", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "OK" @@ -1761,6 +1770,13 @@ "summary": "Update organization", "operationId": "update-organization", "parameters": [ + { + "type": "string", + "description": "Organization ID or name", + "name": "organization", + "in": "path", + "required": true + }, { "description": "Patch organization request", "name": "request", diff --git a/coderd/organizations.go b/coderd/organizations.go index a7bbb24240fa8..16c231c1c2198 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -183,7 +183,7 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { // @Produce json // @Tags Organizations // @Param organization path string true "Organization ID or name" -// @Success 200 +// @Success 200 {object} codersdk.Response // @Router /organizations/{organization} [delete] func (api *API) deleteOrganization(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index c23862ae7c937..04190ed3bb07c 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -187,7 +187,27 @@ func TestPatchOrganizationsByUser(t *testing.T) { require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) }) - t.Run("Update", func(t *testing.T) { + t.Run("UpdateById", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "new", + }) + require.NoError(t, err) + + o, err = client.UpdateOrganization(ctx, o.ID.String(), codersdk.UpdateOrganizationRequest{ + Name: "new-new", + }) + require.NoError(t, err) + require.Equal(t, "new-new", o.Name) + }) + + t.Run("UpdateByName", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) @@ -207,3 +227,57 @@ func TestPatchOrganizationsByUser(t *testing.T) { require.Equal(t, "new-new", o.Name) }) } + +func TestDeleteOrganizationsByUser(t *testing.T) { + t.Parallel() + t.Run("Default", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.Organization(ctx, user.OrganizationID) + require.NoError(t, err) + + err = client.DeleteOrganization(ctx, o.ID.String()) + var apiErr *codersdk.Error + require.ErrorAs(t, err, &apiErr) + require.Equal(t, http.StatusBadRequest, apiErr.StatusCode()) + }) + + t.Run("DeleteById", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "doomed", + }) + require.NoError(t, err) + + err = client.DeleteOrganization(ctx, o.ID.String()) + require.NoError(t, err) + }) + + t.Run("DeleteByName", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ + Name: "doomed", + }) + require.NoError(t, err) + + err = client.DeleteOrganization(ctx, o.Name) + require.NoError(t, err) + }) +} diff --git a/docs/api/organizations.md b/docs/api/organizations.md index dc5e2e9c72ccd..25c7328e94d50 100644 --- a/docs/api/organizations.md +++ b/docs/api/organizations.md @@ -190,6 +190,12 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \ `DELETE /organizations/{organization}` +### Parameters + +| Name | In | Type | Required | Description | +| -------------- | ---- | ------ | -------- | ----------------------- | +| `organization` | path | string | true | Organization ID or name | + ### Responses | Status | Meaning | Description | Schema | @@ -222,9 +228,10 @@ curl -X PATCH http://coder-server:8080/api/v2/organizations/{organization} \ ### Parameters -| Name | In | Type | Required | Description | -| ------ | ---- | ---------------------------------------------------------------------------------- | -------- | -------------------------- | -| `body` | body | [codersdk.UpdateOrganizationRequest](schemas.md#codersdkupdateorganizationrequest) | true | Patch organization request | +| Name | In | Type | Required | Description | +| -------------- | ---- | ---------------------------------------------------------------------------------- | -------- | -------------------------- | +| `organization` | path | string | true | Organization ID or name | +| `body` | body | [codersdk.UpdateOrganizationRequest](schemas.md#codersdkupdateorganizationrequest) | true | Patch organization request | ### Example responses From dda30ec3c4b7bdd2d7e94c0c3cf9094544c157fc Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Thu, 16 May 2024 19:07:42 +0000 Subject: [PATCH 15/23] >:( --- coderd/apidoc/docs.go | 5 ++++- coderd/apidoc/swagger.json | 5 ++++- docs/api/organizations.md | 24 +++++++++++++++++++++--- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 893c04bc19211..9dc9354ebab7c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -2013,7 +2013,10 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } } } }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index c083b0e8efa9b..4e3a26d563088 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -1754,7 +1754,10 @@ ], "responses": { "200": { - "description": "OK" + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.Response" + } } } }, diff --git a/docs/api/organizations.md b/docs/api/organizations.md index 25c7328e94d50..c6f4514eb9bad 100644 --- a/docs/api/organizations.md +++ b/docs/api/organizations.md @@ -185,6 +185,7 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio ```shell # Example request using curl curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \ + -H 'Accept: application/json' \ -H 'Coder-Session-Token: API_KEY' ``` @@ -196,11 +197,28 @@ curl -X DELETE http://coder-server:8080/api/v2/organizations/{organization} \ | -------------- | ---- | ------ | -------- | ----------------------- | | `organization` | path | string | true | Organization ID or name | +### Example responses + +> 200 Response + +```json +{ + "detail": "string", + "message": "string", + "validations": [ + { + "detail": "string", + "field": "string" + } + ] +} +``` + ### Responses -| Status | Meaning | Description | Schema | -| ------ | ------------------------------------------------------- | ----------- | ------ | -| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | | +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------ | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.Response](schemas.md#codersdkresponse) | To perform this operation, you must be authenticated. [Learn more](authentication.md). From 514fcdf3afee2831abe61113a162f9ecc7c70cba Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 16:52:29 +0000 Subject: [PATCH 16/23] small refactors --- coderd/database/dbauthz/dbauthz_test.go | 10 +---- coderd/database/dbmem/dbmem.go | 6 +++ coderd/organizations_test.go | 57 ++++++------------------- 3 files changed, 22 insertions(+), 51 deletions(-) diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index cea02538950ef..f9ea9fd67c5ed 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -640,24 +640,18 @@ func (s *MethodTestSuite) TestOrganization() { rbac.ResourceOrganizationMember.InOrg(o.ID).WithID(u.ID), policy.ActionCreate) })) s.Run("UpdateOrganization", s.Subtest(func(db database.Store, check *expects) { - ctx := testutil.Context(s.T(), testutil.WaitShort) - o, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{ - ID: uuid.New(), + o := dbgen.Organization(s.T(), db, database.Organization{ Name: "something-unique", }) - require.NoError(s.T(), err) check.Args(database.UpdateOrganizationParams{ ID: o.ID, Name: "something-different", }).Asserts(o, policy.ActionUpdate) })) s.Run("DeleteOrganization", s.Subtest(func(db database.Store, check *expects) { - ctx := testutil.Context(s.T(), testutil.WaitShort) - o, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{ - ID: uuid.New(), + o := dbgen.Organization(s.T(), db, database.Organization{ Name: "doomed", }) - require.NoError(s.T(), err) check.Args( o.ID, ).Asserts(o, policy.ActionDelete) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 5d8d930c51f20..5f4ad8e485732 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -1574,6 +1574,9 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error { } func (q *FakeQuerier) DeleteOrganization(_ context.Context, id uuid.UUID) error { + q.mutex.Lock() + defer q.mutex.Unlock() + for i, org := range q.organizations { if org.ID == id && !org.IsDefault { q.organizations = append(q.organizations[:i], q.organizations[i+1:]...) @@ -7159,6 +7162,9 @@ func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateO return database.Organization{}, err } + q.mutex.Lock() + defer q.mutex.Unlock() + for i, org := range q.organizations { if org.ID == arg.ID { org.Name = arg.Name diff --git a/coderd/organizations_test.go b/coderd/organizations_test.go index 04190ed3bb07c..8ce39c5593d90 100644 --- a/coderd/organizations_test.go +++ b/coderd/organizations_test.go @@ -1,7 +1,6 @@ package coderd_test import ( - "context" "net/http" "testing" @@ -16,9 +15,7 @@ func TestMultiOrgFetch(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) makeOrgs := []string{"foo", "bar", "baz"} for _, name := range makeOrgs { @@ -38,9 +35,7 @@ func TestOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) orgs, err := client.OrganizationsByUser(ctx, codersdk.Me) require.NoError(t, err) @@ -62,9 +57,7 @@ func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) _, err := client.OrganizationByUserAndName(ctx, codersdk.Me, "nothing") var apiErr *codersdk.Error @@ -77,9 +70,7 @@ func TestOrganizationByUserAndName(t *testing.T) { client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) other, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) org, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "another", @@ -95,9 +86,7 @@ func TestOrganizationByUserAndName(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) org, err := client.Organization(ctx, user.OrganizationID) require.NoError(t, err) @@ -112,9 +101,7 @@ func TestPostOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) org, err := client.Organization(ctx, user.OrganizationID) require.NoError(t, err) @@ -130,9 +117,7 @@ func TestPostOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) _, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "new", @@ -147,9 +132,7 @@ func TestPatchOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) originalOrg, err := client.Organization(ctx, user.OrganizationID) require.NoError(t, err) @@ -170,9 +153,7 @@ func TestPatchOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "something-unique", @@ -191,9 +172,7 @@ func TestPatchOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "new", @@ -211,9 +190,7 @@ func TestPatchOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "new", @@ -234,9 +211,7 @@ func TestDeleteOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.Organization(ctx, user.OrganizationID) require.NoError(t, err) @@ -251,9 +226,7 @@ func TestDeleteOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "doomed", @@ -268,9 +241,7 @@ func TestDeleteOrganizationsByUser(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitMedium) o, err := client.CreateOrganization(ctx, codersdk.CreateOrganizationRequest{ Name: "doomed", From 4ee37e2330fd5669bfa3305881d2d371e4e38e41 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 16:55:09 +0000 Subject: [PATCH 17/23] named sql args --- coderd/database/queries.sql.go | 10 +++++----- coderd/database/queries/organizations.sql | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 34bc41bc456df..3cc9f15d8ba8c 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -4109,21 +4109,21 @@ const updateOrganization = `-- name: UpdateOrganization :one UPDATE organizations SET - updated_at = $2, - name = $3 + updated_at = $1, + name = $2 WHERE - id = $1 + id = $3 RETURNING id, name, description, created_at, updated_at, is_default ` type UpdateOrganizationParams struct { - ID uuid.UUID `db:"id" json:"id"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` Name string `db:"name" json:"name"` + ID uuid.UUID `db:"id" json:"id"` } func (q *sqlQuerier) UpdateOrganization(ctx context.Context, arg UpdateOrganizationParams) (Organization, error) { - row := q.db.QueryRowContext(ctx, updateOrganization, arg.ID, arg.UpdatedAt, arg.Name) + row := q.db.QueryRowContext(ctx, updateOrganization, arg.UpdatedAt, arg.Name, arg.ID) var i Organization err := row.Scan( &i.ID, diff --git a/coderd/database/queries/organizations.sql b/coderd/database/queries/organizations.sql index aaa262b939caa..9d5cec1324fe6 100644 --- a/coderd/database/queries/organizations.sql +++ b/coderd/database/queries/organizations.sql @@ -58,10 +58,10 @@ VALUES UPDATE organizations SET - updated_at = $2, - name = $3 + updated_at = @updated_at, + name = @name WHERE - id = $1 + id = @id RETURNING *; -- name: DeleteOrganization :exec From f34b1eb9f4a467907cee15589e2af579e1104ebe Mon Sep 17 00:00:00 2001 From: Kayla Washburn-Love Date: Fri, 17 May 2024 11:50:39 -0600 Subject: [PATCH 18/23] rework update error handling Co-authored-by: Steven Masley --- coderd/organizations.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coderd/organizations.go b/coderd/organizations.go index 16c231c1c2198..ed8306afce562 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -166,6 +166,20 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { UpdatedAt: dbtime.Now(), Name: req.Name, }) + if httpapi.Is404Error(err) { + httpapi.ResourceNotFound(rw) + return + } + if database.IsUniqueViolation(err) { + httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ + Message: "Organization already exists with that name.", + Validations: []codersdk.ValidationError{{ + Field: "name", + Detail: "This value is already in use and should be unique.", + }}, + }) + return + } if err != nil { httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ Message: "Internal error updating organization.", From 2125e2d9a1af33010fde951744f0c0539450a2fd Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 17:54:33 +0000 Subject: [PATCH 19/23] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/organizations.go | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index ed8306afce562..1203e2b6aeb7e 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -137,28 +137,12 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { return } - if req.Name != organization.Name { - if req.Name == codersdk.DefaultOrganization && !organization.IsDefault { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), - }) - return - } - - _, err := api.Database.GetOrganizationByName(ctx, req.Name) - if err == nil { - httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: "Organization already exists with that name.", - }) - return - } - if !errors.Is(err, sql.ErrNoRows) { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("Internal error fetching organization %q.", req.Name), - Detail: err.Error(), - }) - return - } + // Can't rename to the default org name, unless you are the default org + if req.Name != organization.Name && req.Name == codersdk.DefaultOrganization && !organization.IsDefault { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), + }) + return } organization, err := api.Database.UpdateOrganization(ctx, database.UpdateOrganizationParams{ @@ -172,7 +156,7 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { } if database.IsUniqueViolation(err) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: "Organization already exists with that name.", + Message: fmt.Sprintf("Organization already exists with the name %q.", req.Name), Validations: []codersdk.ValidationError{{ Field: "name", Detail: "This value is already in use and should be unique.", From 6686bb365f85a18b3408ea6fb2ff215e177ff739 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 18:18:38 +0000 Subject: [PATCH 20/23] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- coderd/organizations.go | 5 +++-- enterprise/coderd/roles_test.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/coderd/organizations.go b/coderd/organizations.go index 1203e2b6aeb7e..2a43ed2a7011a 100644 --- a/coderd/organizations.go +++ b/coderd/organizations.go @@ -137,8 +137,9 @@ func (api *API) patchOrganization(rw http.ResponseWriter, r *http.Request) { return } - // Can't rename to the default org name, unless you are the default org - if req.Name != organization.Name && req.Name == codersdk.DefaultOrganization && !organization.IsDefault { + // "default" is a reserved name that always refers to the default org (much like the way we + // use "me" for users). + if req.Name == codersdk.DefaultOrganization { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ Message: fmt.Sprintf("Organization name %q is reserved.", codersdk.DefaultOrganization), }) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index 450f80e0b7fe3..b8aac6fa5816c 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -64,7 +64,7 @@ func TestCustomRole(t *testing.T) { // Verify the role exists in the list // TODO: Turn this assertion back on when the cli api experience is created. - //allRoles, err := tmplAdmin.ListSiteRoles(ctx) + // allRoles, err := tmplAdmin.ListSiteRoles(ctx) //require.NoError(t, err) // //require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool { From 9c769f8ce2d264b496c42adc7beba6f3ed37775d Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 19:01:28 +0000 Subject: [PATCH 21/23] enforce unique_organizations_name constraint in dbmem --- coderd/database/dbmem/dbmem.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 080fcaa34b72c..ba1d15f3787fe 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -7254,6 +7254,18 @@ func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateO q.mutex.Lock() defer q.mutex.Unlock() + // Enforce the unique constraint, because the API endpoint relies on the database catching + // non-unique names during updates. + for _, org := range q.organizations { + if org.Name == arg.Name && org.ID != arg.ID { + // https://github.com/lib/pq/blob/3d613208bca2e74f2a20e04126ed30bcb5c4cc27/error.go#L178 + return database.Organization{}, &pq.Error{ + Code: pq.ErrorCode("23505"), // "unique_violation" + Constraint: string(database.UniqueOrganizationsName), + } + } + } + for i, org := range q.organizations { if org.ID == arg.ID { org.Name = arg.Name From 94484a0cbab7ad378f8314e9218fe7fea7fb89ba Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 19:15:15 +0000 Subject: [PATCH 22/23] refactor unique constraint error mocking a little --- coderd/database/dbmem/dbmem.go | 45 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ba1d15f3787fe..0a409582c27a5 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -33,15 +33,18 @@ import ( var validProxyByHostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) -var errForeignKeyConstraint = &pq.Error{ - Code: "23503", - Message: "update or delete on table violates foreign key constraint", -} - -var errDuplicateKey = &pq.Error{ - Code: "23505", - Message: "duplicate key value violates unique constraint", -} +// A full mapping of error codes from pq v1.10.9 can be found here: +// https://github.com/lib/pq/blob/2a217b94f5ccd3de31aec4152a541b9ff64bed05/error.go#L75 +var ( + errForeignKeyConstraint = &pq.Error{ + Code: "23503", // "foreign_key_violation" + Message: "update or delete on table violates foreign key constraint", + } + errUniqueConstraint = &pq.Error{ + Code: "23505", // "unique_violation" + Message: "duplicate key value violates unique constraint", + } +) // New returns an in-memory fake of the database. func New() database.Store { @@ -5809,7 +5812,7 @@ func (q *FakeQuerier) InsertDBCryptKey(_ context.Context, arg database.InsertDBC for _, key := range q.dbcryptKeys { if key.Number == arg.Number { - return errDuplicateKey + return errUniqueConstraint } } @@ -5913,7 +5916,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar for _, group := range q.groups { if group.OrganizationID == arg.OrganizationID && group.Name == arg.Name { - return database.Group{}, errDuplicateKey + return database.Group{}, errUniqueConstraint } } @@ -5944,7 +5947,7 @@ func (q *FakeQuerier) InsertGroupMember(_ context.Context, arg database.InsertGr for _, member := range q.groupMembers { if member.GroupID == arg.GroupID && member.UserID == arg.UserID { - return errDuplicateKey + return errUniqueConstraint } } @@ -6028,7 +6031,7 @@ func (q *FakeQuerier) InsertOAuth2ProviderApp(_ context.Context, arg database.In for _, app := range q.oauth2ProviderApps { if app.Name == arg.Name { - return database.OAuth2ProviderApp{}, errDuplicateKey + return database.OAuth2ProviderApp{}, errUniqueConstraint } } @@ -6390,7 +6393,7 @@ func (q *FakeQuerier) InsertUser(_ context.Context, arg database.InsertUserParam for _, user := range q.users { if user.Username == arg.Username && !user.Deleted { - return database.User{}, errDuplicateKey + return database.User{}, errUniqueConstraint } } @@ -6803,7 +6806,7 @@ func (q *FakeQuerier) InsertWorkspaceProxy(_ context.Context, arg database.Inser lastRegionID := int32(0) for _, p := range q.workspaceProxies { if !p.Deleted && p.Name == arg.Name { - return database.WorkspaceProxy{}, errDuplicateKey + return database.WorkspaceProxy{}, errUniqueConstraint } if p.RegionID > lastRegionID { lastRegionID = p.RegionID @@ -7197,7 +7200,7 @@ func (q *FakeQuerier) UpdateOAuth2ProviderAppByID(_ context.Context, arg databas for _, app := range q.oauth2ProviderApps { if app.Name == arg.Name && app.ID != arg.ID { - return database.OAuth2ProviderApp{}, errDuplicateKey + return database.OAuth2ProviderApp{}, errUniqueConstraint } } @@ -7258,11 +7261,7 @@ func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateO // non-unique names during updates. for _, org := range q.organizations { if org.Name == arg.Name && org.ID != arg.ID { - // https://github.com/lib/pq/blob/3d613208bca2e74f2a20e04126ed30bcb5c4cc27/error.go#L178 - return database.Organization{}, &pq.Error{ - Code: pq.ErrorCode("23505"), // "unique_violation" - Constraint: string(database.UniqueOrganizationsName), - } + return database.Organization{}, errUniqueConstraint } } @@ -7873,7 +7872,7 @@ func (q *FakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWork continue } if other.Name == arg.Name { - return database.Workspace{}, errDuplicateKey + return database.Workspace{}, errUniqueConstraint } } @@ -8213,7 +8212,7 @@ func (q *FakeQuerier) UpdateWorkspaceProxy(_ context.Context, arg database.Updat for _, p := range q.workspaceProxies { if p.Name == arg.Name && p.ID != arg.ID { - return database.WorkspaceProxy{}, errDuplicateKey + return database.WorkspaceProxy{}, errUniqueConstraint } } From b1bfba6ca333c9627269fbd17e59593973e2a956 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Fri, 17 May 2024 19:22:02 +0000 Subject: [PATCH 23/23] wow --- enterprise/coderd/roles_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/enterprise/coderd/roles_test.go b/enterprise/coderd/roles_test.go index b8aac6fa5816c..fd43de0edd72e 100644 --- a/enterprise/coderd/roles_test.go +++ b/enterprise/coderd/roles_test.go @@ -65,11 +65,11 @@ func TestCustomRole(t *testing.T) { // Verify the role exists in the list // TODO: Turn this assertion back on when the cli api experience is created. // allRoles, err := tmplAdmin.ListSiteRoles(ctx) - //require.NoError(t, err) + // require.NoError(t, err) // - //require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool { - // return selected.Name == role.Name - //}), "role missing from site role list") + // require.True(t, slices.ContainsFunc(allRoles, func(selected codersdk.AssignableRoles) bool { + // return selected.Name == role.Name + // }), "role missing from site role list") }) // Revoked licenses cannot modify/create custom roles, but they can