Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitcf1fcab

Browse files
authored
feat: notify about created user account (#14010)
1 parentc6fb779 commitcf1fcab

File tree

9 files changed

+177
-18
lines changed

9 files changed

+177
-18
lines changed

‎coderd/autobuild/lifecycle_executor_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,13 +1115,14 @@ func TestNotifications(t *testing.T) {
11151115
require.NotNil(t,workspace.DormantAt)
11161116

11171117
// Check that a notification was enqueued
1118-
require.Len(t,notifyEnq.Sent,1)
1119-
require.Equal(t,notifyEnq.Sent[0].UserID,workspace.OwnerID)
1120-
require.Equal(t,notifyEnq.Sent[0].TemplateID,notifications.TemplateWorkspaceDormant)
1121-
require.Contains(t,notifyEnq.Sent[0].Targets,template.ID)
1122-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.ID)
1123-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OrganizationID)
1124-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OwnerID)
1118+
require.Len(t,notifyEnq.Sent,2)
1119+
// notifyEnq.Sent[0] is an event for created user account
1120+
require.Equal(t,notifyEnq.Sent[1].UserID,workspace.OwnerID)
1121+
require.Equal(t,notifyEnq.Sent[1].TemplateID,notifications.TemplateWorkspaceDormant)
1122+
require.Contains(t,notifyEnq.Sent[1].Targets,template.ID)
1123+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.ID)
1124+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.OrganizationID)
1125+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.OwnerID)
11251126
})
11261127
}
11271128

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DELETEFROM notification_templatesWHERE id='4e19c0ac-94e1-4532-9515-d1801aa283b2';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
INSERT INTO notification_templates (id, name, title_template, body_template,"group", actions)
2+
VALUES ('4e19c0ac-94e1-4532-9515-d1801aa283b2','User account created', E'User account "{{.Labels.created_account_name}}" created',
3+
E'Hi {{.UserName}},\n\New user account **{{.Labels.created_account_name}}** has been created.',
4+
'Workspace Events','[
5+
{
6+
"label": "View accounts",
7+
"url": "{{ base_url }}/deployment/users?filter=status%3Aactive"
8+
}
9+
]'::jsonb);

‎coderd/notifications/events.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ var (
1313
TemplateWorkspaceAutoUpdated=uuid.MustParse("c34a0c09-0704-4cac-bd1c-0c0146811c2b")
1414
TemplateWorkspaceMarkedForDeletion=uuid.MustParse("51ce2fdf-c9ca-4be1-8d70-628674f9bc42")
1515
)
16+
17+
// Account-related events.
18+
var (
19+
TemplateUserAccountCreated=uuid.MustParse("4e19c0ac-94e1-4532-9515-d1801aa283b2")
20+
)

‎coderd/users.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"github.com/google/uuid"
1313
"golang.org/x/xerrors"
1414

