feat: add vim.secure.read()

This function accepts a path to a file and prompts the user if the file
is trusted. If the user confirms that the file is trusted, the contents
of the file are returned. The user's decision is stored in a trust
database at $XDG_STATE_HOME/nvim/trust. When this function is invoked
with a path that is already marked as trusted in the trust database, the
user is not prompted for a response.
This commit is contained in:
Gregory Anders 2022-11-05 13:37:05 -06:00
parent 9736605672
commit f1922e78a1
6 changed files with 300 additions and 0 deletions

View File

@ -2354,4 +2354,20 @@ parents({start}) *vim.fs.parents()*
Return: ~
(function) Iterator
==============================================================================
Lua module: secure *lua-secure*
read({path}) *vim.secure.read()*
Attempt to read the file at {path} prompting the user if the file should
be trusted. The user's choice is persisted in a trust database at
$XDG_STATE_HOME/nvim/trust.
Parameters: ~
• {path} (string) Path to a file to read.
Return: ~
(string|nil) The contents of the given file if it exists and is
trusted, or nil otherwise.
vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl:

View File

@ -39,6 +39,9 @@ NEW FEATURES *news-features*
The following new APIs or features were added.
• |vim.secure.read()| reads a file and prompts the user if it should be
trusted and, if so, returns the file's contents.
• When using Nvim inside tmux 3.2 or later, the default clipboard provider
will now copy to the system clipboard. |provider-clipboard|

View File

@ -36,6 +36,7 @@ for k, v in pairs({
ui = true,
health = true,
fs = true,
secure = true,
}) do
vim._submodules[k] = v
end

106
runtime/lua/vim/secure.lua Normal file
View File

