diff --git a/cernboxcop.spec b/cernboxcop.spec index 2c27f56..de2061a 100644 --- a/cernboxcop.spec +++ b/cernboxcop.spec @@ -4,7 +4,7 @@ Name: cernboxcop Summary: CERNBox cop helps the ops team to be more efficient -Version: 1.0.11 +Version: 1.0.12 Release: 1%{?dist} License: AGPLv3 BuildRoot: %{_tmppath}/%{name}-buildroot @@ -51,6 +51,8 @@ rm -rf %buildroot/ %changelog +* Wed Nov 25 2020 Ishank Arora 1.0.12 +- Add command to display list of inactive users * Tue Nov 25 2020 Hugo Gonzalez Labrador 1.0.11 - Add OTG commands * Fri Nov 20 2020 Hugo Gonzalez Labrador 1.0.10 diff --git a/cmd/accounting.go b/cmd/accounting.go index ae7a111..0f91b74 100644 --- a/cmd/accounting.go +++ b/cmd/accounting.go @@ -4,14 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/cs3org/reva/pkg/eosclient" - "github.com/dustin/go-humanize" - "github.com/leekchan/accounting" - "github.com/mitchellh/mapstructure" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/tj/go-spin" - "gopkg.in/ldap.v3" "io/ioutil" "net/http" "os" @@ -23,6 +15,15 @@ import ( "sync" "sync/atomic" "time" + + "github.com/cs3org/reva/pkg/eosclient" + "github.com/dustin/go-humanize" + "github.com/leekchan/accounting" + "github.com/mitchellh/mapstructure" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tj/go-spin" + "gopkg.in/ldap.v3" ) const FE = "CERNBox" diff --git a/cmd/root.go b/cmd/root.go index 640f17b..dc08dd8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,8 +4,12 @@ import ( "bufio" "context" "database/sql" + "encoding/json" "fmt" "io" + "io/ioutil" + "net/http" + "net/url" "os" "path" "strings" @@ -120,6 +124,39 @@ func getLDAP() *ldap.Conn { return l } +func getGrappaURLAndToken() (string, string, error) { + params := url.Values{ + "grant_type": {"client_credentials"}, + "audience": {viper.GetString("grappa_target_api")}, + } + + client := &http.Client{} + req, err := http.NewRequest("POST", viper.GetString("oidc_token_endpoint"), strings.NewReader(params.Encode())) + if err != nil { + return "", "", err + } + req.SetBasicAuth(viper.GetString("grappa_client_id"), viper.GetString("grappa_client_secret")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded; param=value") + + res, err := client.Do(req) + if err != nil { + return "", "", err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", "", err + } + + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return "", "", err + } + return viper.GetString("grappa_api_url"), result["access_token"].(string), nil +} + func getEOS(mgm string) *eosclient.Client { eosClientOpts := &eosclient.Options{ URL: mgm, diff --git a/cmd/user.go b/cmd/user.go index 7bc39b1..8e4839c 100644 --- a/cmd/user.go +++ b/cmd/user.go @@ -2,20 +2,29 @@ package cmd import ( "encoding/binary" + "encoding/json" "errors" "fmt" + "io/ioutil" + "net/http" + "os" "strconv" "strings" + "time" "github.com/go-redis/redis" "github.com/spf13/cobra" + "github.com/tj/go-spin" "gopkg.in/ldap.v3" ) +const timeFormat = "2006-01-02T15:04:05.999999" + func init() { rootCmd.AddCommand(userCmd) userCmd.AddCommand(userCheckCmd) userCmd.AddCommand(userGroupsCmd) + userCmd.AddCommand(userListInactiveCmd) } var userCmd = &cobra.Command{ @@ -44,16 +53,6 @@ var userCheckCmd = &cobra.Command{ }, } -var prettyUser = func(userInfos ...*userInfo) { - cols := []string{"Account", "Type", "Name", "Department", "Group", "Section", "Mail", "Phone"} - rows := make([][]string, 0, len(userInfos)) - for _, ui := range userInfos { - row := []string{ui.Account, ui.AccountType, ui.Name, ui.Department, ui.Group, ui.Section, ui.Mail, ui.Phone} - rows = append(rows, row) - } - pretty(cols, rows) -} - var userGroupsCmd = &cobra.Command{ Use: "groups ", Short: "Retrieves the groups the the user is member of", @@ -71,6 +70,54 @@ var userGroupsCmd = &cobra.Command{ }, } +var userListInactiveCmd = &cobra.Command{ + Use: "list-inactive ", + Short: "Lists the user accounts which have been inactive for more than a given number of days", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 1 { + exit(cmd) + } + + days, err := strconv.Atoi(strings.TrimSpace(args[0])) + if err != nil { + log.Err(err).Msgf("value for days is not an integer: %s", args[0]) + } + + apiEP, token, err := getGrappaURLAndToken() + if err != nil { + log.Err(err).Msg("error getting token") + } + + info, err := getInactiveUsers(apiEP, token, days) + if err != nil { + log.Err(err).Msg("error getting token") + } + + prettyInactiveUsers(info...) + + }, +} + +func prettyUser(userInfos ...*userInfo) { + cols := []string{"Account", "Type", "Name", "Department", "Group", "Section", "Mail", "Phone"} + rows := make([][]string, 0, len(userInfos)) + for _, ui := range userInfos { + row := []string{ui.Account, ui.AccountType, ui.Name, ui.Department, ui.Group, ui.Section, ui.Mail, ui.Phone} + rows = append(rows, row) + } + pretty(cols, rows) +} + +func prettyInactiveUsers(userInfos ...*userInfo) { + cols := []string{"Account", "Name", "Mail", "Blocking time"} + rows := make([][]string, 0, len(userInfos)) + for _, ui := range userInfos { + row := []string{ui.Account, ui.Name, ui.Mail, ui.BlockingTime.String()} + rows = append(rows, row) + } + pretty(cols, rows) +} + func getHomePath(username string) string { letter := string(username[0]) return fmt.Sprintf("/eos/user/%s/%s", letter, username) @@ -263,6 +310,137 @@ func getUserGroups(uid string) []string { return gids } +func getInactiveUsers(apiEP, token string, days int) ([]*userInfo, error) { + url := "/api/v1.0/Identity?filter=activeUser:false&field=upn&field=primaryAccountEmail&field=displayName&field=type&field=uid&field=gid&field=primaryAccountId" + users := []*userInfo{} + i := 0 + for { + u, nextPage, err := getInactiveUsersByPage(apiEP, url, token, days, i+1) + if err != nil { + return nil, err + } + i += 1 + users = append(users, u...) + url = nextPage + if nextPage == "" { + break + } + } + return users, nil +} + +func getInactiveUsersByPage(apiEP, url, token string, days, pageIndex int) ([]*userInfo, string, error) { + deadline := time.Now().AddDate(0, 0, -1*days) + + url = apiEP + url + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, "", err + } + req.Header.Set("Authorization", "Bearer "+token) + + res, err := client.Do(req) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, "", err + } + + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, "", err + } + userData, ok := result["data"].([]interface{}) + if !ok { + return nil, "", errors.New("rest: error in type assertion") + } + + s := spin.New() + + users := []*userInfo{} + for i, u := range userData { + fmt.Fprintf(os.Stderr, "\r %s Processing inactive users from page %d: %d/%d", s.Next(), pageIndex, i+1, len(userData)) + info, ok := u.(map[string]interface{}) + if !ok { + return nil, "", errors.New("rest: error in type assertion") + } + + // Check blocking time of the primary account of the user + accountsURL := fmt.Sprintf("%s/api/v1.0/Account?filter=id:%s", apiEP, info["primaryAccountId"].(string)) + req, err = http.NewRequest("GET", accountsURL, nil) + if err != nil { + return nil, "", err + } + req.Header.Set("Authorization", "Bearer "+token) + + res, err = client.Do(req) + if err != nil { + return nil, "", err + } + defer res.Body.Close() + + body, err = ioutil.ReadAll(res.Body) + if err != nil { + return nil, "", err + } + + var result map[string]interface{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, "", err + } + accountData, ok := result["data"].([]interface{}) + if !ok { + return nil, "", errors.New("rest: error in type assertion") + } + accountInfo, ok := accountData[0].(map[string]interface{}) + if !ok { + return nil, "", errors.New("rest: error in type assertion") + } + + blockingTime, ok := accountInfo["blockingTime"].(string) + if !ok { + continue + } + + t, err := time.Parse(timeFormat, blockingTime) + if err != nil { + return nil, "", err + } + if t.After(deadline) { + users = append(users, &userInfo{ + Account: info["upn"].(string), + Name: info["displayName"].(string), + Mail: info["primaryAccountEmail"].(string), + UID: fmt.Sprintf("%0.f", info["uid"]), + GID: fmt.Sprintf("%0.f", info["gid"]), + BlockingTime: t, + }) + } + } + + var nextPage string + if pagination, ok := result["pagination"].(map[string]interface{}); ok { + if links, ok := pagination["links"].(map[string]interface{}); ok { + nextPage, _ = links["next"].(string) + } + } + + return users, nextPage, nil + +} + +func filterUsersByDate(users []*userInfo, days int) ([]*userInfo, error) { + // TODO: Add filters once bug in grappa is fixed + return users, nil +} + func newUserInfo() *userInfo { return &userInfo{ AccountOwner: &userInfo{}, @@ -281,6 +459,7 @@ type userInfo struct { AccountOwner *userInfo AccountOwnerDN string Phone string + BlockingTime time.Time } func (ui *userInfo) accountTypeHuman() string {