Skip to content

chore: ensure default org always exists #12412

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 5, 2024
6 changes: 5 additions & 1 deletion cli/templatelist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ func TestTemplateList(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{})
owner := coderdtest.CreateFirstUser(t, client)

org, err := client.Organization(context.Background(), owner.OrganizationID)
require.NoError(t, err)

templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())

inv, root := clitest.New(t, "templates", "list")
Expand All @@ -107,7 +111,7 @@ func TestTemplateList(t *testing.T) {
require.NoError(t, <-errC)

pty.ExpectMatch("No templates found in")
pty.ExpectMatch(coderdtest.FirstUserParams.Username)
pty.ExpectMatch(org.Name)
pty.ExpectMatch("Create one:")
})
}
2 changes: 1 addition & 1 deletion coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ var (
rbac.ResourceGroup.Type: {rbac.ActionCreate, rbac.ActionUpdate},
rbac.ResourceRoleAssignment.Type: {rbac.ActionCreate, rbac.ActionDelete},
rbac.ResourceSystem.Type: {rbac.WildcardSymbol},
rbac.ResourceOrganization.Type: {rbac.ActionCreate},
rbac.ResourceOrganization.Type: {rbac.ActionCreate, rbac.ActionRead},
rbac.ResourceOrganizationMember.Type: {rbac.ActionCreate},
rbac.ResourceOrgRoleAssignment.Type: {rbac.ActionCreate},
rbac.ResourceProvisionerDaemon.Type: {rbac.ActionCreate, rbac.ActionUpdate},
Expand Down
5 changes: 3 additions & 2 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ func (s *MethodTestSuite) TestOrganization() {
check.Args(o.ID).Asserts(o, rbac.ActionRead).Returns(o)
}))
s.Run("GetDefaultOrganization", s.Subtest(func(db database.Store, check *expects) {
o := dbgen.Organization(s.T(), db, database.Organization{})
o, _ := db.GetDefaultOrganization(context.Background())
check.Args().Asserts(o, rbac.ActionRead).Returns(o)
}))
s.Run("GetOrganizationByName", s.Subtest(func(db database.Store, check *expects) {
Expand Down Expand Up @@ -597,9 +597,10 @@ func (s *MethodTestSuite) TestOrganization() {
check.Args(u.ID).Asserts(a, rbac.ActionRead, b, rbac.ActionRead).Returns(slice.New(a, b))
}))
s.Run("GetOrganizations", s.Subtest(func(db database.Store, check *expects) {
def, _ := db.GetDefaultOrganization(context.Background())
a := dbgen.Organization(s.T(), db, database.Organization{})
b := dbgen.Organization(s.T(), db, database.Organization{})
check.Args().Asserts(a, rbac.ActionRead, b, rbac.ActionRead).Returns(slice.New(a, b))
check.Args().Asserts(def, rbac.ActionRead, a, rbac.ActionRead, b, rbac.ActionRead).Returns(slice.New(def, a, b))
}))
s.Run("GetOrganizationsByUserID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
Expand Down
11 changes: 11 additions & 0 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ func New() database.Store {
locks: map[int64]struct{}{},
},
}
// Always start with a default org. Matching migration 198.
_, err := q.InsertOrganization(context.Background(), database.InsertOrganizationParams{
ID: uuid.New(),
Name: "first-organization",
Description: "Builtin default organization.",
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
})
if err != nil {
panic(fmt.Errorf("failed to create default organization: %w", err))
}
q.defaultProxyDisplayName = "Default"
q.defaultProxyIconURL = "/emojis/1f3e1.png"
return q
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- There is no down. If the org is created, just let it be. Deleting an org feels dangerous in a migration.
16 changes: 16 additions & 0 deletions coderd/database/migrations/000198_ensure_default_org.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- This ensures a default organization always exists.
INSERT INTO
organizations(id, name, description, created_at, updated_at, is_default)
SELECT
-- Avoid calling it "default" as we are reserving that word as a keyword to fetch
-- the default org regardless of the name.
gen_random_uuid(),
'first-organization',
'Builtin default organization.',
now(),
now(),
true
WHERE
-- Only insert if no organizations exist.
NOT EXISTS (SELECT * FROM organizations);

15 changes: 3 additions & 12 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,20 +506,11 @@ func TestDefaultOrg(t *testing.T) {
db := database.New(sqlDB)
ctx := context.Background()

// Should start with 0 orgs
// Should start with the default org
all, err := db.GetOrganizations(ctx)
require.NoError(t, err)
require.Len(t, all, 0)

org, err := db.InsertOrganization(ctx, database.InsertOrganizationParams{
ID: uuid.New(),
Name: "default",
Description: "",
CreatedAt: dbtime.Now(),
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
require.True(t, org.IsDefault, "first org should always be default")
require.Len(t, all, 1)
require.True(t, all[0].IsDefault, "first org should always be default")
}

type tvArgs struct {
Expand Down
43 changes: 30 additions & 13 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,16 +171,37 @@ func (api *API) postFirstUser(rw http.ResponseWriter, r *http.Request) {
}
}

//nolint:gocritic // needed to create first user
defaultOrg, err := api.Database.GetDefaultOrganization(dbauthz.AsSystemRestricted(ctx))
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching default organization. If you are encountering this error, you will have to restart the Coder deployment.",
Detail: err.Error(),
})
return
}

//nolint:gocritic // ensure everyone group
_, err = api.Database.InsertAllUsersGroup(dbauthz.AsSystemRestricted(ctx), defaultOrg.ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also be part of the migration?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that too. What I didn't like about the migration was I can't tell if the INSERT INTO organizations ran with an insert, or without. So the followup of INSERT INTO groups cannot be dependent on if an org did not exist at the start of the migration or not.

Putting it in postFirstUser ensures that it is a new instance setup, so I know this is safe.

Although we do not allow dropping the everyone group, so it should be a safe INSERT INTO groups ... DO NOTHING.


I can move it to a migration if you feel that is better.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just wary of all this side-effect stuff of creating a user.

I also kind of like the symmetry of creating an org and the corresponding everyone group together, since that's presumably what we'll do when we allow creating new organizations, since there is no similar "first user creation" event to tie it to.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw this comment after the approval merge, my mistake.

I can make another migration for the everyone group and do nothing on conflict.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you

// A unique constraint violation just means the group already exists.
// This should not happen, but is ok if it does.
if err != nil && !database.IsUniqueViolation(err) {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error creating all users group.",
Detail: err.Error(),
})
return
}

//nolint:gocritic // needed to create first user
user, organizationID, err := api.CreateUser(dbauthz.AsSystemRestricted(ctx), api.Database, CreateUserRequest{
CreateUserRequest: codersdk.CreateUserRequest{
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
// Create an org for the first user.
OrganizationID: uuid.Nil,
Email: createUser.Email,
Username: createUser.Username,
Password: createUser.Password,
OrganizationID: defaultOrg.ID,
},
CreateOrganization: true,
CreateOrganization: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove this field, which also allows cleaning up a lot of cruft in userauth.go

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh you are right. I think we can drop the CreateOrganization behavior entirely. I'll make a new PR that will look into dropping it.

LoginType: database.LoginTypePassword,
})
if err != nil {
Expand Down Expand Up @@ -1033,10 +1054,7 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) {
}

for _, mem := range memberships {
// If we can read the org member, include the roles.
if err == nil {
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
}
resp.OrganizationRoles[mem.OrganizationID] = mem.Roles
}

httpapi.Write(ctx, rw, http.StatusOK, resp)
Expand Down Expand Up @@ -1247,9 +1265,8 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
// TODO: When organizations are allowed to be created, we should
// come back to determining the default role of the person who
// creates the org. Until that happens, all users in an organization
// should be just regular members.
orgRoles = append(orgRoles, rbac.RoleOrgMember(req.OrganizationID))

// should be just regular members. Membership role is implied, and
// not required to be explicit.
_, err = tx.InsertAllUsersGroup(ctx, organization.ID)
if err != nil {
return xerrors.Errorf("create %q group: %w", database.EveryoneGroup, err)
Expand Down
4 changes: 1 addition & 3 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1093,9 +1093,7 @@ func TestInitialRoles(t *testing.T) {
rbac.RoleOwner(),
}, "should be a member and admin")

require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{
rbac.RoleOrgMember(first.OrganizationID),
}, "should be a member and admin")
require.ElementsMatch(t, roles.OrganizationRoles[first.OrganizationID], []string{}, "should be a member")
}

func TestPutUserSuspend(t *testing.T) {
Expand Down