diff --git a/changelogs/unreleased/gh-8861-config-privilege-sync.md b/changelogs/unreleased/gh-8861-config-privilege-sync.md
new file mode 100644
index 0000000000000000000000000000000000000000..a5f4c0165420217b5bc272ecbb6f490783a7c0c5
--- /dev/null
+++ b/changelogs/unreleased/gh-8861-config-privilege-sync.md
@@ -0,0 +1,4 @@
+## feature/config
+
+* Improved the credentials applier: now it supports two-way synchronization
+  of roles and privileges for both users and roles (gh-8861).
diff --git a/src/box/lua/config/applier/credentials.lua b/src/box/lua/config/applier/credentials.lua
index 424e1b11d50933ed560a7dad12cc021e94868b11..9bbfb84581cbc0e0c3394c7615f59050f343ef38 100644
--- a/src/box/lua/config/applier/credentials.lua
+++ b/src/box/lua/config/applier/credentials.lua
@@ -1,30 +1,328 @@
 local log = require('internal.config.utils.log')
 
-local function grant_privileges(name, privileges, role_or_user, grant_f)
-    for _, privilege in ipairs(privileges or {}) do
-        log.verbose('credentials.apply: grant %s to %s %s (if not exists)',
-            privilege, role_or_user, name)
-        for _, permission in ipairs(privilege.permissions or {}) do
-            local opts = {if_not_exists = true}
-            if privilege.universe then
-                grant_f(name, permission, 'universe', nil, opts)
-            end
-            -- TODO: It is not possible to grant a permission for
-            -- a non-existing object. It blocks ability to set it
-            -- from a config. Disabled for now.
-            --[[
-            for _, space in ipairs(privilege.spaces or {}) do
-                grant_f(name, permission, 'space', space, opts)
+--[[
+This intermediate representation is a formatted set of
+all permissions for a user or role. It is required to
+standardize diff functions. All the validation is done
+by config or box.info(), so neither the format nor the
+helper function don't perform it. Below you can find two
+converters to this representation, from box format and
+config schema format, accordingly `privileges_{box,config}_convert()`.
+
+[obj_type][obj_name] = {
+    read = true,
+    write = true,
+    ...
+}
+
+obj_types:
+ - 'user'
+ - 'role'
+ - 'space'
+ - 'function'
+ - 'sequence'
+ - 'universe'
+
+obj_names:
+ - mostly user defined strings, provided by config or box
+ - special value '', when there is no obj_name, e.g. for
+   'universe' obj_type or for granting permission for all
+   objects of a type.
+
+privs:
+ - read
+ - write
+ - execute
+   - lua_eval
+   - lua_call
+   - sql
+ - session
+ - usage
+ - create
+ - drop
+ - alter
+ - reference
+ - trigger
+ - insert
+ - update
+ - delete
+
+Examples:
+- - box.schema.user.grant('myuser', 'execute', 'function', 'myfunc')
+  - ['function']['myfunc']['execute'] = true
+  - grant execute of myfunc
+
+- - box.schema.user.grant('myuser', 'execute', 'function')
+  - ['function']['']['execute'] = true
+  - grant execute of all registered functions
+
+- - box.schema.user.grant('myuser', 'read', 'universe')
+  - ['universe']['']['read'] = true
+  - grant read to universe
+
+- - box.schema.user.grant('myuser', 'execute', 'role', 'super')
+  - ['role']['super']['execute'] = true
+  - equivalent to granting a role to myuser
+
+]]--
+
+local function privileges_from_box(privileges)
+    privileges = privileges or {}
+    assert(type(privileges) == 'table')
+
+    local res = {
+        ['user'] = {},
+        ['role'] = {},
+        ['space'] = {},
+        ['function'] = {},
+        ['sequence'] = {},
+        ['universe'] = {},
+    }
+
+    for _, priv in ipairs(privileges) do
+        local perms, obj_type, obj_name = unpack(priv)
+        obj_name = obj_name or ''
+
+        res[obj_type][obj_name] = res[obj_type][obj_name] or {}
+
+        for _, perm in ipairs(perms:split(',')) do
+            res[obj_type][obj_name][perm] = true
+        end
+    end
+
+    return res
+end
+
+-- Note that 'all' is considered a special value, meaning all objects of
+-- obj_type will be granted this permission. Don't use this function if it
+-- may occur in any other meaning, e.g. user defined name.
+--
+-- Note: `obj_names` can be either an array with objects names or a string
+--       with a single one. It could also be `nil`, meaning "do nothing".
+local function privileges_add_perm(obj_type, obj_names, perm, intermediate)
+    if obj_names == nil then
+        return
+    end
+    if type(obj_names) == 'string' then
+        obj_names = {obj_names}
+    end
+
+    for _, obj_name  in ipairs(obj_names) do
+        if obj_name == 'all' then
+            -- '' is a special value, meaning all objects of this obj_type.
+            obj_name = ''
+        end
+        intermediate[obj_type][obj_name] =
+            intermediate[obj_type][obj_name] or {}
+        intermediate[obj_type][obj_name][perm] = true
+    end
+end
+
+local function privileges_from_config(config_data)
+    local privileges = config_data.privileges or {}
+    assert(type(privileges) == 'table')
+
+    local intermediate = {
+        ['user'] = {},
+        ['role'] = {},
+        ['space'] = {},
+        ['function'] = {},
+        ['sequence'] = {},
+        ['universe'] = {},
+    }
+
+    for _, priv in ipairs(privileges) do
+        for _, perm in ipairs(priv.permissions) do
+            if priv.universe then
+                privileges_add_perm('universe', 'all', perm, intermediate)
             end
-            for _, func in ipairs(privilege.functions or {}) do
-                grant_f(name, permission, 'function', func, opts)
+            privileges_add_perm('space', priv.spaces, perm, intermediate)
+            privileges_add_perm('function', priv.functions, perm, intermediate)
+            privileges_add_perm('sequence', priv.sequences, perm, intermediate)
+        end
+    end
+
+    local roles = config_data.roles or {}
+
+    for _, role_name in ipairs(roles) do
+        -- Unlike spaces, functions and sequences, role is allowed to be
+        -- named 'all', so `privileges_add_perm()` isn't used.
+        intermediate['role'][role_name] = intermediate['role'][role_name] or {}
+        intermediate['role'][role_name]['execute'] = true
+    end
+
+    return intermediate
+end
+
+-- Intermediate representation is basically a set, so this function subtracts
+-- `current` from `target`.
+-- Return privileges that are present in `target` but not in `current` as
+-- a list in the following format:
+-- - obj_type: my_type
+--   obj_name: my_name
+--   privs:
+--    - read
+--    - write
+--    - ...
+--
+local function privileges_subtract(target, current)
+    local lacking = {}
+
+    for obj_type, privileges_group in pairs(target) do
+        for obj_name, privileges in pairs(privileges_group) do
+            local lacking_privs = {}
+            for priv, target_val in pairs(privileges) do
+                if target_val and (current[obj_type][obj_name] == nil or
+                                   not current[obj_type][obj_name][priv]) then
+                    table.insert(lacking_privs, priv)
+                end
             end
-            for _, seq in ipairs(privilege.sequences or {}) do
-                grant_f(name, permission, 'sequence', seq, opts)
+            if next(lacking_privs) then
+                table.insert(lacking, {
+                    obj_type = obj_type,
+                    obj_name = obj_name,
+                    privs = lacking_privs,
+                })
             end
-            ]]--
         end
     end
