Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e6837ef
register users clean
DaedalusG Aug 7, 2022
4104aae
added usage statistics to User Node type
DaedalusG Aug 7, 2022
5bb1f27
mark
DaedalusG Aug 8, 2022
74a3d43
define worker function for user removal
DaedalusG Aug 8, 2022
3b0aa52
before implementing time.Parse(time.RFC3339, payload.UsageStatistics.…
DaedalusG Aug 8, 2022
c8b390c
now computes time since last active
DaedalusG Aug 9, 2022
8941c36
added utility function structure
DaedalusG Aug 9, 2022
3a5d5c5
initialize array to store users to be deleted
DaedalusG Aug 10, 2022
25301b8
working query to remove users and flags
DaedalusG Aug 12, 2022
c6d6bc8
added a user verification to command
DaedalusG Aug 18, 2022
a4cfba4
corrects logic around removeNeverActive flag
DaedalusG Aug 18, 2022
1093e14
better warning messaging
DaedalusG Aug 18, 2022
7f399eb
formating warning
DaedalusG Aug 18, 2022
febdc29
add flag to skip verify check
DaedalusG Aug 24, 2022
9fad30c
commented out placeholder code and added TODO comments
DaedalusG Aug 24, 2022
7a6ea3a
addressed many review concerns, added lower bound on -days flag, made…
DaedalusG Sep 1, 2022
e11fede
Update cmd/src/users.go
DaedalusG Sep 1, 2022
be8a1c5
Update cmd/src/users_clean.go
DaedalusG Sep 1, 2022
01300f6
Update cmd/src/users_clean.go
DaedalusG Sep 1, 2022
c642024
correct variable naming bug
DaedalusG Sep 1, 2022
918e36f
remove unused params in get users query
DaedalusG Sep 1, 2022
b396f32
Update cmd/src/users.go
DaedalusG Sep 8, 2022
d170645
admins must be explcitly removed
DaedalusG Sep 8, 2022
3065b9f
commit dependencies
DaedalusG Sep 8, 2022
010ff3b
camel case
DaedalusG Sep 8, 2022
817d78e
ensure clean doesnt clean the user issuing the command
DaedalusG Sep 8, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions cmd/src/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The commands are:
get gets a user
create creates a user account
delete deletes a user account
clean deletes inactive users
tag add/remove a tag on a user

Use "src users [command] -h" for more information about a command.
Expand Down Expand Up @@ -57,7 +58,11 @@ fragment UserFields on User {
}
emails {
email
verified
verified
}
usageStatistics {
lastActiveTime
lastActiveCodeHostIntegrationTime
}
url
}
Expand All @@ -71,11 +76,17 @@ type User struct {
Organizations struct {
Nodes []Org
}
Emails []UserEmail
URL string
Emails []UserEmail
UsageStatistics UserUsageStatistics
URL string
}

type UserEmail struct {
Email string
Verified bool
}

type UserUsageStatistics struct {
LastActiveTime string
LastActiveCodeHostIntegrationTime string
}
212 changes: 212 additions & 0 deletions cmd/src/users_clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package main

import (
"context"
"flag"
"fmt"
"os"
"strings"
"time"

"github.com/jedib0t/go-pretty/v6/table"

"github.com/sourcegraph/src-cli/internal/api"
)

func init() {
usage := `
This command removes users from a Sourcegraph instance who have been inactive for 60 or more days. Admin accounts are omitted by default.

Examples:

$ src users clean -days 182

$ src users clean -remove-admin -remove-never-active
`

flagSet := flag.NewFlagSet("clean", flag.ExitOnError)
usageFunc := func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name())
flagSet.PrintDefaults()
fmt.Println(usage)
}
var (
daysToDelete = flagSet.Int("days", 60, "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ")
removeAdmin = flagSet.Bool("remove-admin", false, "clean admin accounts")
removeNoLastActive = flagSet.Bool("remove-never-active", false, "removes users with null lastActive value")
skipConfirmation = flagSet.Bool("force", false, "skips user confirmation step allowing programmatic use")
apiFlags = api.NewFlags(flagSet)
)

