Skip to content

Commit 2a63033

Browse files
authored
users prune - Paginate graphql site users - fix bug for soft deleted users (#909)
* fixes bug in which request is made to delete users that have already been deleted but exist in the aggregated_user_stats table * implemented some pagination -- totalCount of nodes is always 0 -- needing to fix that for break condition * fixed totalCount issue -- switched DoRaw methods to Do * remove development print statements; hide users table behind flag; improve comments * gofmt * added a few more comments * style improvements * better graphQL querry formatting for single use queries
1 parent 0ffe3d3 commit 2a63033

File tree

2 files changed

+93
-57
lines changed

2 files changed

+93
-57
lines changed

cmd/src/users.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,5 @@ type SiteUser struct {
9797
Email string
9898
SiteAdmin bool
9999
LastActiveAt string
100+
DeletedAt string
100101
}

cmd/src/users_prune.go

Lines changed: 92 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ Examples:
3131
fmt.Println(usage)
3232
}
3333
var (
34-
daysToDelete = flagSet.Int("days", 60, "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ")
35-
removeAdmin = flagSet.Bool("remove-admin", false, "prune admin accounts")
36-
removeNoLastActive = flagSet.Bool("remove-null-users", false, "removes users with no last active value")
37-
skipConfirmation = flagSet.Bool("force", false, "skips user confirmation step allowing programmatic use")
38-
apiFlags = api.NewFlags(flagSet)
34+
daysToDelete = flagSet.Int("days", 60, "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ")
35+
removeAdmin = flagSet.Bool("remove-admin", false, "prune admin accounts")
36+
removeNoLastActive = flagSet.Bool("remove-null-users", false, "removes users with no last active value")
37+
skipConfirmation = flagSet.Bool("force", false, "skips user confirmation step allowing programmatic use")
38+
displayUsersToDelete = flagSet.Bool("display-users", false, "display table of users to be deleted by prune")
39+
apiFlags = api.NewFlags(flagSet)
3940
)
4041