+
+    return lacking
+end
+
+local function privileges_add_defaults(name, role_or_user, intermediate)
+    local res = table.deepcopy(intermediate)
+
+    if role_or_user == 'user' then
+        if name == 'guest' then
+            privileges_add_perm('role', 'public', 'execute', res)
+
+            privileges_add_perm('universe', '', 'session', res)
+            privileges_add_perm('universe', '', 'usage', res)
+
+        elseif name == 'admin' then
+            privileges_add_perm('universe', '', 'read', res)
+            privileges_add_perm('universe', '', 'write', res)
+            privileges_add_perm('universe', '', 'execute', res)
+            privileges_add_perm('universe', '', 'session', res)
+            privileges_add_perm('universe', '', 'usage', res)
+            privileges_add_perm('universe', '', 'create', res)
+            privileges_add_perm('universe', '', 'drop', res)
+            privileges_add_perm('universe', '', 'alter', res)
+            privileges_add_perm('universe', '', 'reference', res)
+            privileges_add_perm('universe', '', 'trigger', res)
+            privileges_add_perm('universe', '', 'insert', res)
+            privileges_add_perm('universe', '', 'update', res)
+            privileges_add_perm('universe', '', 'delete', res)
+
+        else
+            -- Newly created user:
+            privileges_add_perm('role', 'public', 'execute', res)
+
+            privileges_add_perm('universe', '', 'session', res)
+            privileges_add_perm('universe', '', 'usage', res)
+
+            privileges_add_perm('user', name, 'alter', res)
+        end
+
+    elseif role_or_user == 'role' then
+        -- luacheck: ignore 542 empty if branch
+        if name == 'public' then
+            privileges_add_perm('function', 'box.schema.user.info', 'execute',
+                                res)
+            privileges_add_perm('function', 'LUA', 'read', res)
+
+            privileges_add_perm('space', '_vcollation', 'read', res)
+            privileges_add_perm('space', '_vspace', 'read', res)
+            privileges_add_perm('space', '_vsequence', 'read', res)
+            privileges_add_perm('space', '_vindex', 'read', res)
+            privileges_add_perm('space', '_vfunc', 'read', res)
+            privileges_add_perm('space', '_vuser', 'read', res)
+            privileges_add_perm('space', '_vpriv', 'read', res)
+            privileges_add_perm('space', '_vspace_sequence', 'read', res)
+
+            privileges_add_perm('space', '_truncate', 'write', res)
+
+            privileges_add_perm('space', '_session_settings', 'read', res)
+            privileges_add_perm('space', '_session_settings', 'write', res)
+
+        elseif name == 'replication' then
+            privileges_add_perm('space', '_cluster', 'write', res)
+            privileges_add_perm('universe', '', 'read', res)
+
+        elseif name == 'super' then
+            privileges_add_perm('universe', '', 'read', res)
+            privileges_add_perm('universe', '', 'write', res)
+            privileges_add_perm('universe', '', 'execute', res)
+            privileges_add_perm('universe', '', 'session', res)
+            privileges_add_perm('universe', '', 'usage', res)
+            privileges_add_perm('universe', '', 'create', res)
+            privileges_add_perm('universe', '', 'drop', res)
+            privileges_add_perm('universe', '', 'alter', res)
+            privileges_add_perm('universe', '', 'reference', res)
+            privileges_add_perm('universe', '', 'trigger', res)
+            privileges_add_perm('universe', '', 'insert', res)
+            privileges_add_perm('universe', '', 'update', res)
+            privileges_add_perm('universe', '', 'delete', res)
+
+        else
+            -- Newly created role has NO permissions.
+        end
+    else
+        assert(false, 'neither role nor user provided')
+    end
+
+    return res
+end
+
+-- The privileges synchronization between A and B is performed in three steps:
+-- 1. Grant all privileges that are present in B,
+--    but not present in A (`grant(B - A)`).
+-- 2. Add default privileges to B (`B = B + defaults`).
+-- 3. Revoke all privileges that are not present in B,
+--    but present in A (`revoke(A - B)).
+--
+-- Default privileges are not granted on step 1, so they stay revoked if
+-- revoked manually (e.g. with `box.schema.{user,role}.revoke()`).
+-- However, defaults should never be revoked, so target state B is enriched
+-- with them before step 3.
+local function sync_privileges(name, config_privileges, role_or_user)
+    assert(role_or_user == 'user' or role_or_user == 'role')
+    log.verbose('syncing privileges for %s %q', role_or_user, name)
+
+    local grant_f = function(name, privs, obj_type, obj_name)
+        privs = table.concat(privs, ',')
+        log.debug('credentials.apply: ' .. role_or_user ..
+                  '.grant(%q, %q, %q, %q)', name, privs, obj_type, obj_name)
+        box.schema[role_or_user].grant(name, privs, obj_type, obj_name)
+    end
+    local revoke_f = function(name, privs, obj_type, obj_name)
+        privs = table.concat(privs, ',')
+        log.debug('credentials.apply: ' .. role_or_user ..
+                  '.revoke(%q, %q, %q, %q)', name, privs, obj_type, obj_name)
+        box.schema[role_or_user].revoke(name, privs, obj_type, obj_name)
+    end
+
+    local box_privileges = box.schema[role_or_user].info(name)
+
+    config_privileges = privileges_from_config(config_privileges)
+    box_privileges = privileges_from_box(box_privileges)
+
+    local grants = privileges_subtract(config_privileges, box_privileges)
+
+    for _, to_grant in ipairs(grants) do
+        grant_f(name, to_grant.privs, to_grant.obj_type, to_grant.obj_name)
+    end
+
+    config_privileges = privileges_add_defaults(name, role_or_user,
+                                                config_privileges)
+
+    local revokes = privileges_subtract(box_privileges, config_privileges)
+
+    for _, to_revoke in ipairs(revokes) do
+        revoke_f(name, to_revoke.privs, to_revoke.obj_type, to_revoke.obj_name)
+    end
+
 end
 
 -- {{{ Create roles
@@ -37,14 +335,6 @@ local function create_role(role_name)
     end
 end
 
-local function assign_roles_to_role(role_name, roles)
-    for _, role in ipairs(roles or {}) do
-        log.verbose('credentials.apply: add role %q as underlying for ' ..
-            'role %q (if not exists)', role, role_name)
-        box.schema.role.grant(role_name, role, nil, nil, {if_not_exists = true})
-    end
-end
-
 -- Create roles, grant them permissions and assign underlying
 -- roles.
 local function create_roles(role_map)
@@ -52,20 +342,16 @@ local function create_roles(role_map)
         return
     end
 
-    -- Create roles and grant then permissions. Skip assigning
-    -- underlying roles till all the roles will be created.
     for role_name, role_def in pairs(role_map or {}) do
         if role_def ~= nil then
             create_role(role_name)
-            grant_privileges(role_name, role_def.privileges, 'role',
-                box.schema.role.grant)
         end
     end
 
-    -- Assign underlying roles.
+    -- Sync privileges and assign underlying roles.
     for role_name, role_def in pairs(role_map or {}) do
         if role_def ~= nil then
-            assign_roles_to_role(role_name, role_def.roles)
+            sync_privileges(role_name, role_def, 'role')
         end
     end
 end
@@ -117,13 +403,6 @@ local function set_password(user_name, password)
     end
 end
 
-local function assing_roles_to_user(user_name, roles)
-    for _, role in ipairs(roles or {}) do
-        log.verbose('grant role %q to user %q (if not exists)', role, user_name)
-        box.schema.user.grant(user_name, role, nil, nil, {if_not_exists = true})
-    end
-end
-
 -- Create users, set them passwords, assign roles, grant
 -- permissions.
 local function create_users(user_map)
@@ -135,9 +414,7 @@ local function create_users(user_map)
         if user_def ~= nil then
             create_user(user_name)
             set_password(user_name, user_def.password)
-            assing_roles_to_user(user_name, user_def.roles)
-            grant_privileges(user_name, user_def.privileges, 'user',
-                box.schema.user.grant)
+            sync_privileges(user_name, user_def, 'user')
         end
     end
 end
@@ -177,5 +454,13 @@ end
 
 return {
     name = 'credentials',
-    apply = apply
+    apply = apply,
+    -- Exported for testing purposes.
+    _internal = {
+        privileges_from_box = privileges_from_box,
+        privileges_from_config = privileges_from_config,
+        privileges_subtract = privileges_subtract,
+        privileges_add_defaults = privileges_add_defaults,
+        sync_privileges = sync_privileges,
+    },
 }
diff --git a/test/config-luatest/credentials_applier_test.lua b/test/config-luatest/credentials_applier_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..a55d1c06b532c8add6b12189b72e9764387d3ba7
--- /dev/null
+++ b/test/config-luatest/credentials_applier_test.lua
@@ -0,0 +1,480 @@
+local json = require('json')
+local it = require('test.interactive_tarantool')
+local t = require('luatest')
+local treegen = require('test.treegen')
+
+local g = t.group()
+
+local internal = require('internal.config.applier.credentials')._internal
+
+g.before_all(function(g)
+    treegen.init(g)
+end)
+
+g.after_all(function(g)
+    treegen.clean(g)
+end)
+
+g.test_converters = function()
+    -- Guest privileges in format provided by box.schema.{user,role}.info()
+    local box_guest_privileges = {{
+            'execute',
+            'role',
+            'public',
+        }, {
+            'session,usage',
+            'universe',
+        },
+    }
+
+    -- Guest privileges in format provided by config schema
+    local config_guest_data = {
+        privileges = {{
+                permissions = {
+                    'session',
+                    'usage'
+                },
+                universe = true,
+            }
+        },
+        roles = {
+            'public'
+        },
+    }
+
+    -- Guest privileges in format of intermediate representation
+    local intermediate_guest_privileges = {
+        ['user'] = {},
+        ['role'] = {
+            ['public'] = {
+                ['execute'] = true
+            }
+        },
+        ['space'] = {},
+        ['function'] = {},
+        ['sequence'] = {},
+        ['universe'] = {
+            [''] = {
+                ['session'] = true,
+                ['usage'] = true,
+            }
+        },
+    }
+
+    t.assert_equals(internal.privileges_from_box(box_guest_privileges),
+                    intermediate_guest_privileges)
+
+    t.assert_equals(internal.privileges_from_config(config_guest_data),
+                    intermediate_guest_privileges)
+
+
+    local box_admin_privileges = {{
+            'read,write,execute,session,usage,create,drop,alter,reference,' ..
+            'trigger,insert,update,delete',
+            'universe'
+        },
+    }
+
+    local config_admin_data = {
+        privileges = {{
+                permissions = {
+                    'read',
+                    'write',
+                    'execute',
+                    'session',
+                    'usage',
+                    'create',
+                    'drop',
+                    'alter',
+                    'reference',
+                    'trigger',
+                    'insert',
+                    'update',
+                    'delete',
+                },
+                universe = true,
+            },
+        }
+    }
+
+    local intermediate_admin_privileges = {
+        ['user'] = {},
+        ['role'] = {},
+        ['space'] = {},
+        ['function'] = {},
+        ['sequence'] = {},
+        ['universe'] = {
+            [''] = {
+                ['read'] = true,
+                ['write'] = true,
+                ['execute'] = true,
+                ['session'] = true,
+                ['usage'] = true,
+                ['create'] = true,
+                ['drop'] = true,
+                ['alter'] = true,
+                ['reference'] = true,
+                ['trigger'] = true,
+                ['insert'] = true,
+                ['update'] = true,
+                ['delete'] = true,
+            }
+        },
+    }
+
+    t.assert_equals(internal.privileges_from_box(box_admin_privileges),
+                    intermediate_admin_privileges)
+
+    t.assert_equals(internal.privileges_from_config(config_admin_data),
+                    intermediate_admin_privileges)
+
+    local box_replication_privileges = {{
+            'write',
+            'space',
+            '_cluster',
+        }, {
+            'read',
+            'universe',
+        },
+    }
+
+    local config_replication_data = {
+        privileges = {{
+                permissions = {
+                    'write'
+                },
+                spaces = {
+                    '_cluster',
+                },
+            }, {
+                permissions = {
+                    'read'
+                },
+                universe = true,
+            },
+        }
+    }
+
+    local intermediate_replication_privileges = {
+        ['user'] = {},
+        ['role'] = {},
+        ['space'] = {
+            ['_cluster'] = {
+                ['write'] = true,
+            },
+        },
+        ['function'] = {},
+        ['sequence'] = {},
+        ['universe'] = {
+            [''] = {
+                ['read'] = true,
+            },
+        },
+    }
+
+    t.assert_equals(internal.privileges_from_box(box_replication_privileges),
+                    intermediate_replication_privileges)
+
+    t.assert_equals(internal.privileges_from_config(config_replication_data),
+                    intermediate_replication_privileges)
+
+
+    local box_custom_privileges = {{
+            'read,write',
+            'space',
+        }, {
+            'read,write',
+            'sequence',
+            'myseq1',
+        }, {
+            'read,write',
+            'sequence',
+            'myseq2',
+        }, {
+            'execute',
+            'function',
+            'myfunc1',
+        }, {
+            'execute',
+            'function',
+            'myfunc2',
+        }, {
+            'read',
+            'universe',
+        }, {
+            'execute',
+            'role',
+            'myrole1',
+        }, {
+            'execute',
+            'role',
+            'myrole2',
+        }, {
+            'execute',
+            'role',
+            'public',
+        },
+    }
+
+    local config_custom_data = {
+        privileges = {{
+                permissions = {
+                    'read',
+                    'write',
+                },
+                spaces = {
+                    'all',
+                },
+                sequences = {
+                    'myseq1',
+                    'myseq2',
+                },
+            }, {
+                permissions = {
+                    'execute',
+                },
+                functions = {
+                    'myfunc1',
+                    'myfunc2',
+                },
+            }, {
+                permissions = {
+                    'read',
+                },
+                universe = true,
+            },
+        },
+        roles = {
+            'myrole1',
+            'myrole2',
+            'public',
+        }
+    }
+
+    local intermediate_custom_privileges = {
+        ['user'] = {},
+        ['role'] = {
+            ['myrole1'] = {
+                ['execute'] = true,
+            },
+            ['myrole2'] = {
+                ['execute'] = true,
+            },
+            ['public'] = {
+                ['execute'] = true,
+            },
+        },
+        ['space'] = {
+            [''] = {
+                ['read'] = true,
+                ['write'] = true,
+            },
+        },
+        ['function'] = {
+            ['myfunc1'] = {
+                ['execute'] = true,
+            },
+            ['myfunc2'] = {
+                ['execute'] = true,
+            },
+        },
+        ['sequence'] = {
+            ['myseq1'] = {
+                ['read'] = true,
+                ['write'] = true,
+            },
+            ['myseq2'] = {
+                ['read'] = true,
+                ['write'] = true,
+            },
+        },
+        ['universe'] = {
+            [''] = {
+                ['read'] = true,
+            },
+        },
+    }
+
+    t.assert_equals(internal.privileges_from_box(box_custom_privileges),
+                    intermediate_custom_privileges)
+
+    t.assert_equals(internal.privileges_from_config(config_custom_data),
+                    intermediate_custom_privileges)
+end
+
+g.test_privileges_subtract = function()
+    local target = {
+        ['user'] = {},
+        ['role'] = {},
+        ['space'] = {
+            ['myspace1'] = {
+                ['read'] = true,
+                ['write'] = true,
+            },
+            ['myspace2'] = {
+                ['read'] = true,
+            }
+        },
+        ['function'] = {
+            ['myfunc1'] = {
+                ['execute'] = true,
+            },
+        },
+        ['sequence'] = {
+            ['myseq1'] = {
+                ['read'] = true,
+                ['write'] = true,
+            }
+        },
+        ['universe'] = {},
+    }
+
+    local current = {
+        ['user'] = {},
+        ['role'] = {
+            ['myrole1'] = {
+                ['execute'] = true,
+            },
+        },
+        ['space'] = {
+            ['myspace1'] = {
+                ['read'] = true,
+            },
+        },
+        ['function'] = {
+            ['myfunc1'] = {
+                ['execute'] = true,
+            },
+        },
+        ['sequence'] = {},
+        ['universe'] = {},
+    }
+
+    local lack = {{
+            obj_type = 'space',
+            obj_name = 'myspace1',
+            privs = {'write'},
+        }, {
+            obj_type = 'space',
+            obj_name = 'myspace2',
+            privs = {'read'},
+        }, {
+            obj_type = 'sequence',
+            obj_name = 'myseq1',
+            privs = {'read', 'write'},
+        },
+    }
+
+    t.assert_items_equals(internal.privileges_subtract(target, current), lack)
+end
+
+g.test_privileges_add_defaults = function(g)
+    local cases = {
+        {'user', 'guest'},
+        {'user', 'admin'},
+        {'user', '<newly_created>'},
+        {'role', 'public'},
+        {'role', 'replication'},
+        {'role', 'super'},
+        {'role', '<newly_created>'},
+    }
+
+    for _, case in ipairs(cases) do
+        local role_or_user, name = unpack(case)
+
+        local child = it.new()
+        local dir = treegen.prepare_directory(g, {}, {})
+
+        child:execute_command(("box.cfg{work_dir = %q}"):format(dir))
+        child:read_response()
+        if name == '<newly_created>' then
+            name = 'somerandomname'
+            child:execute_command(("box.schema.%s.create('%s')"):format(
+                                   role_or_user, name))
+            child:read_response()
+        end
+        child:execute_command(("box.schema.%s.info('%s')"):format(role_or_user,
+                                                                  name))
+        local box_privileges = child:read_response()
+        box_privileges = internal.privileges_from_box(box_privileges)
+
+        local defaults = {
+            ['user'] = {},
+            ['role'] = {},
+            ['space'] = {},
+            ['function'] = {},
+            ['sequence'] = {},
+            ['universe'] = {},
+        }
+        defaults = internal.privileges_add_defaults(name, role_or_user,
+                                                    defaults)
+
+        t.assert_equals(defaults, box_privileges)
+
+        child:close()
+    end
+end
+
+g.test_sync_privileges = function(g)
+    local box_configuration = {{
+            "grant", "read", "universe", ""
+        }, {
+            "revoke", "session,usage", "universe", ""
+        }, {
+            "grant", "execute", "functions", ""
+        },
+    }
+
+    local config_privileges = {
+        privileges = {{
+                permissions = {
+                    'write',
+                    'execute',
+                },
+                universe = true,
+            }, {
+                permissions = {
+                    'session',
+                    'usage',
+                },
+                universe = true,
+            }
+        },
+        roles = {
+            'public'
+        },
+    }
+
+    local child = it.new()
+    local dir = treegen.prepare_directory(g, {}, {})
+    child:roundtrip(("box.cfg{work_dir = %q}"):format(dir))
+
+    local name = "myuser"
+    child:roundtrip(("box.schema.user.create(%q)"):format(name))
+    for _, command in ipairs(box_configuration) do
+        local action, perm, obj_type, obj_name = unpack(command)
+        local opts = "{if_not_exists = true}"
+        child:roundtrip(("box.schema.user.%s(%q, %q, %q, %q, %s)"):format(
+                         action, name, perm, obj_type, obj_name, opts))
+    end
+    child:roundtrip("sync_privileges = require('internal.config.applier." ..
+                    "credentials')._internal.sync_privileges")
+    child:roundtrip("json = require('json')")
+    child:roundtrip(("config_privileges = json.decode(%q)"):format(
+                     json.encode(config_privileges)))
+    child:roundtrip(("sync_privileges(%q, config_privileges, 'user')")
+                    :format(name))
+
+    child:execute_command(("box.schema.user.info('%s')"):format(name))
+    local result_privileges = child:read_response()
+
+    result_privileges = internal.privileges_from_box(result_privileges)
+    config_privileges = internal.privileges_from_config(config_privileges)
+
+    config_privileges = internal.privileges_add_defaults(name, "user",
+                                                         config_privileges)
+
+    t.assert_equals(result_privileges, config_privileges)
+
+    child:close()
+end