--[[
Copyright (c) 2012-2018 Kaarle Ritvanen
See LICENSE file for license details
--]]

local M = require('aconf.model')
local node = require('aconf.model.node')
local object = require('aconf.object')
local map = require('aconf.util').map

local digest = require('openssl.digest')


local conf_addr = '/json'..require('posix').getcwd()..'/config'


local Role = M.new()
Role.permissions = M.Set{type=M.Reference{scope='../../../permissions'}}


local function hash_password(algorithm, salt, password)
   return algorithm..'$'..salt..'$'..table.concat(
      map(
	 function(b) return ('%02x'):format(b) end,
	 {string.byte(digest.new(algorithm):final(salt..password), 1, -1)}
      )
   )
end

local hash_pattern = '^(%w+)%$(%w+)%$%x+$'


local Password = object.class(M.String)

function Password:init() object.super(self, Password):init{detail=true} end

function Password:normalize(context, value)
   if value:find(hash_pattern) then return value end

   local salt = ''
   for i = 1,12 do
      local c = math.random(48, 109)
      if c > 57 then c = c + 7 end
      if c > 90 then c = c + 6 end
      salt = salt..string.char(c)
   end
   return hash_password('sha256', salt, value)
end


local User = M.new()
User.password = Password
User.real_name = M.String
User.superuser = M.Boolean{default=false}
User.roles = M.Set{type=M.Reference{scope='../../../roles'}}

function User:check_password(password)
   if not self.password then return false end
   local _, _, algorithm, salt = self.password:find(hash_pattern)
   if not salt then return false end
   return hash_password(algorithm, salt, password) == self.password
end

function User:check_permission(permission)
   local mt = getmetatable(self)
   if not mt.permissions then
      mt.permissions = {}
      for _, p in pairs(self:fetch('../../permissions')) do
	 mt.permissions[p] = false
      end
      for _, role in node.pairs(self.roles, true) do
	 for _, p in node.pairs(role.permissions, true) do
	    mt.permissions[p] = true
	 end
      end
   end
   return mt.permissions[permission]
end


local Record = M.new()
Record.user = M.String{required=true, editable=false}
Record.action = M.String{required=true, editable=false}
Record.path = M.String{editable=false}
Record.data = M.String{editable=false}
Record.timestamp = M.time.Timestamp{editable=false}


local Aaa = M.new()
Aaa.users = M.Collection{type=User}
Aaa.roles = M.Collection{type=Role}
Aaa.permissions = M.Set{
   type=M.String, addr='/volatile/aaa/permissions', editable=false
}
Aaa.audit_trail = M.List{
   type=Record,
   addr=conf_addr..'/audit.json',
   editable=false,
   ui_name='Audit trail',
   ui_member='Record'
}
Aaa.action_log = M.List{type=Record, addr=node.null_addr, visible=false}

function Aaa:validate()
   local time = os.time()
   for _, action in pairs(self.action_log) do
      action.timestamp = time
      node.insert(self.audit_trail, action)
   end
end


M.register('aaa', Aaa, {addr=conf_addr..'/auth.json', ui_name='AAA'})

M.permission.defaults('/aaa')