4142
handler := func(args []string) error {
@@ -50,73 +51,109 @@ Examples:
5051
ctx := context.Background()
5152
client := cfg.apiClient(apiFlags, flagSet.Output())
5253

53-
currentUserQuery := `
54-
query getCurrentUser {
55-
currentUser {
56-
username
57-
}
58-
}
59-
`
54+
// get current user so as not to delete issuer of the prune request
55+
currentUserQuery := `query getCurrentUser { currentUser { username }}`
6056
var currentUserResult struct {
61-
Data struct {
62-
CurrentUser struct {
63-
Username string
57+
CurrentUser struct {
58+
Username string
59+
}
60+
}
61+
if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(currentUserQuery, nil).Do(context.Background(), &currentUserResult); err != nil || !ok {
62+
return err
63+
}
64+
65+
// get total users to paginate over
66+
totalUsersQuery := `query getTotalUsers { site { users { totalCount }}}`
67+
var totalUsers struct {
68+
Site struct {
69+
Users struct {
70+
TotalCount float64
6471
}
6572
}
6673
}
67-
if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(currentUserQuery, nil).DoRaw(context.Background(), &currentUserResult); err != nil || !ok {
74+
if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(totalUsersQuery, nil).Do(context.Background(), &totalUsers); err != nil || !ok {
6875
return err
6976
}
7077

78+
// get 100 site users
7179
getInactiveUsersQuery := `
72-
query getInactiveUsers {
80+
query getInactiveUsers($limit: Int $offset: Int) {
7381
site {
7482
users {
75-
nodes {
83+
nodes (limit: $limit offset: $offset) {
84+
id
7685
username
7786
email
7887
siteAdmin
7988
lastActiveAt
89+
deletedAt
8090
}
8191
}
8292
}
8393
}
8494
`
8595

86-
var usersResult struct {
87-
Site struct {
88-
Users struct {
89-
Nodes []SiteUser
90-
}
96+
// paginate through users
97+
var aggregatedUsers []SiteUser
98+
// pagination variables, limit set to maximum possible users returned per request
99+
offset := 0
100+
const limit int = 100
101+
102+
// paginate requests until all site users have been checked -- this includes soft deleted users
103+
for len(aggregatedUsers) < int(totalUsers.Site.Users.TotalCount) {
104+
pagVars := map[string]interface{}{
105+
"offset": offset,
106+
"limit": limit,
91107
}
92-
}
93108

94-
if ok, err := client.NewRequest(getInactiveUsersQuery, nil).Do(ctx, &usersResult); err != nil || !ok {
95-
return err
109+
var usersResult struct {
110+
Site struct {
111+
Users struct {
112+
Nodes []SiteUser
113+
}
114+
TotalCount float64
115+
}
116+
}
117+
if ok, err := client.NewRequest(getInactiveUsersQuery, pagVars).Do(ctx, &usersResult); err != nil || !ok {
118+
return err
119+
}
120+
// increment graphql request offset by the length of the last user set returned
121+
offset = offset + len(usersResult.Site.Users.Nodes)
122+
// append graphql user results to aggregated users to be processed against user removal conditions
123+
aggregatedUsers = append(aggregatedUsers, usersResult.Site.Users.Nodes...)
96124
}
97125

126+
// filter users for deletion
98127
usersToDelete := make([]UserToDelete, 0)
99-
for _, user := range usersResult.Site.Users.Nodes {
128+
for _, user := range aggregatedUsers {
129+
// never remove user issuing command
130+
if user.Username == currentUserResult.CurrentUser.Username {
131+
continue
132+
}
133+
// filter out soft deleted users returned by site graphql endpoint
134+
if user.DeletedAt != "" {
135+
continue
136+
}
137+
//compute days since last use
100138
daysSinceLastUse, hasLastActive, err := computeDaysSinceLastUse(user)
101139
if err != nil {
102140
return err
103141
}
104-
// never remove user issuing command
105-
if user.Username == currentUserResult.Data.CurrentUser.Username {
106-
continue
107-
}
142+
// don't remove users with no last active value unless option flag is set
108143
if !hasLastActive && !*removeNoLastActive {
109144
continue
110145
}
146+
// don't remove admins unless option flag is set
111147
if !*removeAdmin && user.SiteAdmin {
112148
continue
113149
}
150+
// remove users who have been inactive for longer than the threshold set by the -days flag
114151
if daysSinceLastUse <= *daysToDelete && hasLastActive {
115152
continue
116153
}
117-
deleteUser := UserToDelete{user, daysSinceLastUse}
118-
119-
usersToDelete = append(usersToDelete, deleteUser)
154+
// serialize user to print in table as part of confirmUserRemoval, add to delete slice
155+
userToDelete := UserToDelete{user, daysSinceLastUse}
156+
usersToDelete = append(usersToDelete, userToDelete)
120157
}
121158

122159
if *skipConfirmation {
@@ -129,7 +166,7 @@ query getInactiveUsers {
129166
}
130167

131168
// confirm and remove users
132-
if confirmed, _ := confirmUserRemoval(usersToDelete); !confirmed {
169+
if confirmed, _ := confirmUserRemoval(usersToDelete, int(totalUsers.Site.Users.TotalCount), *daysToDelete, *displayUsersToDelete); !confirmed {
133170
fmt.Println("Aborting removal")
134171
return nil
135172
} else {
@@ -152,9 +189,9 @@ query getInactiveUsers {
152189
})
153190
}
154191

155-
// computes days since last usage from current day and time and UsageStatistics.LastActiveTime, uses time.Parse
192+
// computes days since last usage from current day and time and aggregated_user_statistics.lastActiveAt, uses time.Parse
156193
func computeDaysSinceLastUse(user SiteUser) (timeDiff int, hasLastActive bool, _ error) {
157-
// handle for null LastActiveAt returned from
194+
// handle for null LastActiveAt, users who have never been active
158195
if user.LastActiveAt == "" {
159196
hasLastActive = false
160197
return 0, hasLastActive, nil
@@ -170,11 +207,7 @@ func computeDaysSinceLastUse(user SiteUser) (timeDiff int, hasLastActive bool, _
170207

171208
// Issue graphQL api request to remove user
172209
func removeUser(user SiteUser, client api.Client, ctx context.Context) error {
173-
query := `mutation DeleteUser($user: ID!) {
174-
deleteUser(user: $user) {
175-
alwaysNil
176-
}
177-
}`
210+
query := `mutation DeleteUser($user: ID!) { deleteUser(user: $user) { alwaysNil }}`
178211
vars := map[string]interface{}{
179212
"user": user.ID,
180213
}
@@ -190,25 +223,27 @@ type UserToDelete struct {
190223
}
191224

192225
// Verify user wants to remove users with table of users and a command prompt for [y/N]
193-
func confirmUserRemoval(usersToRemove []UserToDelete) (bool, error) {
194-
fmt.Printf("Users to remove from instance at %s\n", cfg.Endpoint)
195-
t := table.NewWriter()
196-
t.SetOutputMirror(os.Stdout)
197-
t.AppendHeader(table.Row{"Username", "Email", "Days Since Last Active"})
198-
for _, user := range usersToRemove {
199-
if user.User.Email != "" {
200-
t.AppendRow([]interface{}{user.User.Username, user.User.Email, user.DaysSinceLastUse})
201-
t.AppendSeparator()
202-
} else {
203-
t.AppendRow([]interface{}{user.User.Username, "", user.DaysSinceLastUse})
204-
t.AppendSeparator()
226+
func confirmUserRemoval(usersToDelete []UserToDelete, totalUsers int, daysThreshold int, displayUsers bool) (bool, error) {
227+
if displayUsers {
228+
fmt.Printf("Users to remove from %s\n", cfg.Endpoint)
229+
t := table.NewWriter()
230+
t.SetOutputMirror(os.Stdout)
231+
t.AppendHeader(table.Row{"Username", "Email", "Days Since Last Active"})
232+
for _, user := range usersToDelete {
233+
if user.User.Email != "" {
234+
t.AppendRow([]interface{}{user.User.Username, user.User.Email, user.DaysSinceLastUse})
235+
t.AppendSeparator()
236+
} else {
237+
t.AppendRow([]interface{}{user.User.Username, "", user.DaysSinceLastUse})
238+
t.AppendSeparator()
239+
}
205240
}
241+
t.SetStyle(table.StyleRounded)
242+
t.Render()
206243
}
207-
t.SetStyle(table.StyleRounded)
208-
t.Render()
209244
input := ""
210245
for strings.ToLower(input) != "y" && strings.ToLower(input) != "n" {
211-
fmt.Printf("Do you wish to proceed with user removal [y/N]: ")
246+
fmt.Printf("%v users were inactive for more than %v days on %v.\nDo you wish to proceed with user removal [y/N]: ", len(usersToDelete), daysThreshold, cfg.Endpoint)
212247
if _, err := fmt.Scanln(&input); err != nil {
213248
return false, err
214249
}

0 commit comments

Comments
 (0)