@@ -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
156193func 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
172209func 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. \n Do 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