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 {
61 Client *arvados.Client
62 CurrentUser arvados.User
63 DeactivateUnlisted bool
68 func ParseFlags(cfg *ConfigParams) error {
69 flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
70 flags.Usage = func() {
71 usageStr := `Synchronize remote users into Arvados from a CSV format file with 5 columns:
75 * 4th: Active status (0 or 1)
76 * 5th: Admin status (0 or 1)`
77 fmt.Fprintf(flags.Output(), "%s\n\n", usageStr)
78 fmt.Fprintf(flags.Output(), "Usage:\n%s [OPTIONS] <input-file.csv>\n\n", os.Args[0])
79 fmt.Fprintf(flags.Output(), "Options:\n")
83 deactivateUnlisted := flags.Bool(
84 "deactivate-unlisted",
86 "Deactivate users that are not in the input file.")
87 verbose := flags.Bool(
90 "Log informational messages. Off by default.")
91 getVersion := flags.Bool(
94 "Print version information and exit.")
96 if ok, code := cmd.ParseFlags(flags, os.Args[0], os.Args[1:], "input-file.csv", os.Stderr); !ok {
98 } else if *getVersion {
99 fmt.Printf("%s %s\n", os.Args[0], version)
103 // Input file as a required positional argument
104 if flags.NArg() == 0 {
105 return fmt.Errorf("please provide a path to an input file")
107 srcPath := &os.Args[flags.NFlag()+1]
111 return fmt.Errorf("input file path invalid")
114 cfg.DeactivateUnlisted = *deactivateUnlisted
116 cfg.Verbose = *verbose
121 // GetConfig sets up a ConfigParams struct
122 func GetConfig() (cfg ConfigParams, err error) {
123 err = ParseFlags(&cfg)
128 cfg.Client = arvados.NewClientFromEnv()
130 // Check current user permissions
131 u, err := cfg.Client.CurrentUser()
133 return cfg, fmt.Errorf("error getting the current user: %s", err)
136 return cfg, fmt.Errorf("current user %q is not an admin user", u.UUID)
139 log.Printf("Running as admin user %q", u.UUID)
146 func doMain(cfg *ConfigParams) error {
147 // Try opening the input file early, just in case there's a problem.
148 f, err := os.Open(cfg.Path)
150 return fmt.Errorf("error opening input file: %s", err)
154 allUsers := make(map[string]arvados.User)
155 processedUsers := make(map[string]bool)
156 results, err := GetAll(cfg.Client, "users", arvados.ResourceListParams{}, &UserList{})
158 return fmt.Errorf("error getting all users: %s", err)
160 log.Printf("Found %d users", len(results))
161 for _, item := range results {
162 u := item.(arvados.User)
163 allUsers[strings.ToLower(u.Email)] = u
164 processedUsers[strings.ToLower(u.Email)] = false
167 loadedRecords, err := LoadInputFile(f)
169 return fmt.Errorf("reading input file %q: %s", cfg.Path, err)
171 log.Printf("Loaded %d records from input file", len(loadedRecords))
173 updatesSucceeded, updatesFailed := 0, 0
174 for _, record := range loadedRecords {
175 if record.Email == cfg.CurrentUser.Email {
176 log.Printf("Skipping current user %q from processing", record.Email)
179 if updated, err := ProcessRecord(cfg, record, allUsers); err != nil {
180 log.Printf("error processing record %q: %s", record.Email, err)
183 processedUsers[strings.ToLower(record.Email)] = true
188 if cfg.DeactivateUnlisted {
189 for email, user := range allUsers {
190 if user.UUID == cfg.CurrentUser.UUID {
191 log.Printf("Skipping current user deactivation: %s", user.UUID)
194 if !processedUsers[email] {
196 log.Printf("Deactivating unlisted user %q", user.UUID)
198 var updatedUser arvados.User
199 if err := UnsetupUser(cfg.Client, user.UUID, &updatedUser); err != nil {
200 log.Printf("error deactivating unlisted user %q: %s", user.UUID, err)
203 allUsers[email] = updatedUser
210 log.Printf("Updated %d user(s), failed to update %d user(s)", updatesSucceeded, updatesFailed)
215 type userRecord struct {
223 // ProcessRecord creates or updates a user based on the given record
224 func ProcessRecord(cfg *ConfigParams, record userRecord, allUsers map[string]arvados.User) (bool, error) {
226 log.Printf("Processing record for user %q", record.Email)
229 wantedActiveStatus := strconv.FormatBool(record.Active)
230 wantedAdminStatus := strconv.FormatBool(record.Admin)
231 createRequired := false
232 updateRequired := false
233 // Check if user exists, set its active & admin status.
234 var user arvados.User
235 user, ok := allUsers[record.Email]
238 log.Printf("User %q does not exist, creating", record.Email)
240 createRequired = true
241 err := CreateUser(cfg.Client, &user, map[string]string{
242 "email": record.Email,
243 "first_name": record.FirstName,
244 "last_name": record.LastName,
245 "is_active": strconv.FormatBool(record.Active),
246 "is_admin": strconv.FormatBool(record.Admin),
249 return false, fmt.Errorf("error creating user %q: %s", record.Email, err)
252 if record.Active != user.IsActive {
253 updateRequired = true
256 log.Printf("User %q is inactive, activating", record.Email)
258 // Here we assume the 'setup' is done elsewhere if needed.
259 err := UpdateUser(cfg.Client, user.UUID, &user, map[string]string{
260 "is_active": wantedActiveStatus,
261 "is_admin": wantedAdminStatus, // Just in case it needs to be changed.
264 return false, fmt.Errorf("error updating user %q: %s", record.Email, err)
268 log.Printf("User %q is active, deactivating", record.Email)
270 err := UnsetupUser(cfg.Client, user.UUID, &user)
272 return false, fmt.Errorf("error deactivating user %q: %s", record.Email, err)
276 // Inactive users cannot be admins.
277 if user.IsActive && record.Admin != user.IsAdmin {
279 log.Printf("User %q is active, changing admin status to %v", record.Email, record.Admin)
281 updateRequired = true
282 err := UpdateUser(cfg.Client, user.UUID, &user, map[string]string{
283 "is_admin": wantedAdminStatus,
286 return false, fmt.Errorf("error updating user %q: %s", record.Email, err)
289 allUsers[record.Email] = user
291 log.Printf("Created user %q", record.Email)
294 log.Printf("Updated user %q", record.Email)
297 return createRequired || updateRequired, nil
300 // LoadInputFile reads the input file and returns a list of user records
301 func LoadInputFile(f *os.File) (loadedRecords []userRecord, err error) {
303 csvReader := csv.NewReader(f)
304 loadedRecords = make([]userRecord, 0)
307 record, e := csvReader.Read()
313 err = fmt.Errorf("parsing error at line %d: %s", lineNo, e)
316 if len(record) != 5 {
317 err = fmt.Errorf("parsing error at line %d: expected 5 fields, found %d", lineNo, len(record))
320 email := strings.ToLower(strings.TrimSpace(record[0]))
321 firstName := strings.TrimSpace(record[1])
322 lastName := strings.TrimSpace(record[2])
323 active := strings.TrimSpace(record[3])
324 admin := strings.TrimSpace(record[4])
325 if email == "" || firstName == "" || lastName == "" || active == "" || admin == "" {
326 err = fmt.Errorf("parsing error at line %d: fields cannot be empty", lineNo)
329 activeBool, err := strconv.ParseBool(active)
331 return nil, fmt.Errorf("parsing error at line %d: active status not recognized", lineNo)
333 adminBool, err := strconv.ParseBool(admin)
335 return nil, fmt.Errorf("parsing error at line %d: admin status not recognized", lineNo)
337 loadedRecords = append(loadedRecords, userRecord{
339 FirstName: firstName,
345 return loadedRecords, nil
348 // GetAll adds all objects of type 'resource' to the 'allItems' list
349 func GetAll(c *arvados.Client, res string, params arvados.ResourceListParams, page resourceList) (allItems []interface{}, err error) {
350 // Use the maximum page size the server allows
352 params.Limit = &limit
354 params.Order = "uuid"
356 if err = GetResourceList(c, &page, res, params); err != nil {
359 // Have we finished paging?
363 allItems = append(allItems, page.GetItems()...)
364 params.Offset += page.Len()
369 func jsonReader(rscName string, ob interface{}) io.Reader {
370 j, err := json.Marshal(ob)
375 v[rscName] = []string{string(j)}
376 return bytes.NewBufferString(v.Encode())
379 // GetResourceList fetches res list using params
380 func GetResourceList(c *arvados.Client, dst *resourceList, res string, params interface{}) error {
381 return c.RequestAndDecode(dst, "GET", "/arvados/v1/"+res, nil, params)
384 // CreateUser creates a user with userData parameters, assigns it to dst
385 func CreateUser(c *arvados.Client, dst *arvados.User, userData map[string]string) error {
386 return c.RequestAndDecode(dst, "POST", "/arvados/v1/users", jsonReader("user", userData), nil)
389 // UpdateUser updates a user with userData parameters
390 func UpdateUser(c *arvados.Client, userUUID string, dst *arvados.User, userData map[string]string) error {
391 return c.RequestAndDecode(&dst, "PUT", "/arvados/v1/users/"+userUUID, jsonReader("user", userData), nil)
394 // UnsetupUser deactivates a user
395 func UnsetupUser(c *arvados.Client, userUUID string, dst *arvados.User) error {
396 return c.RequestAndDecode(&dst, "POST", "/arvados/v1/users/"+userUUID+"/unsetup", nil, nil)