@ -0,0 +1,106 @@
local M = {}
--- Attempt to read the file at {path} prompting the user if the file should be
--- trusted. The user's choice is persisted in a trust database at
--- $XDG_STATE_HOME/nvim/trust.
---
---@param path (string) Path to a file to read.
---
---@return (string|nil) The contents of the given file if it exists and is
--- trusted, or nil otherwise.
function M.read(path)
vim.validate({ path = { path, 's' } })
local fullpath = vim.loop.fs_realpath(vim.fs.normalize(path))
if not fullpath then
return nil
end
local trust = {}
do
local f = io.open(vim.fn.stdpath('state') .. '/trust', 'r')
if f then
local contents = f:read('*a')
if contents then
for line in vim.gsplit(contents, '\n') do
local hash, file = string.match(line, '^(%S+) (.+)$')
if hash and file then
trust[file] = hash
end
end
end
f:close()
end
end
if trust[fullpath] == '!' then
-- File is denied
return nil
end
local contents
do
local f = io.open(fullpath, 'r')
if not f then
return nil
end
contents = f:read('*a')
f:close()
end
local hash = vim.fn.sha256(contents)
if trust[fullpath] == hash then
-- File already exists in trust database
return contents
end
-- File either does not exist in trust database or the hash does not match
local choice = vim.fn.confirm(
string.format('%s is not trusted.', fullpath),
'&ignore\n&view\n&deny\n&allow',
1
)
if choice == 0 or choice == 1 then
-- Cancelled or ignored
return nil
elseif choice == 2 then
-- View
vim.cmd('new')
local buf = vim.api.nvim_get_current_buf()
local lines = vim.split(string.gsub(contents, '\n$', ''), '\n')
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].bufhidden = 'hide'
vim.bo[buf].buftype = 'nofile'
vim.bo[buf].swapfile = false
vim.bo[buf].modeline = false
vim.bo[buf].buflisted = false
vim.bo[buf].readonly = true
vim.bo[buf].modifiable = false
return nil
elseif choice == 3 then
-- Deny
trust[fullpath] = '!'
contents = nil
elseif choice == 4 then
-- Allow
trust[fullpath] = hash
end
do
local f, err = io.open(vim.fn.stdpath('state') .. '/trust', 'w')
if not f then
error(err)
end
local t = {}
for p, h in pairs(trust) do
t[#t + 1] = string.format('%s %s\n', h, p)
end
f:write(table.concat(t))
f:close()
end
return contents
end
return M

View File

@ -131,6 +131,7 @@ CONFIG = {
'filetype.lua',
'keymap.lua',
'fs.lua',
'secure.lua',
],
'files': [
'runtime/lua/vim/_editor.lua',
@ -140,6 +141,7 @@ CONFIG = {
'runtime/lua/vim/filetype.lua',
'runtime/lua/vim/keymap.lua',
'runtime/lua/vim/fs.lua',
'runtime/lua/vim/secure.lua',
],
'file_patterns': '*.lua',
'fn_name_prefix': '',
@ -166,6 +168,7 @@ CONFIG = {
'filetype': 'vim.filetype',
'keymap': 'vim.keymap',
'fs': 'vim.fs',
'secure': 'vim.secure',
},
'append_only': [
'shared.lua',

View File

@ -0,0 +1,171 @@
local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen')
local eq = helpers.eq
local clear = helpers.clear
local command = helpers.command
local pathsep = helpers.get_pathsep()
local iswin = helpers.iswin()
local curbufmeths = helpers.curbufmeths
local exec_lua = helpers.exec_lua
local feed_command = helpers.feed_command
local feed = helpers.feed
local funcs = helpers.funcs
local pcall_err = helpers.pcall_err
describe('vim.secure', function()
describe('read()', function()
local xstate = 'Xstate'
setup(function()
helpers.mkdir_p(xstate .. pathsep .. (iswin and 'nvim-data' or 'nvim'))
end)
teardown(function()
helpers.rmdir(xstate)
end)
before_each(function()
helpers.write_file('Xfile', [[
let g:foobar = 42
]])
clear{env={XDG_STATE_HOME=xstate}}
end)
after_each(function()
os.remove('Xfile')
helpers.rmdir(xstate)
end)
it('works', function()
local screen = Screen.new(80, 8)
screen:attach()
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
[2] = {bold = true, reverse = true},
[3] = {bold = true, foreground = Screen.colors.SeaGreen},
[4] = {reverse = true},
})
local cwd = funcs.getcwd()
-- Need to use feed_command instead of exec_lua because of the confirmation prompt
feed_command([[lua vim.secure.read('Xfile')]])
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
{2: }|
:lua vim.secure.read('Xfile') |
{3:]] .. cwd .. pathsep .. [[Xfile is untrusted}{MATCH:%s+}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^ |
]]}
feed('d')
screen:expect{grid=[[
^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
local trust = helpers.read_file(funcs.stdpath('state') .. pathsep .. 'trust')
eq(string.format('! %s', cwd .. pathsep .. 'Xfile'), vim.trim(trust))
eq(helpers.NIL, exec_lua([[return vim.secure.read('Xfile')]]))
os.remove(funcs.stdpath('state') .. pathsep .. 'trust')
feed_command([[lua vim.secure.read('Xfile')]])
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
{2: }|
:lua vim.secure.read('Xfile') |
{3:]] .. cwd .. pathsep .. [[Xfile is untrusted}{MATCH:%s+}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^ |
]]}
feed('a')
screen:expect{grid=[[
^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
local hash = funcs.sha256(helpers.read_file('Xfile'))
trust = helpers.read_file(funcs.stdpath('state') .. pathsep .. 'trust')
eq(string.format('%s %s', hash, cwd .. pathsep .. 'Xfile'), vim.trim(trust))
eq(helpers.NIL, exec_lua([[vim.secure.read('Xfile')]]))
os.remove(funcs.stdpath('state') .. pathsep .. 'trust')
feed_command([[lua vim.secure.read('Xfile')]])
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
{2: }|
:lua vim.secure.read('Xfile') |
{3:]] .. cwd .. pathsep .. [[Xfile is untrusted}{MATCH:%s+}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^ |
]]}
feed('i')
screen:expect{grid=[[
^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
-- Trust database is not updated
trust = helpers.read_file(funcs.stdpath('state') .. pathsep .. 'trust')
eq(nil, trust)
feed_command([[lua vim.secure.read('Xfile')]])
screen:expect{grid=[[
|
{1:~ }|
{1:~ }|
{1:~ }|
{2: }|
:lua vim.secure.read('Xfile') |
{3:]] .. cwd .. pathsep .. [[Xfile is untrusted}{MATCH:%s+}|
{3:[i]gnore, (v)iew, (d)eny, (a)llow: }^ |
]]}
feed('v')
screen:expect{grid=[[
^ let g:foobar = 42 |
{1:~ }|
{1:~ }|
{2:]] .. cwd .. pathsep .. [[Xfile [RO]{MATCH:%s+}|
|
{1:~ }|
{4:[No Name] }|
|
]]}
-- Trust database is not updated
trust = helpers.read_file(funcs.stdpath('state') .. pathsep .. 'trust')
eq(nil, trust)
-- Cannot write file
pcall_err(command, 'write')
eq(false, curbufmeths.get_option('modifiable'))
end)
end)
end)