diff options
author | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-18 00:52:30 +0200 |
---|---|---|
committer | Kaarle Ritvanen <kaarle.ritvanen@datakunkku.fi> | 2014-03-25 09:02:58 +0200 |
commit | 751f019580e210ff22fc1ac0eea72cece854534a (patch) | |
tree | 074f3226c7702ff8196207de84ab0e4ec88b5024 | |
parent | 499351fd1c1e7c1462df9a962e927fb4dba6b594 (diff) | |
download | aconf-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.lua | 43 | ||||
-rw-r--r-- | aconf/model/model.lua | 36 | ||||
-rw-r--r-- | aconf/model/node.lua | 96 | ||||
-rw-r--r-- | aconf/transaction/init.lua | 14 | ||||
-rw-r--r-- | server.lua | 101 |
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 @@ -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 |