summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-18 00:52:30 +0200
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-25 09:02:58 +0200
commit751f019580e210ff22fc1ac0eea72cece854534a (patch)
tree074f3226c7702ff8196207de84ab0e4ec88b5024
parent499351fd1c1e7c1462df9a962e927fb4dba6b594 (diff)
downloadaconf-751f019580e210ff22fc1ac0eea72cece854534a.tar.bz2
aconf-751f019580e210ff22fc1ac0eea72cece854534a.tar.xz
move permission checking from server to model
hide all model data and functions inaccessible to the user
-rw-r--r--aconf/model/field.lua43
-rw-r--r--aconf/model/model.lua36
-rw-r--r--aconf/model/node.lua96
-rw-r--r--aconf/transaction/init.lua14
-rw-r--r--server.lua101
5 files changed, 165 insertions, 125 deletions
diff --git a/aconf/model/field.lua b/aconf/model/field.lua
index 26711f2..ec8e2c7 100644
--- a/aconf/model/field.lua
+++ b/aconf/model/field.lua
@@ -68,8 +68,16 @@ function M.Field:init(params)
for _, param in ipairs{'compute', 'store', 'editable'} do
local func = self[param]
- if type(func) == 'string' then
- self[param] = function(obj, ...) return obj[func](obj, ...) end
+ if func then
+ if type(func) == 'string' then
+ local method = func
+ function func(obj, ...) return obj[method](obj, ...) end
+ end
+ if type(func) == 'function' then
+ self[param] = function(obj, ...)
+ return func(node.escalate(obj), ...)
+ end
+ end
end
end
@@ -128,7 +136,8 @@ function M.Field:meta(context)
{
type=self.dtype,
visible=self.visible,
- editable=self:_editable(context),
+ editable=self:_editable(context) and
+ node.has_permission(context.parent, 'modify'),
condition=self.condition,
required=self.required,
default=self.default,
@@ -193,12 +202,6 @@ function M.Field:_validate(context, value)
return value
end
-function M.Field:check_editable(context)
- if not self:_editable(context) then
- raise(context.path, 'Is not editable')
- end
-end
-
function M.Field:check_required(context)
if self.required then raise(context.path, 'Required value not set') end
end
@@ -208,7 +211,10 @@ function M.Field:normalize(context, value) return value end
function M.Field:validate(context, value) end
function M.Field:save(context, value)
- self:check_editable(context)
+ if not self:_editable(context) then
+ raise(context.path, 'Is not editable')
+ end
+
if self.store then self.store(context.parent, value, context.txn)
else self:_save(context, self:_validate(context, value)) end
end
@@ -326,9 +332,10 @@ function M.TreeNode:load(context, options)
options or {}, 'create', self.create
) or self:_load(context)
) then return end
- return self.itype(
+ local res = self.itype(
context, update({editable=self:_editable(context)}, self.iparams)
)
+ return node.has_permission(res, 'read') and res or nil
end
function M.TreeNode:save(context, value)
@@ -342,13 +349,6 @@ function M.TreeNode:save(context, value)
return
end
- local check = value ~= nil and next(value) ~= nil
- if not check then
- local old = self:_load(context)
- if old and next(old) ~= nil then check = true end
- end
- if check then self:check_editable(context) end
-
self:_save(context)
if value then
@@ -364,6 +364,8 @@ function M.TreeNode:save(context, value)
errors:collect(self.save_member, new, k, v)
end
errors:raise()
+
+ return new
end
end
@@ -384,5 +386,10 @@ function M.Model:init(params)
self.dtype = 'model'
end
+function M.Model:save(context, value)
+ local new = super(self, M.Model):save(context, value)
+ if new then node.check_permission(new, 'create') end
+end
+
return M
diff --git a/aconf/model/model.lua b/aconf/model/model.lua
index 3f90450..5d4145f 100644
--- a/aconf/model/model.lua
+++ b/aconf/model/model.lua
@@ -153,6 +153,8 @@ function M.Model:init(context)
assert(mt.txn)
if isinstance(v, M.Action) then
+ mt.check_permission(k)
+
local f = v.field and BoundMember(self, k, v.field)
if options.create then return f and f:load(options) end
@@ -164,7 +166,7 @@ function M.Model:init(context)
'Action does not accept an input argument'
)
end
- local res = v.func(self, f and f:load())
+ local res = v.func(mt.escalate, f and f:load())
if f then f:_save() end
return res
end
@@ -174,9 +176,13 @@ function M.Model:init(context)
end
if self.is_removable then
- function mt.removable() return self:is_removable() end
+ function mt.removable()
+ return mt.has_permission('delete') and self:is_removable()
+ end
end
+ function mt.removing_permitted() return mt.has_permission('modify') end
+
local value_removable = mt.value_removable
function mt.value_removable(v)
return type(v) == 'table' and value_removable(v)
@@ -190,7 +196,11 @@ function M.Model:init(context)
function mt.save(k, v)
k = normalize_name(k)
local field = mt.member(k, false, Field)
- if isinstance(field, fld.TreeNode) then mt.check_removable(k, v) end
+
+ if v == nil and isinstance(field, fld.TreeNode) then
+ mt.check_removable(k)
+ else mt.check_permission('modify') end
+
return field:save(v)
end
@@ -199,7 +209,15 @@ function M.Model:init(context)
end
function mt.init_meta(meta)
- util.update(meta, {fields=tmeta(Field), actions=tmeta(M.Action)})
+ util.update(
+ meta,
+ {
+ fields=tmeta(Field),
+ actions=util.filter(
+ function(a) mt.has_permission(a.name) end, tmeta(M.Action)
+ )
+ }
+ )
end
function mt.members()
@@ -215,12 +233,13 @@ function M.Model:init(context)
end
if self.has_permission then
- function mt.has_permission(user, permission)
- return self:has_permission(user, permission)
+ function mt.has_permission(permission)
+ return mt.privileged or self:has_permission(mt.txn.user, permission)
end
end
- for _, f in ipairs(_members(Model)) do mt.load(f.name) end
+ local tload = getmetatable(mt.escalate).load
+ for _, f in ipairs(_members(Model)) do tload(f.name) end
end
function M.Model:fetch(path, create)
@@ -228,8 +247,9 @@ function M.Model:fetch(path, create)
end
function M.Model:match(filter)
+ local tload = getmetatable(getmetatable(self).escalate).load
for k, v in pairs(filter) do
- if not util.contains(v, getmetatable(self).load(k)) then return false end
+ if not util.contains(v, tload(k)) then return false end
end
return true
end
diff --git a/aconf/model/node.lua b/aconf/model/node.lua
index 92f3a1f..50edac6 100644
--- a/aconf/model/node.lua
+++ b/aconf/model/node.lua
@@ -40,6 +40,7 @@ function M.BoundMember:init(parent, name, field)
field,
{
txn=pmt.txn,
+ privileged=pmt.privileged,
parent=parent,
path=pth.join(pmt.path, name),
addr=pth.to_absolute(addr, pmt.addr)
@@ -66,13 +67,18 @@ function M.TreeNode:init(context, params)
mt.name = pth.name(mt.path)
mt.__eq = equal_tns
+ if not (mt.txn and mt.txn.user) then mt.privileged = true end
+ mt.escalate = mt.privileged and self or mt.class(
+ setdefaults({privileged=true}, context), params
+ )
+
function mt.get(k, options) return mt.load(k, options) end
function mt.fetch(path, create)
if type(path) == 'string' then
if pth.is_absolute(path) and mt.path > '/' then
assert(not create)
- return mt.txn:fetch(path)
+ return mt.txn:fetch(path, mt.privileged)
end
path = pth.split(path)
end
@@ -111,6 +117,26 @@ function M.TreeNode:init(context, params)
return getmetatable(next).fetch(path, create)
end
+ function mt.has_permission(permission)
+ if mt.privileged then return true end
+
+ local p = permission..mt.path
+ if getmetatable(mt.escalate).fetch('/auth/permissions')[p] then
+ return mt.txn.user:check_permission(p)
+ end
+
+ if ({create=true, delete=true})[permission] then
+ permission = 'modify'
+ end
+ return getmetatable(mt.parent).has_permission(permission)
+ end
+
+ function mt.check_permission(permission)
+ if not mt.has_permission(permission) then
+ raise('forbidden', permission..mt.path)
+ end
+ end
+
function mt.removable() end
function mt.value_removable(v)
@@ -118,13 +144,15 @@ function M.TreeNode:init(context, params)
end
local function key_removable(k)
+ if not mt.removing_permitted() then return false end
+
local res = mt.value_removable(mt.load(k, {dereference=false}))
if res == nil then return params.editable end
return res
end
- function mt.check_removable(k, v)
- if v == nil and not key_removable(k) then
+ function mt.check_removable(k)
+ if not key_removable(k) then
raise(pth.join(mt.path, k), 'Cannot be deleted')
end
end
@@ -155,18 +183,6 @@ function M.TreeNode:init(context, params)
function mt.__index(t, k) return mt.get(k, {private=true}) end
function mt.__newindex(t, k, v) mt.save(k, v) end
- function mt.has_permission(user, permission)
- local p = permission..mt.path
- if mt.fetch('/auth/permissions')[p] then
- return user:check_permission(p)
- end
-
- if ({create=true, delete=true})[permission] then
- permission = 'modify'
- end
- return M.has_permission(mt.parent, user, permission)
- end
-
mt.txn.validable[mt.path] = mt.addr
end
@@ -175,7 +191,8 @@ function M.TreeNode:search_refs(path)
if #path == 0 then return {} end
- local mt = getmetatable(self)
+ local mt = getmetatable(getmetatable(self).escalate)
+
local name = path[1]
table.remove(path, 1)
@@ -224,13 +241,15 @@ function M.Collection:init(context, params)
function mt.load(k, options) return mt.member(k):load(options) end
+ function mt.removing_permitted() return mt.has_permission('delete') end
+
if not mt.txn then return end
function mt.init_meta(meta)
update(
meta,
{
- editable=params.editable,
+ editable=params.editable and mt.has_permission('create'),
members=field:meta(),
required=params.required,
['ui-member']=params.ui_member or meta['ui-name']:gsub('s$', ''),
@@ -260,13 +279,26 @@ function M.Collection:init(context, params)
validate(mt.parent)
end
end
-
+
function mt.save(k, v)
- if not params.editable then
- raise(mt.path, 'Collection is not editable')
- end
+ local delete = v == nil
+ local old = mt.load(k, {dereference=false})
+
+ if old == nil then
+ if delete then return end
+ if not params.editable then
+ raise(mt.path, 'Collection is not editable')
+ end
+ mt.check_permission('create')
+
+ elseif delete then mt.check_removable(k)
+
+ elseif type(old) == 'table' then
+ mt.check_removable(k)
+ mt.check_permission('create')
+
+ else mt.check_permission('modify') end
- mt.check_removable(k, v)
if params.key then
local kf = M.BoundMember(self, k, params.key)
@@ -287,26 +319,32 @@ function M.List:init(context, params)
super(self, M.List):init(context, setdefaults(params, {dtype='list'}))
local mt = getmetatable(self)
+ local tmt = getmetatable(mt.escalate)
+
+ mt._save = mt.save
- local save = mt.save
function mt.save(k, v)
assert(type(k) == 'number')
if v == nil then
+ mt.check_removable(k)
local len = #mt.members()
while k < len do
- save(k, mt.load(k + 1, {dereference=false}))
+ tmt._save(k, tmt.load(k + 1, {dereference=false}))
k = k + 1
end
- end
- save(k, v)
+ tmt._save(len)
+ else mt._save(k, v) end
end
function mt.insert(v, i)
assert(v ~= nil)
+ mt.check_permission('create')
local len = #mt.members()
if not i then i = len + 1 end
- for j = len,i,-1 do save(j + 1, mt.load(j, {dereference=false})) end
- save(i, v)
+ for j = len,i,-1 do
+ tmt._save(j + 1, tmt.load(j, {dereference=false}))
+ end
+ tmt._save(i, v)
end
end
@@ -336,7 +374,9 @@ end
for _, mf in ipairs{
'addr',
+ 'check_permission',
'contains',
+ 'escalate',
'fetch',
'has_permission',
'insert',
diff --git a/aconf/transaction/init.lua b/aconf/transaction/init.lua
index aba1b66..d4c79fe 100644
--- a/aconf/transaction/init.lua
+++ b/aconf/transaction/init.lua
@@ -19,12 +19,14 @@ local ModelTransaction = object.class(
require('aconf.transaction.base').Transaction
)
-function ModelTransaction:init(backend, validate)
+function ModelTransaction:init(backend, validate, user)
super(self, ModelTransaction):init(backend)
self.validate = validate
self.validable = {}
+ self.user = user
+
self.root = root.RootModel{txn=self}
end
@@ -79,7 +81,10 @@ function ModelTransaction:check_deleted(path)
end
end
-function ModelTransaction:fetch(path) return self.root:fetch(path) end
+function ModelTransaction:fetch(path, escalate)
+ local root = self.root
+ return (escalate and getmetatable(root).escalate or root):fetch(path)
+end
function ModelTransaction:meta(path) return self.root:meta(path) end
@@ -93,7 +98,7 @@ function ModelTransaction:commit()
local function validate(path)
if path > '/' then validate(pth.parent(path)) end
if not self.commit_val[path] then return end
- errors:collect(getmetatable(self:fetch(path)).validate)
+ errors:collect(getmetatable(self:fetch(path, true)).validate)
self.commit_val[path] = nil
end
@@ -119,6 +124,7 @@ return function(options)
options = options or {}
return ModelTransaction(
options.parent or (options.allow_commit_defer and def_store or store),
- not (options.parent and options.defer_validation)
+ not (options.parent and options.defer_validation),
+ options.parent and options.parent.user or options.user
)
end
diff --git a/server.lua b/server.lua
index 7e6620f..bbb7798 100644
--- a/server.lua
+++ b/server.lua
@@ -67,7 +67,7 @@ return function(env)
end
end
- local session, user, txn_id
+ local session
function reset_session_expiry() session.expires = os.time() + 600 end
local sid = tonumber(env.HTTP_X_ACONF_AUTH_TOKEN)
@@ -75,47 +75,20 @@ return function(env)
session = sessions[sid]
if not session then return 401 end
reset_session_expiry()
- user = session.user
- txn_id = tonumber(env.HTTP_X_ACONF_TRANSACTION_ID)
- end
-
- local parent_txn
- if txn_id then
- parent_txn = session.txns[txn_id]
- if not parent_txn then
- return 400, nil, 'Invalid transaction ID'
- end
- end
-
- local function new_txn(defer_validation)
- return aconf.start_txn{
- allow_commit_defer=true,
- defer_validation=defer_validation,
- parent=parent_txn
- }
- end
-
- local txn = new_txn(true)
-
- local function fetch_user(name)
- user = name and txn:fetch('/auth/users')[name]
- end
- if user then
- fetch_user(user)
- if not user then return 401 end
end
if path == '/login' then
if method == 'POST' then
if not data.username or not data.password then return 401 end
- fetch_user(data.username)
+ local user =
+ aconf.start_txn():fetch('/auth/users')[data.username]
if user and user:check_password(data.password) then
local sid
repeat
sid = math.floor(math.random() * 2^32)
until not sessions[sid]
- session = {user=data.username, last_txn_id=0, txns={}}
+ session = {user=user, last_txn_id=0, txns={}}
reset_session_expiry()
sessions[sid] = session
return 204, {
@@ -126,7 +99,7 @@ return function(env)
return 401
end
- if not user then return 401 end
+ if not session then return 401 end
if method == 'DELETE' then
sessions[sid] = nil
@@ -136,7 +109,7 @@ return function(env)
return 405
end
- if not user then return 401 end
+ if not session then return 401 end
if path == '/save' then
if not save_req then return 404 end
@@ -147,6 +120,27 @@ return function(env)
local success, code, hdr, res = aconf.call(
function()
+ local txn_id = tonumber(env.HTTP_X_ACONF_TRANSACTION_ID)
+ local parent_txn
+
+ if txn_id then
+ parent_txn = session.txns[txn_id]
+ if not parent_txn then
+ return 400, nil, 'Invalid transaction ID'
+ end
+ end
+
+ local function new_txn(defer_validation)
+ return aconf.start_txn{
+ allow_commit_defer=true,
+ defer_validation=defer_validation,
+ parent=parent_txn,
+ user=session.user
+ }
+ end
+
+ local txn = new_txn(true)
+
if stringy.startswith(path, '/meta/') then
if method ~= 'GET' then return 405 end
return 200, nil, txn:meta(path:sub(6, -1))
@@ -167,30 +161,25 @@ return function(env)
if type(obj) == 'function' then return 404 end
if isinstance(obj, mnode.TreeNode) then
- if not mnode.has_permission(obj, user, 'read') then
- return 403
- end
-
local node = {}
for k, v in mnode.pairs(obj) do
local readable = true
if isinstance(v, mnode.TreeNode) then
- readable = mnode.has_permission(v, user, 'read')
v = mnode.path(v)
elseif isinstance(v, mbin.Data) then v = v.path end
- if readable then node[k] = v end
+ node[k] = v
end
res = {data=node, meta=mnode.meta(obj)}
- elseif mnode.has_permission(parent, user, 'read') then
+ else
if isinstance(obj, mbin.Data) then
obj = obj:encode()
end
res = {data=obj, meta=mnode.mmeta(parent, name)}
- else return 403 end
+ end
return 200, nil, res
end
@@ -199,10 +188,6 @@ return function(env)
local obj = txn:fetch(path)
if isinstance(obj, mnode.List) then
- if not mnode.has_permission(obj, user, 'create') then
- return 403
- end
-
local index
if not isinstance(obj, mnode.Set) then
index = data.index
@@ -210,37 +195,17 @@ return function(env)
end
mnode.insert(obj, data, index)
- elseif type(obj) == 'function' then
- if not mnode.has_permission(parent, user, name) then
- return 403
- end
- res = obj(data)
+ elseif type(obj) == 'function' then res = obj(data)
else return 405 end
else
- local obj = parent[name]
- if obj ~= nil and not isinstance(obj, mnode.TreeNode) then
- obj = parent
- end
-
if method == 'DELETE' then
- if obj == nil then return 404 end
- if not mnode.has_permission(obj, user, 'delete') then
- return 403
- end
+ if parent[name] == nil then return 404 end
parent[name] = nil
elseif method == 'PUT' then
if isinstance(parent, mnode.Set) then return 405 end
- local permission = 'modify'
- if obj == nil then
- obj = parent
- permission = 'create'
- end
- if not mnode.has_permission(obj, user, permission) then
- return 403
- end
parent[name] = data
else return 405 end
@@ -272,6 +237,8 @@ return function(env)
if success then return code, hdr, res, true end
+ if code.forbidden then return 403, nil, code.forbidden, true end
+
if code.conflict then return 409, nil, code.conflict, true end
return 422, nil, code, true