feat(coderd): add endpoints for editing and deleting organizations (#13287)

This commit is contained in:
Kayla Washburn-Love
2024-05-21 12:46:31 -06:00
committed by GitHub
parent 0a86d6d176
commit 3f1e9c038a
19 changed files with 757 additions and 63 deletions
+11
View File
@@ -984,6 +984,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(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
@@ -2853,6 +2857,13 @@ 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) {
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 {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceProvisionerDaemon); err != nil {
return err
+18 -1
View File
@@ -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,23 @@ 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) {
o := dbgen.Organization(s.T(), db, database.Organization{
Name: "something-unique",
})
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) {
o := dbgen.Organization(s.T(), db, database.Organization{
Name: "doomed",
})
check.Args(
o.ID,
).Asserts(o, 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{})
+61 -18
View File
@@ -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 {
@@ -1601,6 +1604,19 @@ func (q *FakeQuerier) DeleteOldWorkspaceAgentStats(_ context.Context) error {
return nil
}
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:]...)
return nil
}
}
return sql.ErrNoRows
}
func (q *FakeQuerier) DeleteReplicasUpdatedBefore(_ context.Context, before time.Time) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -5823,7 +5839,7 @@ func (q *FakeQuerier) InsertDBCryptKey(_ context.Context, arg database.InsertDBC
for _, key := range q.dbcryptKeys {
if key.Number == arg.Number {
return errDuplicateKey
return errUniqueConstraint
}
}
@@ -5927,7 +5943,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
}
}
@@ -5958,7 +5974,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
}
}
@@ -6042,7 +6058,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
}
}
@@ -6423,7 +6439,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
}
}
@@ -6836,7 +6852,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
@@ -7230,7 +7246,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
}
}
@@ -7278,6 +7294,33 @@ func (q *FakeQuerier) UpdateOAuth2ProviderAppSecretByID(_ context.Context, arg d
return database.OAuth2ProviderAppSecret{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateOrganization(_ context.Context, arg database.UpdateOrganizationParams) (database.Organization, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.Organization{}, err
}
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 {
return database.Organization{}, errUniqueConstraint
}
}
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 {
err := validateDatabaseType(arg)
if err != nil {
@@ -7875,7 +7918,7 @@ func (q *FakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWork
continue
}
if other.Name == arg.Name {
return database.Workspace{}, errDuplicateKey
return database.Workspace{}, errUniqueConstraint
}
}
@@ -8215,7 +8258,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
}
}
+14
View File
@@ -284,6 +284,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)
@@ -1845,6 +1852,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)
+29
View File
@@ -455,6 +455,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()
@@ -3881,6 +3895,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()
+2
View File
@@ -74,6 +74,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)
@@ -368,6 +369,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
+44
View File
@@ -3934,6 +3934,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
@@ -4126,6 +4139,37 @@ func (q *sqlQuerier) InsertOrganization(ctx context.Context, arg InsertOrganizat
return i, err
}
const updateOrganization = `-- name: UpdateOrganization :one
UPDATE
organizations
SET
updated_at = $1,
name = $2
WHERE
id = $3
RETURNING id, name, description, created_at, updated_at, is_default
`
type UpdateOrganizationParams struct {
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.UpdatedAt, arg.Name, arg.ID)
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
+17
View File
@@ -53,3 +53,20 @@ 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 = @updated_at,
name = @name
WHERE
id = @id
RETURNING *;
-- name: DeleteOrganization :exec
DELETE FROM
organizations
WHERE
id = $1 AND
is_default = false;