Merge branch '16803-shell-sync-tokens' refs #16803
[arvados.git] / services / login-sync / bin / arvados-login-sync
1 #!/usr/bin/env ruby
2 # Copyright (C) The Arvados Authors. All rights reserved.
3 #
4 # SPDX-License-Identifier: AGPL-3.0
5
6 require 'rubygems'
7 require 'pp'
8 require 'arvados'
9 require 'etc'
10 require 'fileutils'
11 require 'yaml'
12
13 req_envs = %w(ARVADOS_API_HOST ARVADOS_API_TOKEN ARVADOS_VIRTUAL_MACHINE_UUID)
14 req_envs.each do |k|
15   unless ENV[k]
16     abort "Fatal: These environment vars must be set: #{req_envs}"
17   end
18 end
19
20 exclusive_mode = ARGV.index("--exclusive")
21 exclusive_banner = "#######################################################################################
22 #  THIS FILE IS MANAGED BY #{$0} -- CHANGES WILL BE OVERWRITTEN  #
23 #######################################################################################\n\n"
24 start_banner = "### BEGIN Arvados-managed keys -- changes between markers will be overwritten\n"
25 end_banner = "### END Arvados-managed keys -- changes between markers will be overwritten\n"
26
27 # Don't try to create any local accounts
28 skip_missing_users = ARGV.index("--skip-missing-users")
29
30 keys = ''
31
32 begin
33   arv = Arvados.new({ :suppress_ssl_warnings => false })
34
35   vm_uuid = ENV['ARVADOS_VIRTUAL_MACHINE_UUID']
36
37   logins = arv.virtual_machine.logins(:uuid => vm_uuid)[:items]
38   logins = [] if logins.nil?
39   logins = logins.reject { |l| l[:username].nil? or l[:hostname].nil? or l[:virtual_machine_uuid] != vm_uuid }
40
41   # No system users
42   uid_min = 1000
43   open("/etc/login.defs", encoding: "utf-8") do |login_defs|
44     login_defs.each_line do |line|
45       next unless match = /^UID_MIN\s+(\S+)$/.match(line)
46       if match[1].start_with?("0x")
47         base = 16
48       elsif match[1].start_with?("0")
49         base = 8
50       else
51         base = 10
52       end
53       new_uid_min = match[1].to_i(base)
54       uid_min = new_uid_min if (new_uid_min > 0)
55     end
56   end
57
58   pwnam = Hash.new()
59   logins.reject! do |l|
60     if not pwnam[l[:username]]
61       begin
62         pwnam[l[:username]] = Etc.getpwnam(l[:username])
63       rescue
64         if skip_missing_users
65           STDERR.puts "Account #{l[:username]} not found. Skipping"
66           true
67         end
68       else
69         if pwnam[l[:username]].uid < uid_min
70           STDERR.puts "Account #{l[:username]} uid #{pwnam[l[:username]].uid} < uid_min #{uid_min}. Skipping"
71           true
72         end
73       end
74     end
75   end
76   keys = Hash.new()
77
78   # Collect all keys
79   logins.each do |l|
80     keys[l[:username]] = Array.new() if not keys.has_key?(l[:username])
81     key = l[:public_key]
82     if !key.nil?
83       # Handle putty-style ssh public keys
84       key.sub!(/^(Comment: "r[^\n]*\n)(.*)$/m,'ssh-rsa \2 \1')
85       key.sub!(/^(Comment: "d[^\n]*\n)(.*)$/m,'ssh-dss \2 \1')
86       key.gsub!(/\n/,'')
87       key.strip
88
89       keys[l[:username]].push(key) if not keys[l[:username]].include?(key)
90     end
91   end
92
93   seen = Hash.new()
94   devnull = open("/dev/null", "w")
95
96   logins.each do |l|
97     next if seen[l[:username]]
98     seen[l[:username]] = true
99
100     unless pwnam[l[:username]]
101       STDERR.puts "Creating account #{l[:username]}"
102       groups = l[:groups] || []
103       # Adding users to the FUSE group has long been hardcoded behavior.
104       groups << "fuse"
105       groups.select! { |g| Etc.getgrnam(g) rescue false }
106       # Create new user
107       unless system("useradd", "-m",
108                 "-c", l[:username],
109                 "-s", "/bin/bash",
110                 "-G", groups.join(","),
111                 l[:username],
112                 out: devnull)
113         STDERR.puts "Account creation failed for #{l[:username]}: #{$?}"
114         next
115       end
116       begin
117         pwnam[l[:username]] = Etc.getpwnam(l[:username])
118       rescue => e
119         STDERR.puts "Created account but then getpwnam() failed for #{l[:username]}: #{e}"
120         raise
121       end
122     end
123
124     homedir = pwnam[l[:username]].dir
125     userdotssh = File.join(homedir, ".ssh")
126     Dir.mkdir(userdotssh) if !File.exist?(userdotssh)
127
128     newkeys = "###\n###\n" + keys[l[:username]].join("\n") + "\n###\n###\n"
129
130     keysfile = File.join(userdotssh, "authorized_keys")
131
132     if File.exist?(keysfile)
133       oldkeys = IO::read(keysfile)
134     else
135       oldkeys = ""
136     end
137
138     if exclusive_mode
139       newkeys = exclusive_banner + newkeys
140     elsif oldkeys.start_with?(exclusive_banner)
141       newkeys = start_banner + newkeys + end_banner
142     elsif (m = /^(.*?\n|)#{start_banner}(.*?\n|)#{end_banner}(.*)/m.match(oldkeys))
143       newkeys = m[1] + start_banner + newkeys + end_banner + m[3]
144     else
145       newkeys = start_banner + newkeys + end_banner + oldkeys
146     end
147
148     if oldkeys != newkeys then
149       f = File.new(keysfile, 'w')
150       f.write(newkeys)
151       f.close()
152     end
153
154     userdotconfig = File.join(homedir, ".config")
155     if !File.exist?(userdotconfig)
156       Dir.mkdir(userdotconfig)
157     end
158
159     configarvados = File.join(userdotconfig, "arvados")
160     Dir.mkdir(configarvados) if !File.exist?(configarvados)
161
162     tokenfile = File.join(configarvados, "settings.conf")
163
164     begin
165       if !File.exist?(tokenfile)
166         user_token = arv.api_client_authorization.create(api_client_authorization: {owner_uuid: l[:user_uuid], api_client_id: 0})
167         f = File.new(tokenfile, 'w')
168         f.write("ARVADOS_API_HOST=#{ENV['ARVADOS_API_HOST']}\n")
169         f.write("ARVADOS_API_TOKEN=v2/#{user_token[:uuid]}/#{user_token[:api_token]}\n")
170         f.close()
171       end
172     rescue => e
173       STDERR.puts "Error setting token for #{l[:username]}: #{e}"
174     end
175
176     FileUtils.chown_R(l[:username], nil, userdotssh)
177     FileUtils.chown_R(l[:username], nil, userdotconfig)
178     File.chmod(0700, userdotssh)
179     File.chmod(0700, userdotconfig)
180     File.chmod(0700, configarvados)
181     File.chmod(0750, homedir)
182     File.chmod(0600, keysfile)
183     if File.exist?(tokenfile)
184       File.chmod(0600, tokenfile)
185     end
186   end
187
188   devnull.close
189 rescue Exception => bang
190   puts "Error: " + bang.to_s
191   puts bang.backtrace.join("\n")
192   exit 1
193 end