15+
"cdr.dev/slog"
16+
1517
"github.com/coder/coder/v2/coderd/audit"
1618
"github.com/coder/coder/v2/coderd/database"
1719
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -20,6 +22,7 @@ import (
2022
"github.com/coder/coder/v2/coderd/gitsshkey"
2123
"github.com/coder/coder/v2/coderd/httpapi"
2224
"github.com/coder/coder/v2/coderd/httpmw"
25+
"github.com/coder/coder/v2/coderd/notifications"
2326
"github.com/coder/coder/v2/coderd/rbac"
2427
"github.com/coder/coder/v2/coderd/rbac/policy"
2528
"github.com/coder/coder/v2/coderd/searchquery"
@@ -1200,7 +1203,8 @@ func (api *API) organizationByUserAndName(rw http.ResponseWriter, r *http.Reques
12001203

12011204
typeCreateUserRequeststruct {
12021205
codersdk.CreateUserRequest
1203-
LoginType database.LoginType
1206+
LoginType database.LoginType
1207+
SkipNotificationsbool
12041208
}
12051209

12061210
func (api*API)CreateUser(ctx context.Context,store database.Store,reqCreateUserRequest) (database.User, uuid.UUID,error) {
@@ -1211,7 +1215,7 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
12111215
}
12121216

12131217
varuser database.User
1214-
returnuser,req.OrganizationID,store.InTx(func(tx database.Store)error {
1218+
err:=store.InTx(func(tx database.Store)error {
12151219
orgRoles:=make([]string,0)
12161220
// Organization is required to know where to allocate the user.
12171221
ifreq.OrganizationID==uuid.Nil {
@@ -1272,6 +1276,37 @@ func (api *API) CreateUser(ctx context.Context, store database.Store, req Create
12721276
}
12731277
returnnil
12741278
},nil)
1279+
iferr!=nil||req.SkipNotifications {
1280+
returnuser,req.OrganizationID,err
1281+
}
1282+
1283+
// Notify all users with user admin permission including owners
1284+
// Notice: we can't scrape the user information in parallel as pq
1285+
// fails with: unexpected describe rows response: 'D'
1286+
owners,err:=store.GetUsers(ctx, database.GetUsersParams{
1287+
RbacRole: []string{codersdk.RoleOwner},
1288+
})
1289+
iferr!=nil {
1290+
returnuser,req.OrganizationID,xerrors.Errorf("get owners: %w",err)
1291+
}
1292+
userAdmins,err:=store.GetUsers(ctx, database.GetUsersParams{
1293+
RbacRole: []string{codersdk.RoleUserAdmin},
1294+
})
1295+
iferr!=nil {
1296+
returnuser,req.OrganizationID,xerrors.Errorf("get user admins: %w",err)
1297+
}
1298+
1299+
for_,u:=rangeappend(owners,userAdmins...) {
1300+
if_,err:=api.NotificationsEnqueuer.Enqueue(ctx,u.ID,notifications.TemplateUserAccountCreated,
1301+
map[string]string{
1302+
"created_account_name":user.Username,
1303+
},"api-users-create",
1304+
user.ID,
1305+
);err!=nil {
1306+
api.Logger.Warn(ctx,"unable to notify about created user",slog.F("created_user",user.Username),slog.Error(err))
1307+
}
1308+
}
1309+
returnuser,req.OrganizationID,err
12751310
}
12761311

12771312
funcconvertUsers(users []database.User,organizationIDsByUserIDmap[uuid.UUID][]uuid.UUID) []codersdk.User {

‎coderd/users_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/coder/coder/v2/coderd"
1212
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
13+
"github.com/coder/coder/v2/coderd/notifications"
1314
"github.com/coder/coder/v2/coderd/rbac/policy"
1415
"github.com/coder/serpent"
1516

@@ -598,6 +599,99 @@ func TestPostUsers(t *testing.T) {
598599
})
599600
}
600601

602+
funcTestNotifyCreatedUser(t*testing.T) {
603+
t.Parallel()
604+
605+
t.Run("OwnerNotified",func(t*testing.T) {
606+
t.Parallel()
607+
608+
// given
609+
notifyEnq:=&testutil.FakeNotificationsEnqueuer{}
610+
adminClient:=coderdtest.New(t,&coderdtest.Options{
611+
NotificationsEnqueuer:notifyEnq,
612+
})
613+
firstUser:=coderdtest.CreateFirstUser(t,adminClient)
614+
615+
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
616+
defercancel()
617+
618+
// when
619+
user,err:=adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
620+
OrganizationID:firstUser.OrganizationID,
621+
Email:"another@user.org",
622+
Username:"someone-else",
623+
Password:"SomeSecurePassword!",
624+
})
625+
require.NoError(t,err)
626+
627+
// then
628+
require.Len(t,notifyEnq.Sent,1)
629+
require.Equal(t,notifications.TemplateUserAccountCreated,notifyEnq.Sent[0].TemplateID)
630+
require.Equal(t,firstUser.UserID,notifyEnq.Sent[0].UserID)
631+
require.Contains(t,notifyEnq.Sent[0].Targets,user.ID)
632+
require.Equal(t,user.Username,notifyEnq.Sent[0].Labels["created_account_name"])
633+
})
634+
635+
t.Run("UserAdminNotified",func(t*testing.T) {
636+
t.Parallel()
637+
638+
// given
639+
notifyEnq:=&testutil.FakeNotificationsEnqueuer{}
640+
adminClient:=coderdtest.New(t,&coderdtest.Options{
641+
NotificationsEnqueuer:notifyEnq,
642+
})
643+
firstUser:=coderdtest.CreateFirstUser(t,adminClient)
644+
645+
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
646+
defercancel()
647+
648+
userAdmin,err:=adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
649+
OrganizationID:firstUser.OrganizationID,
650+
Email:"user-admin@user.org",
651+
Username:"mr-user-admin",
652+
Password:"SomeSecurePassword!",
653+
})
654+
require.NoError(t,err)
655+
656+
_,err=adminClient.UpdateUserRoles(ctx,userAdmin.Username, codersdk.UpdateRoles{
657+
Roles: []string{
658+
rbac.RoleUserAdmin().String(),
659+
},
660+
})
661+
require.NoError(t,err)
662+
663+
// when
664+
member,err:=adminClient.CreateUser(ctx, codersdk.CreateUserRequest{
665+
OrganizationID:firstUser.OrganizationID,
666+
Email:"another@user.org",
667+
Username:"someone-else",
668+
Password:"SomeSecurePassword!",
669+
})
670+
require.NoError(t,err)
671+
672+
// then
673+
require.Len(t,notifyEnq.Sent,3)
674+
675+
// "User admin" account created, "owner" notified
676+
require.Equal(t,notifications.TemplateUserAccountCreated,notifyEnq.Sent[0].TemplateID)
677+
require.Equal(t,firstUser.UserID,notifyEnq.Sent[0].UserID)
678+
require.Contains(t,notifyEnq.Sent[0].Targets,userAdmin.ID)
679+
require.Equal(t,userAdmin.Username,notifyEnq.Sent[0].Labels["created_account_name"])
680+
681+
// "Member" account created, "owner" notified
682+
require.Equal(t,notifications.TemplateUserAccountCreated,notifyEnq.Sent[1].TemplateID)
683+
require.Equal(t,firstUser.UserID,notifyEnq.Sent[1].UserID)
684+
require.Contains(t,notifyEnq.Sent[1].Targets,member.ID)
685+
require.Equal(t,member.Username,notifyEnq.Sent[1].Labels["created_account_name"])
686+
687+
// "Member" account created, "user admin" notified
688+
require.Equal(t,notifications.TemplateUserAccountCreated,notifyEnq.Sent[1].TemplateID)
689+
require.Equal(t,userAdmin.ID,notifyEnq.Sent[2].UserID)
690+
require.Contains(t,notifyEnq.Sent[2].Targets,member.ID)
691+
require.Equal(t,member.Username,notifyEnq.Sent[2].Labels["created_account_name"])
692+
})
693+
}
694+
601695
funcTestUpdateUserProfile(t*testing.T) {
602696
t.Parallel()
603697
t.Run("UserNotFound",func(t*testing.T) {

‎coderd/workspaces_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3476,13 +3476,14 @@ func TestNotifications(t *testing.T) {
34763476

34773477
// Then
34783478
require.NoError(t,err,"mark workspace as dormant")
3479-
require.Len(t,notifyEnq.Sent,1)
3480-
require.Equal(t,notifyEnq.Sent[0].TemplateID,notifications.TemplateWorkspaceDormant)
3481-
require.Equal(t,notifyEnq.Sent[0].UserID,workspace.OwnerID)
3482-
require.Contains(t,notifyEnq.Sent[0].Targets,template.ID)
3483-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.ID)
3484-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OrganizationID)
3485-
require.Contains(t,notifyEnq.Sent[0].Targets,workspace.OwnerID)
3479+
require.Len(t,notifyEnq.Sent,2)
3480+
// notifyEnq.Sent[0] is an event for created user account
3481+
require.Equal(t,notifyEnq.Sent[1].TemplateID,notifications.TemplateWorkspaceDormant)
3482+
require.Equal(t,notifyEnq.Sent[1].UserID,workspace.OwnerID)
3483+
require.Contains(t,notifyEnq.Sent[1].Targets,template.ID)
3484+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.ID)
3485+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.OrganizationID)
3486+
require.Contains(t,notifyEnq.Sent[1].Targets,workspace.OwnerID)
34863487
})
34873488

