1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
20 "git.arvados.org/arvados.git/lib/cmd"
21 "git.arvados.org/arvados.git/sdk/go/arvados"
26 type resourceList interface {
28 GetItems() []interface{}
31 // UserList implements resourceList interface
32 type UserList struct {
36 // Len returns the amount of items this list holds
37 func (l UserList) Len() int {
41 // GetItems returns the list of items
42 func (l UserList) GetItems() (out []interface{}) {
43 for _, item := range l.Items {
44 out = append(out, item)
50 cfg, err := GetConfig()
55 if err := doMain(&cfg); err != nil {
60 type ConfigParams struct {
63 Client *arvados.Client
66 func ParseFlags(cfg *ConfigParams) error {
67 flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
68 flags.Usage = func() {
69 usageStr := `Synchronize remote users into Arvados from a CSV format file with 5 columns:
73 * 4th: Active status (0 or 1)
74 * 5th: Admin status (0 or 1)`
75 fmt.Fprintf(flags.Output(), "%s\n\n", usageStr)
76 fmt.Fprintf(flags.Output(), "Usage:\n%s [OPTIONS] <input-file.csv>\n\n", os.Args[0])
77 fmt.Fprintf(flags.Output(), "Options:\n")
81 verbose := flags.Bool(
84 "Log informational messages. Off by default.")
85 getVersion := flags.Bool(
88 "Print version information and exit.")
90 if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "input-file.csv", os.Stderr); !ok {
92 } else if *getVersion {
93 fmt.Printf("%s %s\n", os.Args[0], version)
97 // Input file as a required positional argument
98 if flags.NArg() == 0 {
99 return fmt.Errorf("please provide a path to an input file")
101 srcPath := &os.Args[flags.NFlag()+1]
105 return fmt.Errorf("input file path invalid")
109 cfg.Verbose = *verbose
114 // GetConfig sets up a ConfigParams struct
115 func GetConfig() (cfg ConfigParams, err error) {
116 err = ParseFlags(&cfg)
121 cfg.Client = arvados.NewClientFromEnv()
123 // Check current user permissions
124 u, err := cfg.Client.CurrentUser()
126 return cfg, fmt.Errorf("error getting the current user: %s", err)
129 return cfg, fmt.Errorf("current user (%s) is not an admin user", u.UUID)
135 func doMain(cfg *ConfigParams) error {
136 // Try opening the input file early, just in case there's a problem.
137 f, err := os.Open(cfg.Path)
139 return fmt.Errorf("error opening input file: %s", err)
143 allUsers := make(map[string]arvados.User)
144 results, err := GetAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
146 return fmt.Errorf("error getting all users: %s", err)
148 log.Printf("Found %d users", len(results))
149 for _, item := range results {
150 u := item.(arvados.User)
151 allUsers[strings.ToLower(u.Email)] = u
154 loadedRecords, err := LoadInputFile(f)
156 return fmt.Errorf("reading input file %q: %s", cfg.Path, err)
158 log.Printf("Loaded %d records from input file", len(loadedRecords))
160 updatesSucceeded, updatesFailed := 0, 0
161 for _, record := range loadedRecords {
162 if updated, err := ProcessRecord(cfg, record, allUsers); err != nil {
163 log.Printf("error processing record %q: %s", record.Email, err)
169 log.Printf("Updated %d account(s), failed to update %d account(s)", updatesSucceeded, updatesFailed)
174 type userRecord struct {
182 // ProcessRecord creates or updates a user based on the given record
183 func ProcessRecord(cfg *ConfigParams, record userRecord, allUsers map[string]arvados.User) (bool, error) {
184 wantedActiveStatus := strconv.FormatBool(record.Active)
185 wantedAdminStatus := strconv.FormatBool(record.Admin)
186 updateRequired := false
187 // Check if user exists, set its active & admin status.
188 var user arvados.User
189 user, ok := allUsers[record.Email]
191 err := CreateUser(cfg.Client, &user, map[string]string{
192 "email": record.Email,
193 "first_name": record.FirstName,
194 "last_name": record.LastName,
195 "is_active": strconv.FormatBool(record.Active),
196 "is_admin": strconv.FormatBool(record.Admin),
199 return false, fmt.Errorf("error creating user %q: %s", record.Email, err)
201 updateRequired = true
202 log.Printf("Created user %q", record.Email)
204 if record.Active != user.IsActive {
205 updateRequired = true
207 // Here we assume the 'setup' is done elsewhere if needed.
208 err := UpdateUser(cfg.Client, user.UUID, &user, map[string]string{
209 "is_active": wantedActiveStatus,
210 "is_admin": wantedAdminStatus, // Just in case it needs to be changed.
213 return false, fmt.Errorf("error updating user %q: %s", record.Email, err)
216 err := UnsetupUser(cfg.Client, user.UUID, &user)
218 return false, fmt.Errorf("error deactivating user %q: %s", record.Email, err)
222 // Inactive users cannot be admins.
223 if user.IsActive && record.Admin != user.IsAdmin {
224 updateRequired = true
225 err := UpdateUser(cfg.Client, user.UUID, &user, map[string]string{
226 "is_admin": wantedAdminStatus,
229 return false, fmt.Errorf("error updating user %q: %s", record.Email, err)
232 allUsers[record.Email] = user
234 log.Printf("Updated user %q", record.Email)
237 return updateRequired, nil
240 // LoadInputFile reads the input file and returns a list of user records
241 func LoadInputFile(f *os.File) (loadedRecords []userRecord, err error) {
243 csvReader := csv.NewReader(f)
244 loadedRecords = make([]userRecord, 0)
247 record, e := csvReader.Read()
253 err = fmt.Errorf("parsing error at line %d: %s", lineNo, e)
256 if len(record) != 5 {
257 err = fmt.Errorf("parsing error at line %d: expected 5 fields, found %d", lineNo, len(record))
260 email := strings.ToLower(strings.TrimSpace(record[0]))
261 firstName := strings.TrimSpace(record[1])
262 lastName := strings.TrimSpace(record[2])
263 active := strings.TrimSpace(record[3])
264 admin := strings.TrimSpace(record[4])
265 if email == "" || firstName == "" || lastName == "" || active == "" || admin == "" {
266 err = fmt.Errorf("parsing error at line %d: fields cannot be empty", lineNo)
269 activeBool, err := strconv.ParseBool(active)
271 return nil, fmt.Errorf("parsing error at line %d: active status not recognized", lineNo)
273 adminBool, err := strconv.ParseBool(admin)
275 return nil, fmt.Errorf("parsing error at line %d: admin status not recognized", lineNo)
277 loadedRecords = append(loadedRecords, userRecord{
279 FirstName: firstName,
285 return loadedRecords, nil
288 // GetAll adds all objects of type 'resource' to the 'allItems' list
289 func GetAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
290 // Use the maximum page size the server allows
292 params.Limit = &limit
294 params.Order = "uuid"
296 if err = GetResourceList(c, &page, res, params); err != nil {
299 // Have we finished paging?
303 allItems = append(allItems, page.GetItems()...)
304 params.Offset += page.Len()
309 func jsonReader(rscName string, ob interface{}) io.Reader {
310 j, err := json.Marshal(ob)
315 v[rscName] = []string{string(j)}
316 return bytes.NewBufferString(v.Encode())
319 // GetResourceList fetches res list using params
320 func GetResourceList(c *arvados.Client, dst *resourceList, res string, params interface{}) error {
321 return c.RequestAndDecode(dst, "GET", "/arvados/v1/"+res, nil, params)
324 // CreateUser creates a user with userData parameters, assigns it to dst
325 func CreateUser(c *arvados.Client, dst *arvados.User, userData map[string]string) error {
326 return c.RequestAndDecode(dst, "POST", "/arvados/v1/users", jsonReader("user", userData), nil)
329 // UpdateUser updates a user with userData parameters
330 func UpdateUser(c *arvados.Client, userUUID string, dst *arvados.User, userData map[string]string) error {
331 return c.RequestAndDecode(&dst, "PUT", "/arvados/v1/users/"+userUUID, jsonReader("user", userData), nil)
334 // UnsetupUser deactivates a user
335 func UnsetupUser(c *arvados.Client, userUUID string, dst *arvados.User) error {
336 return c.RequestAndDecode(&dst, "POST", "/arvados/v1/users/"+userUUID+"/unsetup", nil, nil)