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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy