Skip to content

Commit c65996a

Browse files
feat: add user_secrets table (#19162)
Closes coder/internal#780 ## Summary of changes: - added `user_secrets` table - `user_secrets` table contains `env_name` and `file_path` fields which are not used at the moment, but will be used in later PRs - `user_secrets` table doesn't contain `value_key_id`, I will add it in a separate migration in a dbcrypt PR - on one hand I don't want to add fields which are not used (because it's a risk smth may change in implementation later), on the other hand I don't want to add too many migrations for user secrets table - added unique sql indexes - added sql queries for CRUD operations on user-secrets - introduced new `ResourceUserSecret` resource - basic unit-tests for CRUD ops and authorization behavior - Role updates: - owner: - remove `ResourceUserSecret` from site-wide perms - add `ResourceUserSecret` to user-wide perms - orgAdmin - remove `ResourceUserSecret` from org-wide perms; seems it's not strictly required, because `ResourceUserSecret` is not tied to organization in dbauthz wrappers? - memberRole - no need to change memberRole because it implicitly has access to user-secrets thanks to the `allPermsExcept` - is it enough changes to roles? Main questions: - [ ] We will have 2 migrations for user-secrets: - initial migration (in current PR) - adding `value_key_id` in dbcrypt PR - is this approach reasonable? - [ ] Are changes to roles's permissions are correct? - [ ] Are changes in roles_test.go are correct? --------- Co-authored-by: Steven Masley <Emyrk@users.noreply.github.com>
1 parent 34c46c0 commit c65996a

28 files changed

+913
-3
lines changed

coderd/apidoc/docs.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbauthz/dbauthz.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,14 @@ func (q *querier) CountUnreadInboxNotificationsByUserID(ctx context.Context, use
13871387
return q.db.CountUnreadInboxNotificationsByUserID(ctx, userID)
13881388
}
13891389

1390+
func (q *querier) CreateUserSecret(ctx context.Context, arg database.CreateUserSecretParams) (database.UserSecret, error) {
1391+
obj := rbac.ResourceUserSecret.WithOwner(arg.UserID.String())
1392+
if err := q.authorizeContext(ctx, policy.ActionCreate, obj); err != nil {
1393+
return database.UserSecret{}, err
1394+
}
1395+
return q.db.CreateUserSecret(ctx, arg)
1396+
}
1397+
13901398
// TODO: Handle org scoped lookups
13911399
func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
13921400
roleObject := rbac.ResourceAssignRole
@@ -1657,6 +1665,19 @@ func (q *querier) DeleteTailnetTunnel(ctx context.Context, arg database.DeleteTa
16571665
return q.db.DeleteTailnetTunnel(ctx, arg)
16581666
}
16591667

1668+
func (q *querier) DeleteUserSecret(ctx context.Context, id uuid.UUID) error {
1669+
// First get the secret to check ownership
1670+
secret, err := q.GetUserSecret(ctx, id)
1671+
if err != nil {
1672+
return err
1673+
}
1674+
1675+
if err := q.authorizeContext(ctx, policy.ActionDelete, secret); err != nil {
1676+
return err
1677+
}
1678+
return q.db.DeleteUserSecret(ctx, id)
1679+
}
1680+
16601681
func (q *querier) DeleteWebpushSubscriptionByUserIDAndEndpoint(ctx context.Context, arg database.DeleteWebpushSubscriptionByUserIDAndEndpointParams) error {
16611682
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceWebpushSubscription.WithOwner(arg.UserID.String())); err != nil {
16621683
return err
@@ -3075,6 +3096,28 @@ func (q *querier) GetUserNotificationPreferences(ctx context.Context, userID uui
30753096
return q.db.GetUserNotificationPreferences(ctx, userID)
30763097
}
30773098

3099+
func (q *querier) GetUserSecret(ctx context.Context, id uuid.UUID) (database.UserSecret, error) {
3100+
// First get the secret to check ownership
3101+
secret, err := q.db.GetUserSecret(ctx, id)
3102+
if err != nil {
3103+
return database.UserSecret{}, err
3104+
}
3105+
3106+
if err := q.authorizeContext(ctx, policy.ActionRead, secret); err != nil {
3107+
return database.UserSecret{}, err
3108+
}
3109+
return secret, nil
3110+
}
3111+
3112+
func (q *querier) GetUserSecretByUserIDAndName(ctx context.Context, arg database.GetUserSecretByUserIDAndNameParams) (database.UserSecret, error) {
3113+
obj := rbac.ResourceUserSecret.WithOwner(arg.UserID.String())
3114+
if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil {
3115+
return database.UserSecret{}, err
3116+
}
3117+
3118+
return q.db.GetUserSecretByUserIDAndName(ctx, arg)
3119+
}
3120+
30783121
func (q *querier) GetUserStatusCounts(ctx context.Context, arg database.GetUserStatusCountsParams) ([]database.GetUserStatusCountsRow, error) {
30793122
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
30803123
return nil, err
@@ -4153,6 +4196,14 @@ func (q *querier) ListProvisionerKeysByOrganizationExcludeReserved(ctx context.C
41534196
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.ListProvisionerKeysByOrganizationExcludeReserved)(ctx, organizationID)
41544197
}
41554198

4199+
func (q *querier) ListUserSecrets(ctx context.Context, userID uuid.UUID) ([]database.UserSecret, error) {
4200+
obj := rbac.ResourceUserSecret.WithOwner(userID.String())
4201+
if err := q.authorizeContext(ctx, policy.ActionRead, obj); err != nil {
4202+
return nil, err
4203+
}
4204+
return q.db.ListUserSecrets(ctx, userID)
4205+
}
4206+
41564207
func (q *querier) ListWorkspaceAgentPortShares(ctx context.Context, workspaceID uuid.UUID) ([]database.WorkspaceAgentPortShare, error) {
41574208
workspace, err := q.db.GetWorkspaceByID(ctx, workspaceID)
41584209
if err != nil {
@@ -4866,6 +4917,19 @@ func (q *querier) UpdateUserRoles(ctx context.Context, arg database.UpdateUserRo
48664917
return q.db.UpdateUserRoles(ctx, arg)
48674918
}
48684919

4920+
func (q *querier) UpdateUserSecret(ctx context.Context, arg database.UpdateUserSecretParams) (database.UserSecret, error) {
4921+
// First get the secret to check ownership
4922+
secret, err := q.db.GetUserSecret(ctx, arg.ID)
4923+
if err != nil {
4924+
return database.UserSecret{}, err
4925+
}
4926+
4927+
if err := q.authorizeContext(ctx, policy.ActionUpdate, secret); err != nil {
4928+
return database.UserSecret{}, err
4929+
}
4930+
return q.db.UpdateUserSecret(ctx, arg)
4931+
}
4932+
48694933
func (q *querier) UpdateUserStatus(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
48704934
fetch := func(ctx context.Context, arg database.UpdateUserStatusParams) (database.User, error) {
48714935
return q.db.GetUserByID(ctx, arg.ID)

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5883,3 +5883,64 @@ func (s *MethodTestSuite) TestAuthorizePrebuiltWorkspace() {
58835883
}).Asserts(w, policy.ActionUpdate, w.AsPrebuild(), policy.ActionUpdate)
58845884
}))
58855885
}
5886+
5887+
func (s *MethodTestSuite) TestUserSecrets() {
5888+
s.Run("GetUserSecretByUserIDAndName", s.Subtest(func(db database.Store, check *expects) {
5889+
user := dbgen.User(s.T(), db, database.User{})
5890+
userSecret := dbgen.UserSecret(s.T(), db, database.UserSecret{
5891+
UserID: user.ID,
5892+
})
5893+
arg := database.GetUserSecretByUserIDAndNameParams{
5894+
UserID: user.ID,
5895+
Name: userSecret.Name,
5896+
}
5897+
check.Args(arg).
5898+
Asserts(rbac.ResourceUserSecret.WithOwner(arg.UserID.String()), policy.ActionRead).
5899+
Returns(userSecret)
5900+
}))
5901+
s.Run("GetUserSecret", s.Subtest(func(db database.Store, check *expects) {
5902+
user := dbgen.User(s.T(), db, database.User{})
5903+
userSecret := dbgen.UserSecret(s.T(), db, database.UserSecret{
5904+
UserID: user.ID,
5905+
})
5906+
check.Args(userSecret.ID).
5907+
Asserts(userSecret, policy.ActionRead).
5908+
Returns(userSecret)
5909+
}))
5910+
s.Run("ListUserSecrets", s.Subtest(func(db database.Store, check *expects) {
5911+
user := dbgen.User(s.T(), db, database.User{})
5912+
userSecret := dbgen.UserSecret(s.T(), db, database.UserSecret{
5913+
UserID: user.ID,
5914+
})
5915+
check.Args(user.ID).
5916+
Asserts(rbac.ResourceUserSecret.WithOwner(user.ID.String()), policy.ActionRead).
5917+
Returns([]database.UserSecret{userSecret})
5918+
}))
5919+
s.Run("CreateUserSecret", s.Subtest(func(db database.Store, check *expects) {
5920+
user := dbgen.User(s.T(), db, database.User{})
5921+
arg := database.CreateUserSecretParams{
5922+
UserID: user.ID,
5923+
}
5924+
check.Args(arg).
5925+
Asserts(rbac.ResourceUserSecret.WithOwner(arg.UserID.String()), policy.ActionCreate)
5926+
}))
5927+
s.Run("UpdateUserSecret", s.Subtest(func(db database.Store, check *expects) {
5928+
user := dbgen.User(s.T(), db, database.User{})
5929+
userSecret := dbgen.UserSecret(s.T(), db, database.UserSecret{
5930+
UserID: user.ID,
5931+
})
5932+
arg := database.UpdateUserSecretParams{
5933+
ID: userSecret.ID,
5934+
}
5935+
check.Args(arg).
5936+
Asserts(userSecret, policy.ActionUpdate)
5937+
}))
5938+
s.Run("DeleteUserSecret", s.Subtest(func(db database.Store, check *expects) {
5939+
user := dbgen.User(s.T(), db, database.User{})
5940+
userSecret := dbgen.UserSecret(s.T(), db, database.UserSecret{
5941+
UserID: user.ID,
5942+
})
5943+
check.Args(userSecret.ID).
5944+
Asserts(userSecret, policy.ActionRead, userSecret, policy.ActionDelete)
5945+
}))
5946+
}

coderd/database/dbgen/dbgen.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,6 +1422,20 @@ func PresetParameter(t testing.TB, db database.Store, seed database.InsertPreset
14221422
return parameters
14231423
}
14241424

1425+
func UserSecret(t testing.TB, db database.Store, seed database.UserSecret) database.UserSecret {
1426+
userSecret, err := db.CreateUserSecret(genCtx, database.CreateUserSecretParams{
1427+
ID: takeFirst(seed.ID, uuid.New()),
1428+
UserID: takeFirst(seed.UserID, uuid.New()),
1429+
Name: takeFirst(seed.Name, "secret-name"),
1430+
Description: takeFirst(seed.Description, "secret description"),
1431+
Value: takeFirst(seed.Value, "secret value"),
1432+
EnvName: takeFirst(seed.EnvName, "SECRET_ENV_NAME"),
1433+
FilePath: takeFirst(seed.FilePath, "~/secret/file/path"),
1434+
})
1435+
require.NoError(t, err, "failed to insert user secret")
1436+
return userSecret
1437+
}
1438+
14251439
func ClaimPrebuild(t testing.TB, db database.Store, newUserID uuid.UUID, newName string, presetID uuid.UUID) database.ClaimPrebuiltWorkspaceRow {
14261440
claimedWorkspace, err := db.ClaimPrebuiltWorkspace(genCtx, database.ClaimPrebuiltWorkspaceParams{
14271441
NewUserID: newUserID,

coderd/database/dbmetrics/querymetrics.go

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)