bloodhound-1.0.0.0: tests/Test/SecurityPrivilegesSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module Test.SecurityPrivilegesSpec (spec) where
import Data.Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import TestsUtils.Import
import Prelude
spec :: Spec
spec = do
describe "FieldSecurity" $ do
it "round-trips grant + except arrays" $ do
let fs =
FieldSecurity
{ fsGrant = Just (FieldName "a" :| [FieldName "b"]),
fsExcept = Just (FieldName "secret" :| [])
}
let Just decoded = decode (encode fs) :: Maybe FieldSecurity
(NE.toList <$> fsGrant decoded) `shouldBe` Just [FieldName "a", FieldName "b"]
(NE.toList <$> fsExcept decoded) `shouldBe` Just [FieldName "secret"]
it "decodes a bare-string grant as a singleton list" $ do
let Just fs = decode "{\"grant\":\"email\"}" :: Maybe FieldSecurity
(NE.toList <$> fsGrant fs) `shouldBe` Just [FieldName "email"]
fsExcept fs `shouldBe` Nothing
it "treats an empty grant array as Nothing" $ do
let Just fs = decode "{\"grant\":[]}" :: Maybe FieldSecurity
fsGrant fs `shouldBe` Nothing
describe "IndexPrivilege" $ do
it "round-trips a full entry with field_security, query and extras" $ do
let bytes =
"{\
\ \"names\": [\"logs-*\", \"metrics-*\"],\
\ \"privileges\": [\"read\", \"view_index_metadata\"],\
\ \"field_security\": {\"grant\": [\"a\"], \"except\": [\"b\"]},\
\ \"query\": {\"term\": {\"active\": true}},\
\ \"description\": \"custom note\"\
\}"
let Just decoded = decode bytes :: Maybe IndexPrivilege
NE.toList (ipNames decoded)
`shouldBe` [IndexPattern "logs-*", IndexPattern "metrics-*"]
NE.toList (ipPrivileges decoded)
`shouldBe` ["read", "view_index_metadata"]
ipFieldSecurity decoded `shouldSatisfy` isJust
ipQuery decoded `shouldSatisfy` isJust
-- The unknown @description@ survives in extras.
KM.lookup "description" (ipExtras decoded) `shouldSatisfy` isJust
-- Round-trip is stable.
let Just rt = decode (encode decoded) :: Maybe IndexPrivilege
NE.toList (ipNames rt) `shouldBe` [IndexPattern "logs-*", IndexPattern "metrics-*"]
it "decodes a bare-string names field" $ do
let Just decoded = decode "{\"names\":\"logs\",\"privileges\":[\"read\"]}" :: Maybe IndexPrivilege
NE.toList (ipNames decoded) `shouldBe` [IndexPattern "logs"]
it "rejects an empty names array" $ do
decode "{\"names\":[],\"privileges\":[\"read\"]}" `shouldBe` (Nothing :: Maybe IndexPrivilege)
it "rejects names missing entirely" $ do
decode "{\"privileges\":[\"read\"]}" `shouldBe` (Nothing :: Maybe IndexPrivilege)
describe "ApplicationPrivilege" $ do
it "round-trips application + privileges + resources + extras" $ do
let bytes =
"{\
\ \"application\": \"myapp\",\
\ \"privileges\": [\"read\", \"write\"],\
\ \"resources\": [\"index:1\", \"*\"],\
\ \"note\": \"extra\"\
\}"
let Just decoded = decode bytes :: Maybe ApplicationPrivilege
apvApplication decoded `shouldBe` "myapp"
apvPrivileges decoded `shouldBe` ["read", "write"]
apvResources decoded `shouldBe` ["index:1", "*"]
KM.lookup "note" (apvExtras decoded) `shouldSatisfy` isJust
it "tolerates an absent resources field" $ do
let Just decoded =
decode
"{\"application\":\"myapp\",\"privileges\":[\"read\"]}" ::
Maybe ApplicationPrivilege
apvResources decoded `shouldBe` []
it "decodes a bare-string privileges field" $ do
let Just decoded =
decode
"{\"application\":\"myapp\",\"privileges\":\"read\",\"resources\":[\"*\"]}" ::
Maybe ApplicationPrivilege
apvPrivileges decoded `shouldBe` ["read"]
describe "ActionName" $ do
it "round-trips through JSON and IsString" $ do
let Just decoded = decode "\"data:read/*\"" :: Maybe ActionName
decoded `shouldBe` "data:read/*"
encode decoded `shouldBe` "\"data:read/*\""
describe "ApplicationPrivilegeDefinition" $ do
it "round-trips actions + metadata + extras" $ do
let bytes =
"{\
\ \"actions\": [\"data:read/*\", \"action:login\"],\
\ \"metadata\": {\"description\": \"Read access\"},\
\ \"comment\": \"extra\"\
\}"
Just decoded = decode bytes :: Maybe ApplicationPrivilegeDefinition
NE.toList (apdActions decoded) `shouldBe` ["data:read/*", "action:login"]
apdMetadata decoded `shouldSatisfy` isJust
KM.lookup "comment" (apdExtras decoded) `shouldSatisfy` isJust
let Just rt = decode (encode decoded) :: Maybe ApplicationPrivilegeDefinition
NE.toList (apdActions rt) `shouldBe` ["data:read/*", "action:login"]
KM.lookup "comment" (apdExtras rt) `shouldSatisfy` isJust
it "decodes a bare-string action as a singleton" $ do
let Just decoded = decode "{\"actions\":\"action:login\"}" :: Maybe ApplicationPrivilegeDefinition
NE.toList (apdActions decoded) `shouldBe` ["action:login"]
it "rejects an empty actions array" $ do
decode "{\"actions\":[]}" `shouldBe` (Nothing :: Maybe ApplicationPrivilegeDefinition)
it "rejects actions missing entirely" $ do
decode "{\"metadata\":{}}" `shouldBe` (Nothing :: Maybe ApplicationPrivilegeDefinition)
describe "ApplicationPrivilegeInfo" $ do
it "decodes the GET leaf with application, name, actions, metadata" $ do
let bytes =
"{\
\ \"application\": \"myapp\",\
\ \"name\": \"read\",\
\ \"actions\": [\"data:read/*\", \"action:login\"],\
\ \"metadata\": {\"description\": \"Read access to myapp\"}\
\}"
Just decoded = decode bytes :: Maybe ApplicationPrivilegeInfo
apiApplication decoded `shouldBe` "myapp"
apiName decoded `shouldBe` "read"
apiActions decoded `shouldBe` ["data:read/*", "action:login"]
apiMetadata decoded `shouldSatisfy` isJust
it "tolerates absent actions (defaults to [])" $ do
let Just decoded = decode "{\"application\":\"myapp\",\"name\":\"read\"}" :: Maybe ApplicationPrivilegeInfo
apiActions decoded `shouldBe` []
it "carries unknown fields in extras" $ do
let Just decoded = decode "{\"application\":\"myapp\",\"name\":\"read\",\"actions\":[\"*\"],\"note\":\"x\"}" :: Maybe ApplicationPrivilegeInfo
KM.lookup "note" (apiExtras decoded) `shouldSatisfy` isJust
describe "PrivilegesListResponse" $ do
it "decodes the two-level {app:{name:leaf}} envelope" $ do
let bytes =
"{\"myapp\":{\"read\":{\"application\":\"myapp\",\"name\":\"read\",\"actions\":[\"data:read/*\"]}}}"
Just decoded = decode bytes :: Maybe PrivilegesListResponse
case privilegesListEntries decoded of
[(ApplicationName app, [(ApplicationPrivilegeName priv, info)])] -> do
app `shouldBe` "myapp"
priv `shouldBe` "read"
apiApplication info `shouldBe` "myapp"
apiActions info `shouldBe` ["data:read/*"]
other -> expectationFailure $ "unexpected entries: " <> show other
it "round-trips a multi-app, multi-privilege envelope" $ do
let bytes =
"{\
\ \"app01\": {\
\ \"read\": {\"application\": \"app01\", \"name\": \"read\", \"actions\": [\"*\"]},\
\ \"write\": {\"application\": \"app01\", \"name\": \"write\", \"actions\": [\"data:write/*\"]}\
\ },\
\ \"app02\": {\
\ \"all\": {\"application\": \"app02\", \"name\": \"all\", \"actions\": [\"*\"]}\
\ }\
\}"
Just decoded = decode bytes :: Maybe PrivilegesListResponse
Just rt = decode (encode decoded) :: Maybe PrivilegesListResponse
length (privilegesListEntries decoded) `shouldBe` 2
privilegesListEntries rt `shouldBe` privilegesListEntries decoded
describe "PrivilegeCreatedResponse" $ do
it "decodes {app:{name:{created:true}}} to created True" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"created\":true}}}" :: Maybe PrivilegeCreatedResponse
pcCreated decoded `shouldBe` True
it "decodes created:false on update" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"created\":false}}}" :: Maybe PrivilegeCreatedResponse
pcCreated decoded `shouldBe` False
it "re-decodes after encode to the same value" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"created\":true}}}" :: Maybe PrivilegeCreatedResponse
Just rt = decode (encode decoded) :: Maybe PrivilegeCreatedResponse
rt `shouldBe` decoded
describe "PrivilegeDeletedResponse" $ do
it "decodes {app:{name:{found:true}}}" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"found\":true}}}" :: Maybe PrivilegeDeletedResponse
pdFound decoded `shouldBe` Just True
it "decodes found:false when the privilege was absent" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"found\":false}}}" :: Maybe PrivilegeDeletedResponse
pdFound decoded `shouldBe` Just False
it "re-decodes after encode to the same value (bare-leaf fallback)" $ do
let Just decoded = decode "{\"myapp\":{\"read\":{\"found\":true}}}" :: Maybe PrivilegeDeletedResponse
Just rt = decode (encode decoded) :: Maybe PrivilegeDeletedResponse
rt `shouldBe` decoded
describe "ClearPrivilegeCacheResponse" $ do
it "decodes an empty object" $ do
let Just decoded = decode "{}" :: Maybe ClearPrivilegeCacheResponse
cpcBody decoded `shouldBe` KM.empty
it "round-trips an arbitrary object body" $ do
let bytes = "{\"cleared\":[\"myapp\"]}"
Just decoded = decode bytes :: Maybe ClearPrivilegeCacheResponse
Just rt = decode (encode decoded) :: Maybe ClearPrivilegeCacheResponse
rt `shouldBe` decoded