handler := func(args []string) error {
if err := flagSet.Parse(args); err != nil {
return err
}
if *daysToDelete < 60 {
fmt.Println("-days flag must be set to 60 or greater")
return nil
}

ctx := context.Background()
client := cfg.apiClient(apiFlags, flagSet.Output())

currentUserQuery := `
query {
currentUser {
username
}
}
`
var currentUserResult struct {
Data struct {
CurrentUser struct {
Username string
}
}
}
if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(currentUserQuery, nil).DoRaw(context.Background(), &currentUserResult); err != nil || !ok {
return err
}
fmt.Println(currentUserResult)

usersQuery := `
query Users() {
users() {
nodes {
...UserFields
}
}
}
` + userFragment

// get users to delete
var usersResult struct {
Users struct {
Nodes []User
}
}
if ok, err := client.NewRequest(usersQuery, nil).Do(ctx, &usersResult); err != nil || !ok {
return err
}
fmt.Println(usersResult)

usersToDelete := make([]UserToDelete, 0)
for _, user := range usersResult.Users.Nodes {
daysSinceLastUse, wasLastActive, err := computeDaysSinceLastUse(user)
if err != nil {
return err
}
// never remove user issuing command
if user.Username == currentUserResult.Data.CurrentUser.Username {
continue
}
if !wasLastActive && !*removeNoLastActive {
continue
}
if !*removeAdmin && user.SiteAdmin {
continue
}
if daysSinceLastUse <= *daysToDelete && wasLastActive {
continue
}
deleteUser := UserToDelete{user, daysSinceLastUse}

usersToDelete = append(usersToDelete, deleteUser)
}

if *skipConfirmation {
for _, user := range usersToDelete {
if err := removeUser(user.User, client, ctx); err != nil {
return err
}
}
return nil
}

// confirm and remove users
if confirmed, _ := confirmUserRemoval(usersToDelete); !confirmed {
fmt.Println("Aborting removal")
return nil
} else {
fmt.Println("REMOVING USERS")
for _, user := range usersToDelete {
if err := removeUser(user.User, client, ctx); err != nil {
return err
}
}
}

return nil
}

// Register the command.
usersCommands = append(usersCommands, &command{
flagSet: flagSet,
handler: handler,
usageFunc: usageFunc,
})
}

// computes days since last usage from current day and time and UsageStatistics.LastActiveTime, uses time.Parse
func computeDaysSinceLastUse(user User) (timeDiff int, wasLastActive bool, _ error) {
// handle for null lastActiveTime returned from
if user.UsageStatistics.LastActiveTime == "" {
wasLastActive = false
return 0, wasLastActive, nil
}
timeLast, err := time.Parse(time.RFC3339, user.UsageStatistics.LastActiveTime)
if err != nil {
return 0, false, err
}
timeDiff = int(time.Since(timeLast).Hours() / 24)

return timeDiff, true, err
}

// Issue graphQL api request to remove user
func removeUser(user User, client api.Client, ctx context.Context) error {
query := `mutation DeleteUser($user: ID!) {
deleteUser(user: $user) {
alwaysNil
}
}`
vars := map[string]interface{}{
"user": user.ID,
}
if ok, err := client.NewRequest(query, vars).Do(ctx, nil); err != nil || !ok {
return err
}
return nil
}

type UserToDelete struct {
User User
DaysSinceLastUse int
}

// Verify user wants to remove users with table of users and a command prompt for [y/N]
func confirmUserRemoval(usersToRemove []UserToDelete) (bool, error) {
fmt.Printf("Users to remove from instance at %s\n", cfg.Endpoint)
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Username", "Email", "Days Since Last Active"})
for _, user := range usersToRemove {
if len(user.User.Emails) > 0 {
t.AppendRow([]interface{}{user.User.Username, user.User.Emails[0].Email, user.DaysSinceLastUse})
t.AppendSeparator()
} else {
t.AppendRow([]interface{}{user.User.Username, "", user.DaysSinceLastUse})
t.AppendSeparator()
}
}
t.SetStyle(table.StyleRounded)
t.Render()
input := ""
for strings.ToLower(input) != "y" && strings.ToLower(input) != "n" {
fmt.Printf("Do you wish to proceed with user removal [y/N]: ")
if _, err := fmt.Scanln(&input); err != nil {
return false, err
}
}
return strings.ToLower(input) == "y", nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/google/go-cmp v0.5.8
github.com/grafana/regexp v0.0.0-20220304100321-149c8afcd6cb
github.com/hexops/autogold v1.3.0
github.com/jedib0t/go-pretty/v6 v6.3.7
github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05
github.com/json-iterator/go v1.1.12
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,8 @@ github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0Gqw
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a h1:d4+I1YEKVmWZrgkt6jpXBnLgV2ZjO0YxEtLDdfIZfH4=
github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a/go.mod h1:Zi/ZFkEqFHTm7qkjyNJjaWH4LQA9LQhGJyF0lTYGpxw=
github.com/jedib0t/go-pretty/v6 v6.3.7 h1:H3Ulkf7h6A+p0HgKBGzgDn0bZIupRbKKWF4pO4Bs7iA=
github.com/jedib0t/go-pretty/v6 v6.3.7/go.mod h1:MgmISkTWDSFu0xOqiZ0mKNntMQ2mDgOcwOkwBEkMDJI=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
Expand Down Expand Up @@ -396,6 +398,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
Expand Down