X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/4355586821d71fed6a3fe95fea69f548797f77d8..bdcf09e34f8eec88e1e326094ac60b5d484844e1:/services/login-sync/bin/arvados-login-sync diff --git a/services/login-sync/bin/arvados-login-sync b/services/login-sync/bin/arvados-login-sync index d5ec159867..5c6691ab95 100755 --- a/services/login-sync/bin/arvados-login-sync +++ b/services/login-sync/bin/arvados-login-sync @@ -9,6 +9,8 @@ require 'arvados' require 'etc' require 'fileutils' require 'yaml' +require 'optparse' +require 'open3' req_envs = %w(ARVADOS_API_HOST ARVADOS_API_TOKEN ARVADOS_VIRTUAL_MACHINE_UUID) req_envs.each do |k| @@ -17,20 +19,32 @@ req_envs.each do |k| end end -exclusive_mode = ARGV.index("--exclusive") +options = {} +OptionParser.new do |parser| + parser.on('--exclusive', 'Manage SSH keys file exclusively.') + parser.on('--rotate-tokens', 'Force a rotation of all user tokens.') + parser.on('--skip-missing-users', "Don't try to create any local accounts.") + parser.on('--token-lifetime SECONDS', 'Create user tokens that expire after SECONDS.', Integer) + parser.on('--debug', 'Enable debug output') +end.parse!(into: options) + exclusive_banner = "####################################################################################### # THIS FILE IS MANAGED BY #{$0} -- CHANGES WILL BE OVERWRITTEN # #######################################################################################\n\n" start_banner = "### BEGIN Arvados-managed keys -- changes between markers will be overwritten\n" end_banner = "### END Arvados-managed keys -- changes between markers will be overwritten\n" -# Don't try to create any local accounts -skip_missing_users = ARGV.index("--skip-missing-users") - keys = '' begin + debug = false + if options[:"debug"] + debug = true + end arv = Arvados.new({ :suppress_ssl_warnings => false }) + logincluster_arv = Arvados.new({ :api_host => (ENV['LOGINCLUSTER_ARVADOS_API_HOST'] || ENV['ARVADOS_API_HOST']), + :api_token => (ENV['LOGINCLUSTER_ARVADOS_API_TOKEN'] || ENV['ARVADOS_API_TOKEN']), + :suppress_ssl_warnings => false }) vm_uuid = ENV['ARVADOS_VIRTUAL_MACHINE_UUID'] @@ -61,13 +75,13 @@ begin begin pwnam[l[:username]] = Etc.getpwnam(l[:username]) rescue - if skip_missing_users + if options[:"skip-missing-users"] STDERR.puts "Account #{l[:username]} not found. Skipping" true end else if pwnam[l[:username]].uid < uid_min - STDERR.puts "Account #{l[:username]} uid #{pwnam[l[:username]].uid} < uid_min #{uid_min}. Skipping" + STDERR.puts "Account #{l[:username]} uid #{pwnam[l[:username]].uid} < uid_min #{uid_min}. Skipping" if debug true end end @@ -77,6 +91,7 @@ begin # Collect all keys logins.each do |l| + STDERR.puts("Considering #{l[:username]} ...") if debug keys[l[:username]] = Array.new() if not keys.has_key?(l[:username]) key = l[:public_key] if !key.nil? @@ -91,38 +106,72 @@ begin end seen = Hash.new() - devnull = open("/dev/null", "w") + + current_user_groups = Hash.new + while (ent = Etc.getgrent()) do + ent.mem.each do |member| + current_user_groups[member] ||= Array.new + current_user_groups[member].push ent.name + end + end + Etc.endgrent() logins.each do |l| next if seen[l[:username]] seen[l[:username]] = true + username = l[:username] + unless pwnam[l[:username]] STDERR.puts "Creating account #{l[:username]}" - groups = l[:groups] || [] - # Adding users to the FUSE group has long been hardcoded behavior. - groups << "fuse" - groups.select! { |g| Etc.getgrnam(g) rescue false } # Create new user - unless system("useradd", "-m", - "-c", l[:username], + out, st = Open3.capture2e("useradd", "-m", + "-c", username, "-s", "/bin/bash", - "-G", groups.join(","), - l[:username], - out: devnull) - STDERR.puts "Account creation failed for #{l[:username]}: #{$?}" + username) + if st.exitstatus != 0 + STDERR.puts "Account creation failed for #{l[:username]}:\n#{out}" next end begin - pwnam[l[:username]] = Etc.getpwnam(l[:username]) + pwnam[username] = Etc.getpwnam(username) rescue => e STDERR.puts "Created account but then getpwnam() failed for #{l[:username]}: #{e}" raise end end - @homedir = pwnam[l[:username]].dir - userdotssh = File.join(@homedir, ".ssh") + existing_groups = current_user_groups[username] || [] + groups = l[:groups] || [] + # Adding users to the FUSE group has long been hardcoded behavior. + groups << "fuse" + groups << username + groups.select! { |g| Etc.getgrnam(g) rescue false } + + groups.each do |addgroup| + if existing_groups.index(addgroup).nil? + # User should be in group, but isn't, so add them. + STDERR.puts "Add user #{username} to #{addgroup} group" + out, st = Open3.capture2e("usermod", "-aG", addgroup, username) + if st.exitstatus != 0 + STDERR.puts "Failed to add #{username} to #{addgroup} group:\n#{out}" + end + end + end + + existing_groups.each do |removegroup| + if groups.index(removegroup).nil? + # User is in a group, but shouldn't be, so remove them. + STDERR.puts "Remove user #{username} from #{removegroup} group" + out, st = Open3.capture2e("gpasswd", "-d", username, removegroup) + if st.exitstatus != 0 + STDERR.puts "Failed to remove user #{username} from #{removegroup} group:\n#{out}" + end + end + end + + homedir = pwnam[l[:username]].dir + userdotssh = File.join(homedir, ".ssh") Dir.mkdir(userdotssh) if !File.exist?(userdotssh) newkeys = "###\n###\n" + keys[l[:username]].join("\n") + "\n###\n###\n" @@ -135,7 +184,7 @@ begin oldkeys = "" end - if exclusive_mode + if options[:exclusive] newkeys = exclusive_banner + newkeys elsif oldkeys.start_with?(exclusive_banner) newkeys = start_banner + newkeys + end_banner @@ -150,13 +199,70 @@ begin f.write(newkeys) f.close() end + + userdotconfig = File.join(homedir, ".config") + if !File.exist?(userdotconfig) + Dir.mkdir(userdotconfig) + end + + configarvados = File.join(userdotconfig, "arvados") + Dir.mkdir(configarvados) if !File.exist?(configarvados) + + tokenfile = File.join(configarvados, "settings.conf") + + begin + STDERR.puts "Processing #{tokenfile} ..." if debug + newToken = false + if File.exist?(tokenfile) + # check if the token is still valid + myToken = ENV["ARVADOS_API_TOKEN"] + userEnv = IO::read(tokenfile) + if (m = /^ARVADOS_API_TOKEN=(.*?\n)/m.match(userEnv)) + begin + tmp_arv = Arvados.new({ :api_host => (ENV['LOGINCLUSTER_ARVADOS_API_HOST'] || ENV['ARVADOS_API_HOST']), + :api_token => (m[1]), + :suppress_ssl_warnings => false }) + tmp_arv.user.current + rescue Arvados::TransactionFailedError => e + if e.to_s =~ /401 Unauthorized/ + STDERR.puts "Account #{l[:username]} token not valid, creating new token." + newToken = true + else + raise + end + end + end + elsif !File.exist?(tokenfile) || options[:"rotate-tokens"] + STDERR.puts "Account #{l[:username]} token file not found, creating new token." + newToken = true + end + if newToken + aca_params = {owner_uuid: l[:user_uuid], api_client_id: 0} + if options[:"token-lifetime"] && options[:"token-lifetime"] > 0 + aca_params.merge!(expires_at: (Time.now + options[:"token-lifetime"])) + end + user_token = logincluster_arv.api_client_authorization.create(api_client_authorization: aca_params) + f = File.new(tokenfile, 'w') + f.write("ARVADOS_API_HOST=#{ENV['ARVADOS_API_HOST']}\n") + f.write("ARVADOS_API_TOKEN=v2/#{user_token[:uuid]}/#{user_token[:api_token]}\n") + f.close() + end + rescue => e + STDERR.puts "Error setting token for #{l[:username]}: #{e}" + end + FileUtils.chown_R(l[:username], nil, userdotssh) + FileUtils.chown_R(l[:username], nil, userdotconfig) File.chmod(0700, userdotssh) - File.chmod(0750, @homedir) + File.chmod(0700, userdotconfig) + File.chmod(0700, configarvados) + File.chmod(0750, homedir) File.chmod(0600, keysfile) + if File.exist?(tokenfile) + File.chmod(0600, tokenfile) + end end - devnull.close rescue Exception => bang puts "Error: " + bang.to_s puts bang.backtrace.join("\n")