lib/superhosting/controller/user.rb in superhosting-0.0.1 vs lib/superhosting/controller/user.rb in superhosting-0.0.2

- old
+ new

@@ -1,25 +1,254 @@ module Superhosting module Controller class User < Base - def list + USER_NAME_FORMAT = /^[a-zA-Z][-a-zA-Z0-9_]{,31}$/ + def initialize(**kwargs) + super(**kwargs) + @container_controller = self.get_controller(Container) end - def add(name:, container_name:, no_ssh: false, no_ftp: false) + def list(container_name:) + if (resp = @container_controller.available_validation(name: container_name)).net_status_ok? + { data: self._list(container_name: container_name) } + else + resp + end + end + def _list(container_name:) + self._group_get_users_names(name: container_name) end - def passwd(generate:) + def add(name:, container_name:, ftp_dir: nil, ftp_only: false, generate: false) + return { error: :logical_error, code: :option_ftp_only_is_required } if ftp_dir and !ftp_only + web_mapper = PathMapper.new("/web/#{container_name}") + home_dir = ftp_dir.nil? ? web_mapper.path : web_mapper.f(ftp_dir).path + + if (resp = @container_controller.available_validation(name: container_name)).net_status_ok? + if !File.exists? home_dir + resp = { error: :logical_error, code: :incorrect_ftp_dir, data: { dir: home_dir.to_s } } + elsif (resp = self.not_existing_validation(name: name, container_name: container_name)).net_status_ok? + shell = ftp_only ? '/usr/sbin/nologin' : '/bin/bash' + if (resp = self._add(name: name, container_name: container_name, home_dir: home_dir, shell: shell)).net_status_ok? + if generate + resp = self.passwd(name: name, container_name: container_name, generate: generate) + end + end + end + end + resp end + def passwd(name:, container_name:, generate: false) + if (resp = @container_controller.available_validation(name: container_name)).net_status_ok? + user_name = "#{container_name}_#{name}" + passwords = self._create_password(generate: generate) + self._update_password(name: user_name, encrypted_password: passwords[:encrypted_password]) + generate ? { data: passwords[:password] } : {} + else + resp + end + end + def delete(name:, container_name:) + if (resp = @container_controller.available_validation(name: container_name)).net_status_ok? and + (resp = self.existing_validation(name: name, container_name: container_name)).net_status_ok? + container_lib_mapper = @lib.containers.f(container_name) + passwd_mapper = container_lib_mapper.config.f('etc-passwd') + user_name = "#{container_name}_#{name}" + self._del(name: user_name) + passwd_mapper.remove_line!(/^#{user_name}:.*/) + end + resp + end + def change(name:, container_name:, ftp_dir: nil, ftp_only: false, generate: false) + if (resp = self.delete(name: name, container_name: container_name)).net_status_ok? + self.add(name: name, container_name: container_name, ftp_dir: ftp_dir, ftp_only: ftp_only, generate: generate) + else + resp + end end - def change(name:, container_name:, no_ssh: false, no_ftp: false) + def _get(name:) + begin + Etc.getpwnam(name) + rescue ArgumentError => e + nil + end + end + def _add(name:, container_name:, shell: '/usr/sbin/nologin', home_dir: "/web/#{container_name}") + user = self._get(name: container_name) + self._add_custom(name: "#{container_name}_#{name}", group: container_name, shell: shell, home_dir: home_dir, uid: user.uid) + end + + def _add_system_user(name:, container_name:, shell: '/usr/sbin/nologin', home_dir: "/web/#{container_name}") + self._add_custom(name: "#{container_name}_#{name}", group: container_name, shell: shell, home_dir: home_dir) + end + + def _add_custom(name:, group:, shell: '/usr/sbin/nologin', home_dir: "/web/#{group}", uid: nil) + if (resp = self.adding_validation(name: name, container_name: group)).net_status_ok? + container_lib_mapper = @lib.containers.f(group) + passwd_mapper = container_lib_mapper.config.f('etc-passwd') + + useradd_command = "useradd #{name} -g #{group} -d #{home_dir} -s #{shell}".split + useradd_command += "-u #{uid} -o".split unless uid.nil? + self.command!(useradd_command, debug: false) + + user = self._get(name: name) + + self.with_dry_run do |dry_run| + user_gid, user_uid = dry_run ? ['XXXX', 'XXXX'] : [user.gid, user.uid] + passwd_mapper.append_line!("#{name}:x:#{user_uid}:#{user_gid}::#{home_dir}:#{shell}") + end + end + resp + end + + def _pretty_add_custom(name:, group:, shell: '/usr/sbin/nologin', home_dir: "/web/#{group}", uid: nil) + self.debug_operation(desc: { code: :user, data: { name: name } }) do |&blk| + if self._get(name: name) + blk.call(code: :ok) + {} + else + self._add_custom(name: name, group: group, shell: shell, home_dir: home_dir, uid: uid).tap do + blk.call(code: :added) + end + end + end + end + + def _pretty_del(name:, group:) + with_adding_group = self._group_get_users(name: group).one? ? true : false + self._del(name: name) + self._group_add(name: group) if with_adding_group + end + + def _del(name:) + self.debug_operation(desc: { code: :user, data: { name: name } }) do |&blk| + self.with_dry_run do |dry_run| + resp = {} + resp = self.command!("userdel #{name}", debug: false) unless dry_run + blk.call(code: :deleted) + resp + end + end + end + + def _create_password(generate: false) + password = if generate + SecureRandom.hex + else + while 1 + if (pass = ask('Enter password: ') { |q| q.echo = false }) != ask('Repeat password: ') { |q| q.echo = false } + self.info('Passwords does not match') + elsif !StrongPassword::StrengthChecker.new(pass).is_strong?(min_entropy: @config.f('password_strength', default: '15').to_i) + self.info('Password is weak') + else + break + end + end + pass + end + encrypted_password = UnixCrypt::SHA512.build(password) + { password: password, encrypted_password: encrypted_password } + end + + def _update_password(name:, encrypted_password:) + self.debug_operation(desc: { code: :user, data: { name: name } }) do |&blk| + self.with_dry_run do |dry_run| + resp = {} + resp = self.command!("usermod -p '#{encrypted_password}' #{name}", debug: false) unless dry_run + blk.call(code: :updated) + resp + end + end + end + + def _group_get(name:) + begin + Etc.getgrnam(name) + rescue ArgumentError => e + nil + end + end + + def _group_add(name:) + self.debug_operation(desc: { code: :group, data: { name: name } }) do |&blk| + self.with_dry_run do |dry_run| + resp = {} + resp = self.command!("groupadd #{name}", debug: false) unless dry_run + blk.call(code: :added) + resp + end + end + end + + def _group_pretty_add(name:) + self._group_add(name: name) if self._group_get(name: name).nil? + end + + def _group_del(name:) + self.debug_operation(desc: { code: :group, data: { name: name } }) do |&blk| + self.with_dry_run do |dry_run| + resp = {} + resp = self.command!("groupdel #{name}", debug: false) unless dry_run + blk.call(code: :deleted) + resp + end + end + end + + def _group_pretty_del(name:) + self._group_del(name: name) unless self._group_get(name: name).nil? + end + + def _group_get_users(name:) + if (group = self._group_get(name: name)) + gid = group.gid + + users = [] + Etc.passwd do |user| + users << user if user.gid == gid + end + users + else + [] + end + end + + def _group_get_users_names(name:) + self._group_get_users(name: name).map(&:name) + end + + def _group_get_system_users(name:) + if (base_user = self._get(name: name)) + self._group_get_users(name: name).map {|u| u.name.slice(/(?<=#{name}_).*/) if u.uid != base_user.uid }.compact + else + [] + end + end + + def _group_del_users(name:) + self._group_get_users_names(name: name).each {|user| self._del(name: user) } + end + + def adding_validation(name:, container_name:) + return { error: :input_error, code: :invalid_user_name, data: { name: name, regex: USER_NAME_FORMAT } } if name !~ USER_NAME_FORMAT + self.not_existing_validation(name: name, container_name: container_name) + end + + def existing_validation(name:, container_name:) + user_name = "#{container_name}_#{name}" + PathMapper.new('/etc/passwd').check(user_name) ? {} : { error: :logical_error, code: :user_does_not_exists, data: { name: user_name } } + end + + def not_existing_validation(name:, container_name:) + self.existing_validation(name: name, container_name: container_name).net_status_ok? ? { error: :logical_error, code: :user_exists, data: { name: "#{container_name}_#{name}" } } : {} end end end end \ No newline at end of file