class Forty::Sync
Public Class Methods
new(logger, master_username, production_schemas, acl_config, executor, mailer, dry_run=true)
click to toggle source
# File lib/forty/sync.rb, line 18 def initialize(logger, master_username, production_schemas, acl_config, executor, mailer, dry_run=true) @logger = logger or raise Error, 'No logger provided' @master_username = master_username or raise Error, 'No master username provided' @production_schemas = production_schemas or raise Error, 'No production schemas provided' @system_groups = ["pg_signal_backend"] @system_users = ["postgres"] @acl_config = acl_config or raise Error, 'No acl config provided' @acl_config['users'] ||= {} @acl_config['groups'] ||= {} @executor = executor or raise Error, 'No database executor provided' @mailer = mailer or raise Error, 'No mailer provided' @dry_run = dry_run @logger.warn('Dry mode disabled, executing on production') unless @dry_run end
Public Instance Methods
run()
click to toggle source
# File lib/forty/sync.rb, line 35 def run banner() sync_users() sync_groups() sync_user_groups() sync_user_roles() sync_acl() end
sync_acl()
click to toggle source
# File lib/forty/sync.rb, line 179 def sync_acl sync_database_acl() sync_schema_acl() sync_table_acl() end
sync_database_acl()
click to toggle source
# File lib/forty/sync.rb, line 185 def sync_database_acl current_database_acl = _get_current_database_acl() defined_database_acl = _get_defined_database_acl() diverged = _sync_typed_acl('database', current_database_acl, defined_database_acl) @logger.info('All database privileges are in sync') if diverged == 0 end
sync_groups()
click to toggle source
# File lib/forty/sync.rb, line 91 def sync_groups current_groups = _get_current_dwh_groups().keys defined_groups = @acl_config['groups'].keys undefined_groups = (current_groups - defined_groups - @system_groups).uniq.compact missing_groups = (defined_groups - current_groups).uniq.compact undefined_groups.each { |group| _delete_group(group) } missing_groups.each { |group| _create_group(group) } @logger.info('All groups are in sync') if (undefined_groups.count + missing_groups.count) == 0 end
sync_personal_schemas()
click to toggle source
# File lib/forty/sync.rb, line 134 def sync_personal_schemas users = @acl_config['users'].keys users.each do |user| next if user.eql?(@master_username) schemas_owned_by_user = _get_currently_owned_schemas(user).uniq - @production_schemas unless schemas_owned_by_user.empty? tables_owned_by_user = _get_currently_owned_tables(user) schemas_owned_by_user.each do |schema| @executor.execute("set search_path=#{schema}") tables = @executor.execute("select tablename from pg_tables where schemaname='#{schema}'").map { |row| "#{schema}.#{row['tablename']}" } nonowned_tables_by_user = tables.uniq - tables_owned_by_user nonowned_tables_by_user.each { |table| _execute_statement("alter table #{table} owner to #{user};") } end end end end
sync_schema_acl()
click to toggle source
# File lib/forty/sync.rb, line 193 def sync_schema_acl current_schema_acl = _get_current_schema_acl() defined_schema_acl = _get_defined_schema_acl() diverged = _sync_typed_acl('schema', current_schema_acl, defined_schema_acl) @logger.info('All schema privileges are in sync') if diverged == 0 end
sync_table_acl()
click to toggle source
# File lib/forty/sync.rb, line 201 def sync_table_acl current_table_acl = _get_current_table_acl() defined_table_acl = _get_defined_table_acl() diverged = _sync_typed_acl('table', current_table_acl, defined_table_acl) @logger.info('All table privileges are in sync') if diverged == 0 end
sync_user_groups()
click to toggle source
# File lib/forty/sync.rb, line 104 def sync_user_groups current_user_groups = _get_current_user_groups() defined_user_groups = _get_defined_user_groups() _check_group_unknown(current_user_groups.keys, defined_user_groups.keys) current_users = _get_current_dwh_users().keys defined_users = _get_defined_users() _check_user_unknown(current_users, defined_users) diverged = 0 current_user_groups.each do |group, list| current_list = list defined_list = defined_user_groups[group] || [] undefined_assignments = (current_list - defined_list).uniq.compact missing_assignments = (defined_list - current_list).uniq.compact undefined_assignments.each { |user| _remove_user_from_group(user, group) } missing_assignments.each { |user| _add_user_to_group(user, group) } current_group_diverged = (undefined_assignments.count + missing_assignments.count) diverged += current_group_diverged @logger.debug("Users of group #{group} are in sync") if current_group_diverged == 0 end @logger.info('All user groups are in sync') if diverged == 0 end
sync_user_roles()
click to toggle source
# File lib/forty/sync.rb, line 151 def sync_user_roles defined_user_roles = _get_defined_user_roles() current_user_roles = _get_current_user_roles() users = ((defined_user_roles.keys).concat(current_user_roles.keys)).uniq.compact diverged = 0 users.each do |user| next if user.eql?(@master_username) or @system_users.include?(user) raise Error, "Users are not in sync #{user}" if current_user_roles[user].nil? or defined_user_roles[user].nil? undefined_roles = (current_user_roles[user] - defined_user_roles[user]).uniq.compact missing_roles = (defined_user_roles[user] - current_user_roles[user]).uniq.compact current_roles_diverged = (undefined_roles.count + missing_roles.count) diverged += current_roles_diverged undefined_roles.each { |role| _execute_statement("alter user #{user} no#{role};") } missing_roles.each { |role| _execute_statement("alter user #{user} #{role};") } @logger.debug("Roles of #{user} are in sync") if current_roles_diverged == 0 end @logger.info('All user roles are in sync') if diverged == 0 end
sync_users()
click to toggle source
# File lib/forty/sync.rb, line 68 def sync_users current_users = _get_current_dwh_users.keys defined_users = @acl_config['users'].keys undefined_users = (current_users - defined_users - @system_users).uniq.compact missing_users = (defined_users - current_users).uniq.compact @logger.debug("Undefined users: #{undefined_users}") @logger.debug("Missing users: #{missing_users}") undefined_users.each { |user| _delete_user(user) } missing_users.each do |user| roles = @acl_config['users'][user]['roles'] || [] password = @acl_config['users'][user]['password'] search_path = @production_schemas.join(',') owner = @acl_config['users'][user]['email'] _create_user(user, password, roles, search_path, owner) end @logger.info('All users are in sync') if (undefined_users.count + missing_users.count) == 0 end
Private Instance Methods
_add_user_to_group(user, group)
click to toggle source
# File lib/forty/sync.rb, line 381 def _add_user_to_group(user, group) _execute_statement("alter group #{group} add user #{user};") end
_check_group_unknown(current_groups, defined_groups)
click to toggle source
# File lib/forty/sync.rb, line 254 def _check_group_unknown(current_groups, defined_groups) @logger.debug("Check whether groups are in sync. Current: #{current_groups}; Defined: #{defined_groups}; System: #{@system_groups}") raise Error, 'Groups are out of sync!' if _mismatch?(current_groups - @system_groups, defined_groups) end
_check_user_unknown(current_users, defined_users)
click to toggle source
# File lib/forty/sync.rb, line 259 def _check_user_unknown(current_users, defined_users) @logger.debug("Check whether users are in sync. Current: #{current_users}; Defined: #{defined_users}; System: #{@system_users}") raise Error, 'Users are out of sync!' if _mismatch?(current_users - @system_users, defined_users) end
_create_group(group)
click to toggle source
# File lib/forty/sync.rb, line 288 def _create_group(group) _execute_statement("create group #{group};") end
_create_user(user, password, roles=[], search_path=nil, owner='')
click to toggle source
# File lib/forty/sync.rb, line 312 def _create_user(user, password, roles=[], search_path=nil, owner='') if password.to_s.length == 0 password = _generate_password() end _execute_statement("create user #{user} with password '#{password}' #{roles.join(' ')};") if owner.to_s.length > 0 and owner.include?('@') @mailer.send_welcome(owner, user, password) else @logger.warn("Email address of user '#{user}' is empty or malformed: '#{owner}'") end unless search_path.nil? or search_path.empty? _execute_statement("alter user #{user} set search_path to #{search_path};") end end
_delete_group(group)
click to toggle source
# File lib/forty/sync.rb, line 292 def _delete_group(group) full_group_name = "group #{group}" acl = { 'table' => _get_current_table_acl()[full_group_name], 'schema' => _get_current_schema_acl()[full_group_name], 'database' => _get_current_database_acl()[full_group_name] } acl.each do |type, acl| unless acl.nil? or acl.empty? acl.each do |identifier, permissions| _revoke_privileges(full_group_name, type, identifier, permissions) end end end _execute_statement("drop group #{group};") end
_delete_user(user)
click to toggle source
# File lib/forty/sync.rb, line 341 def _delete_user(user) raise Error, 'Please define the master user in the ACL file!' if user.eql?(@master_username) schemas_owned_by_user = _get_currently_owned_schemas(user) tables_owned_by_user = _get_currently_owned_tables(user) _resolve_object_ownership_upon_user_deletion(schemas_owned_by_user, tables_owned_by_user) _revoke_all_privileges(user) _execute_statement("drop user #{user};") end
_execute_statement(statement)
click to toggle source
# File lib/forty/sync.rb, line 271 def _execute_statement(statement) attempts = 0 @logger.info(statement.sub(/(password\s+')(?:[^\s]+)(')/, '\1\2')) if @dry_run === false begin @logger.info("Retrying to execute statement in #{attempts*10} seconds...") if attempts > 0 sleep (attempts*10) attempts += 1 @executor.execute(statement) rescue PG::UndefinedTable => e @logger.error("#{e.class}: #{e.message}" ) retry unless attempts > 3 raise Error, 'Maximum number of attempts exceeded, giving up' end end end
_generate_password()
click to toggle source
# File lib/forty/sync.rb, line 330 def _generate_password begin password = SecureRandom.base64.gsub(/[^a-zA-Z0-9]/, '') raise 'Not valid' unless password.match(/[a-z]/) && password.match(/[A-Z]/) && password.match(/[0-9]/) rescue retry else password end end
_get_current_database_acl()
click to toggle source
# File lib/forty/sync.rb, line 448 def _get_current_database_acl query = <<-SQL select datname as name , array_to_string(datacl, ',') as acls from pg_database where datacl is not null and datdba != 1 ; SQL raw_database_acl = @executor.execute(query) _parse_current_acl('database', raw_database_acl) end
_get_current_dwh_groups()
click to toggle source
# File lib/forty/sync.rb, line 413 def _get_current_dwh_groups query = <<-SQL select distinct groname as name , array_to_string(grolist, ',') as user_list from pg_group ; SQL raw_dwh_groups = @executor.execute(query) Hash[raw_dwh_groups.map do |row| name = row['name'] user_ids = row['user_list'].to_s.split(',').map { |id| id.to_i } [name, user_ids] end] end
_get_current_dwh_users()
click to toggle source
# File lib/forty/sync.rb, line 389 def _get_current_dwh_users query = <<-SQL select distinct usename as name , usesysid as id from pg_user where usename != 'rdsdb' ; SQL raw_dwh_users = @executor.execute(query) Hash[raw_dwh_users.map do |row| name = row['name'] id = row['id'].to_i [name, id] end] end
_get_current_schema_acl()
click to toggle source
# File lib/forty/sync.rb, line 431 def _get_current_schema_acl query = <<-SQL select nspname as name , array_to_string(nspacl, ',') as acls from pg_namespace where nspacl is not null and nspowner != 1 ; SQL raw_schema_acl = @executor.execute(query) _parse_current_acl('schema', raw_schema_acl) end
_get_current_table_acl()
click to toggle source
# File lib/forty/sync.rb, line 464 def _get_current_table_acl query = <<-SQL select pg_namespace.nspname || '.' || pg_class.relname as name , array_to_string(pg_class.relacl, ',') as acls from pg_class left join pg_namespace on pg_class.relnamespace = pg_namespace.oid where pg_class.relacl is not null and pg_namespace.nspname not in ( 'pg_catalog' , 'pg_toast' , 'information_schema' ) order by pg_namespace.nspname || '.' || pg_class.relname ; SQL raw_table_acl = @executor.execute(query) _parse_current_acl('table', raw_table_acl) end
_get_current_user_groups()
click to toggle source
# File lib/forty/sync.rb, line 220 def _get_current_user_groups current_groups = _get_current_dwh_groups() current_users = _get_current_dwh_users().invert current_user_groups = Hash[current_groups.map { |group, list| [group, list.map { |id| current_users[id] }]}] current_user_groups end
_get_current_user_roles()
click to toggle source
# File lib/forty/sync.rb, line 239 def _get_current_user_roles Hash[@executor.execute(<<-SQL).map { |row| [row['usename'], row['user_roles'].split(',').select { |e| not e.empty? }.compact] }] select usename , case when usecreatedb is true then 'createdb' else '' end || ',' || case when usesuper is true then 'createuser' else '' end as user_roles from pg_user where usename != 'rdsdb' order by usename ; SQL end
_get_currently_owned_schemas(user)
click to toggle source
# File lib/forty/sync.rb, line 751 def _get_currently_owned_schemas(user) query = <<-SQL select pg_namespace.nspname as schemaname from pg_namespace left join pg_user on pg_namespace.nspowner = pg_user.usesysid where pg_user.usename = '#{user}' ; SQL @executor.execute(query).map { |row| row['schemaname'] } end
_get_currently_owned_tables(user)
click to toggle source
# File lib/forty/sync.rb, line 762 def _get_currently_owned_tables(user) query = <<-SQL select (schemaname || '.' || tablename) as tablename from pg_tables where tableowner = '#{user}' ; SQL @executor.execute(query).map { |row| row['tablename'] } end
_get_defined_acl(identifier_type)
click to toggle source
# File lib/forty/sync.rb, line 579 def _get_defined_acl(identifier_type) defined_acl = {} groups = Hash[@acl_config['groups'].map { |name, config| ["group #{name}", config] }] || {} users = @acl_config['users'] || {} grantees = {} .merge(groups) .merge(users) grantees.each do |grantee, config| permissions = config['permissions'] || [] parsed_permissions = _parse_defined_permissions(identifier_type, permissions) defined_acl[grantee] = parsed_permissions unless parsed_permissions.empty? end defined_acl #.select { |_, permissions| !permissions.empty? } end
_get_defined_database_acl()
click to toggle source
# File lib/forty/sync.rb, line 598 def _get_defined_database_acl _get_defined_acl('database') end
_get_defined_schema_acl()
click to toggle source
# File lib/forty/sync.rb, line 602 def _get_defined_schema_acl _get_defined_acl('schema') end
_get_defined_table_acl()
click to toggle source
# File lib/forty/sync.rb, line 606 def _get_defined_table_acl _get_defined_acl('table') end
_get_defined_user_groups()
click to toggle source
# File lib/forty/sync.rb, line 211 def _get_defined_user_groups Hash[@acl_config['groups'].map do |group, _| [group, @acl_config['users'].select do |_, data| groups = data['groups'] || [] groups.include?(group) end.keys] end] end
_get_defined_user_roles()
click to toggle source
# File lib/forty/sync.rb, line 228 def _get_defined_user_roles Hash[@acl_config['users'].map do |user, config| user_groups = @acl_config['groups'].select { |group| (config['groups'] || []).include?(group) } user_roles = config['roles'] || [] user_groups.each do |_, group_config| user_roles.concat(group_config['roles']) if group_config['roles'].is_a?(Array) end [user, user_roles.uniq] end] end
_get_defined_users()
click to toggle source
# File lib/forty/sync.rb, line 409 def _get_defined_users @acl_config['users'].keys end
_grant_privileges(grantee, identifier_type, identifier_name, privileges)
click to toggle source
# File lib/forty/sync.rb, line 565 def _grant_privileges(grantee, identifier_type, identifier_name, privileges) privileges ||= [] unless privileges.empty? _execute_statement("grant #{privileges.join(',')} on #{identifier_type} #{identifier_name} to #{grantee};") end end
_is_in_unmanaged_schema?(identifier_type, identifier_name)
click to toggle source
# File lib/forty/sync.rb, line 554 def _is_in_unmanaged_schema?(identifier_type, identifier_name) managed = true case identifier_type when 'schema' managed = @production_schemas.include?(identifier_name) when 'table' managed = @production_schemas.any? { |p| identifier_name.start_with?("#{p}.") } end !managed end
_mismatch?(current, defined)
click to toggle source
# File lib/forty/sync.rb, line 264 def _mismatch?(current, defined) mismatch_count = 0 mismatch_count += (current - defined).count mismatch_count += (defined - current).count mismatch_count > 0 ? true : false end
_parse_current_acl(identifier_type, raw_acl)
click to toggle source
# File lib/forty/sync.rb, line 693 def _parse_current_acl(identifier_type, raw_acl) parsed_acls = {} raw_acl.each do |row| name = row['name'] @logger.debug("Current ACL: [#{identifier_type}] '#{name}': #{row['acls']}") parsed_acl = _parse_current_permissions(identifier_type, row['acls']) parsed_acl.each do |grantee, privileges| unless grantee.empty? if _get_current_dwh_groups().keys.include?(grantee) @logger.debug("Grantee '#{grantee}' has been identified as a group") grantee = "group #{grantee}" end parsed_acls[grantee] ||= {} parsed_acls[grantee][name] ||= [] parsed_acls[grantee][name].concat(privileges) end end end parsed_acls end
_parse_current_permissions(identifier_type, raw_permissions)
click to toggle source
# File lib/forty/sync.rb, line 714 def _parse_current_permissions(identifier_type, raw_permissions) # http://www.postgresql.org/docs/8.1/static/sql-grant.html # Typical ACL string: # # admin=arwdRxt/admin,someone=r/admin,"group selfservice=r/admin" # # =xxxx -- privileges granted to PUBLIC # uname=xxxx -- privileges granted to a user # group gname=xxxx -- privileges granted to a group # /yyyy -- user who granted this privilege privilege_type = case identifier_type when 'database' Privilege::Database when 'schema' Privilege::Schema when 'table' Privilege::Table else raise Error, 'wtf' end parsed_permissions = {} permissions = raw_permissions.split(',') permissions.map! { |entry| entry.delete('"') } permissions.each do |permission| grantee, permission_string = permission.split('=') privileges_string = permission_string.split('/')[0] next if grantee.eql?(@master_username) # superuser has access to everything anyway parsed_permissions[grantee] ||= privilege_type.parse_privileges_from_string(privileges_string) end parsed_permissions end
_parse_defined_permissions(identifier_type, raw_permissions)
click to toggle source
# File lib/forty/sync.rb, line 610 def _parse_defined_permissions(identifier_type, raw_permissions) defined_acl = {} # Implicitly grant usage on schemas for which we grant table privileges if identifier_type.eql?('schema') table_permissions = raw_permissions.select { |permission| permission['type'].eql?('table') } || [] table_permissions.each do |table_permission| table_permission['identifiers'].each do |schema_and_table| schema, _ = schema_and_table.split('.') schemas_to_grant_usage_on = [] if schema.eql?('*') schemas_to_grant_usage_on = @production_schemas else schemas_to_grant_usage_on << schema end schemas_to_grant_usage_on.each do |schema_name| defined_acl[schema_name] ||= [] defined_acl[schema_name].concat(['usage']) end end end end permissions = raw_permissions.select { |permission| permission['type'].eql?(identifier_type) } || [] permissions.each do |permission| permission['identifiers'].each do |identifier| privileges = permission['privileges'] if identifier.match /\*/ case identifier_type when 'database' raise Error, 'Don\'t know how to resolve database identifiers with wildcard' when 'schema' @production_schemas.each do |schema| defined_acl[schema] ||= [] defined_acl[schema].concat(privileges) end when 'table' schema, table = identifier.split('.') raise Error, 'Cannot resolve wildcard schema for specific table names' if schema.eql?('*') and !table.eql?('*') tables_to_grant_privileges_on = [] if schema.eql?('*') @production_schemas.each do |prod_schema| tables = @executor.execute(<<-SQL).map { |row| "#{prod_schema}.#{row['tablename']}" } select tablename from pg_tables where schemaname='#{prod_schema}' SQL tables_to_grant_privileges_on.concat(tables) end else tables_to_grant_privileges_on = @executor.execute(<<-SQL).map { |row| "#{schema}.#{row['tablename']}" } select tablename from pg_tables where schemaname='#{schema}' SQL end tables_to_grant_privileges_on = tables_to_grant_privileges_on.uniq.compact tables_to_grant_privileges_on.each do |table| defined_acl[table] ||= [] defined_acl[table].concat(privileges) end end else defined_acl[identifier] ||= [] defined_acl[identifier].concat(privileges) end end end uniquely_defined_acl = Hash[defined_acl.map do |identifier, privileges| unique_privileges = privileges.include?('all') ? ['all'] : privileges.uniq.compact [identifier, unique_privileges] end] uniquely_defined_acl end
_remove_user_from_group(user, group)
click to toggle source
# File lib/forty/sync.rb, line 385 def _remove_user_from_group(user, group) _execute_statement("alter group #{group} drop user #{user};") end
_resolve_object_ownership_upon_user_deletion(schemas, tables)
click to toggle source
# File lib/forty/sync.rb, line 353 def _resolve_object_ownership_upon_user_deletion(schemas, tables) non_production_tables = tables.select { |table| !@production_schemas.include?(table.split('.')[0]) } production_tables = tables.select { |table| @production_schemas.include?(table.split('.')[0]) } non_production_tables.each { |table| _execute_statement("drop table #{table};") } production_tables.each { |table| _execute_statement("alter table #{table} owner to #{@master_username};") } non_production_schemas = (schemas - @production_schemas) production_schemas = schemas.select { |schema| @production_schemas.include?(schema) } non_production_schemas.each { |schema| _execute_statement("drop schema #{schema} cascade;") } production_schemas.each { |schema| _execute_statement("alter schema #{schema} owner to #{@master_username};") } end
_revoke_all_privileges(grantee)
click to toggle source
# File lib/forty/sync.rb, line 367 def _revoke_all_privileges(grantee) (_get_current_table_acl[grantee] || {}).each do |name, privileges| _revoke_privileges(grantee, 'table', name, privileges) end (_get_current_schema_acl[grantee] || {}).each do |name, privileges| _revoke_privileges(grantee, 'schema', name, privileges) end (_get_current_database_acl[grantee] || {}).each do |name, privileges| _revoke_privileges(grantee, 'database', name, privileges) end end
_revoke_privileges(grantee, identifier_type, identifier_name, privileges)
click to toggle source
# File lib/forty/sync.rb, line 572 def _revoke_privileges(grantee, identifier_type, identifier_name, privileges) privileges ||= [] unless privileges.empty? _execute_statement("revoke #{privileges.join(',')} on #{identifier_type} #{identifier_name} from #{grantee};") end end
_sync_privileges(grantee, identifier_type, current_acl, defined_acl)
click to toggle source
# File lib/forty/sync.rb, line 519 def _sync_privileges(grantee, identifier_type, current_acl, defined_acl) current_acl ||= {} defined_acl ||= {} identifiers = [] .concat(current_acl.keys) .concat(defined_acl.keys) .uniq .compact unsynced_privileges = 0 identifiers.each do |identifier_name| if _is_in_unmanaged_schema?(identifier_type, identifier_name) @logger.debug("SKIPPED #{identifier_type} '#{identifier_name}'. Cannot sync privileges for object outside production schemas!") else current_privileges = current_acl[identifier_name] || [] defined_privileges = defined_acl[identifier_name] || [] undefined_privileges = (current_privileges - defined_privileges).uniq.compact missing_privileges = (defined_privileges - current_privileges).uniq.compact current_privileges_diverged = (undefined_privileges.count + missing_privileges.count) unsynced_privileges += current_privileges_diverged _revoke_privileges(grantee, identifier_type, identifier_name, undefined_privileges) _grant_privileges(grantee, identifier_type, identifier_name, missing_privileges) end end @logger.debug("#{identifier_type.capitalize} privileges for #{grantee} are in sync") if unsynced_privileges == 0 unsynced_privileges end
_sync_typed_acl(identifier_type, current_acl, defined_acl)
click to toggle source
# File lib/forty/sync.rb, line 487 def _sync_typed_acl(identifier_type, current_acl, defined_acl) diverged = 0 current_acl ||= {} defined_acl ||= {} grantees = [] .concat(current_acl.keys) .concat(defined_acl.keys) .uniq .compact known_grantees = [] .concat(_get_current_dwh_users().keys) .concat(_get_current_dwh_groups().keys.map { |group| "group #{group}" }) .uniq .compact if grantees.any? { |grantee| !known_grantees.include?(grantee) } raise Error, "Users or groups not in sync! Could not find #{grantees.select { |grantee| !known_grantees.include?(grantee) }.join(', ')}" end grantees.each do |grantee| current_grantee_acl = current_acl[grantee] || {} defined_grantee_acl = defined_acl[grantee] || {} unsynced_privileges_count = _sync_privileges(grantee, identifier_type, current_grantee_acl, defined_grantee_acl) diverged += unsynced_privileges_count end diverged end