34883489
t.Run("InitiatorIsOwner",func(t*testing.T) {

‎enterprise/coderd/scim.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ func (api *API) scimPostUser(rw http.ResponseWriter, r *http.Request) {
239239
OrganizationID:defaultOrganization.ID,
240240
},
241241
LoginType:database.LoginTypeOIDC,
242+
// Do not send notifications to user admins as SCIM endpoint might be called sequentially to all users.
243+
SkipNotifications:true,
242244
})
243245
iferr!=nil {
244246
_=handlerutil.WriteError(rw,err)

‎enterprise/coderd/scim_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,15 @@ func TestScim(t *testing.T) {
113113
ctx,cancel:=context.WithTimeout(context.Background(),testutil.WaitLong)
114114
defercancel()
115115

116+
// given
116117
scimAPIKey:= []byte("hi")
117118
mockAudit:=audit.NewMock()
119+
notifyEnq:=&testutil.FakeNotificationsEnqueuer{}
118120
client,_:=coderdenttest.New(t,&coderdenttest.Options{
119-
Options:&coderdtest.Options{Auditor:mockAudit},
121+
Options:&coderdtest.Options{
122+
Auditor:mockAudit,
123+
NotificationsEnqueuer:notifyEnq,
124+
},
120125
SCIMAPIKey:scimAPIKey,
121126
AuditLogging:true,
122127
LicenseOptions:&coderdenttest.LicenseOptions{
@@ -129,12 +134,15 @@ func TestScim(t *testing.T) {
129134
})
130135
mockAudit.ResetLogs()
131136

137+
// when
132138
sUser:=makeScimUser(t)
133139
res,err:=client.Request(ctx,"POST","/scim/v2/Users",sUser,setScimAuth(scimAPIKey))
134140
require.NoError(t,err)
135141
deferres.Body.Close()
136142
require.Equal(t,http.StatusOK,res.StatusCode)
137143

144+
// then
145+
// Expect audit logs
138146
aLogs:=mockAudit.AuditLogs()
139147
require.Len(t,aLogs,1)
140148
af:=map[string]string{}
@@ -143,12 +151,15 @@ func TestScim(t *testing.T) {
143151
assert.Equal(t,coderd.SCIMAuditAdditionalFields,af)
144152
assert.Equal(t,database.AuditActionCreate,aLogs[0].Action)
145153

154+
// Expect users exposed over API
146155
userRes,err:=client.Users(ctx, codersdk.UsersRequest{Search:sUser.Emails[0].Value})
147156
require.NoError(t,err)
148157
require.Len(t,userRes.Users,1)
149-
150158
assert.Equal(t,sUser.Emails[0].Value,userRes.Users[0].Email)
151159
assert.Equal(t,sUser.UserName,userRes.Users[0].Username)
160+
161+
// Expect zero notifications (SkipNotifications = true)
162+
require.Empty(t,notifyEnq.Sent)
152163
})
153164

154165
t.Run("Duplicate",func(t*testing.T) {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp