feat: backend support for creating and storing service accounts (#22698)

Add is_service_account column to users table with CHECK constraints
enforcing login_type='none' and empty email for service accounts.
Update user creation API to validate service account constraints.

Related to:
https://linear.app/codercom/issue/PLAT-27/feat-backend-support-for-creating-and-storing-service-accounts
This commit is contained in:
George K
2026-03-11 10:19:08 -07:00
committed by GitHub
parent e96cd5cbb2
commit e5c19d0af4
28 changed files with 522 additions and 87 deletions
+20 -10
View File
@@ -578,17 +578,27 @@ func WorkspaceBuildParameters(t testing.TB, db database.Store, orig []database.W
}
func User(t testing.TB, db database.Store, orig database.User) database.User {
loginType := takeFirst(orig.LoginType, database.LoginTypePassword)
email := takeFirst(orig.Email, testutil.GetRandomName(t))
// A DB constraint requires login_type = 'none' and email = '' for service
// accounts.
if orig.IsServiceAccount {
loginType = database.LoginTypeNone
email = ""
}
user, err := db.InsertUser(genCtx, database.InsertUserParams{
ID: takeFirst(orig.ID, uuid.New()),
Email: takeFirst(orig.Email, testutil.GetRandomName(t)),
Username: takeFirst(orig.Username, testutil.GetRandomName(t)),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
HashedPassword: takeFirstSlice(orig.HashedPassword, []byte(must(cryptorand.String(32)))),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}),
LoginType: takeFirst(orig.LoginType, database.LoginTypePassword),
Status: string(takeFirst(orig.Status, database.UserStatusDormant)),
ID: takeFirst(orig.ID, uuid.New()),
Email: email,
Username: takeFirst(orig.Username, testutil.GetRandomName(t)),
Name: takeFirst(orig.Name, testutil.GetRandomName(t)),
HashedPassword: takeFirstSlice(orig.HashedPassword, []byte(must(cryptorand.String(32)))),
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
UpdatedAt: takeFirst(orig.UpdatedAt, dbtime.Now()),
RBACRoles: takeFirstSlice(orig.RBACRoles, []string{}),
LoginType: loginType,
Status: string(takeFirst(orig.Status, database.UserStatusDormant)),
IsServiceAccount: orig.IsServiceAccount,
})
require.NoError(t, err, "insert user")
+14
View File
@@ -213,6 +213,20 @@ func TestGenerator(t *testing.T) {
require.Equal(t, exp, must(db.GetUserByID(context.Background(), exp.ID)))
})
t.Run("ServiceAccountUser", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
user := dbgen.User(t, db, database.User{
IsServiceAccount: true,
Email: "should-be-overridden@coder.com",
LoginType: database.LoginTypePassword,
})
require.True(t, user.IsServiceAccount)
require.Empty(t, user.Email)
require.Equal(t, database.LoginTypeNone, user.LoginType)
require.Equal(t, user, must(db.GetUserByID(context.Background(), user.ID)))
})
t.Run("SSHKey", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)