Skip to content

Commit

Permalink
kafka: implement basic ACL lib
Browse files Browse the repository at this point in the history
  • Loading branch information
Commelina committed Jan 30, 2024
1 parent 61a0cc6 commit 1446c5a
Show file tree
Hide file tree
Showing 20 changed files with 2,164 additions and 0 deletions.
178 changes: 178 additions & 0 deletions hstream-kafka/HStream/Kafka/Common/Acl.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{-# LANGUAGE RecordWildCards #-}

module HStream.Kafka.Common.Acl where

import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T

import HStream.Kafka.Common.Resource

-- [0..14]
data AclOperation
= AclOp_UNKNOWN
| AclOp_ANY
| AclOp_ALL
| AclOp_READ
| AclOp_WRITE
| AclOp_CREATE
| AclOp_DELETE
| AclOp_ALTER
| AclOp_DESCRIBE
| AclOp_CLUSTER_ACTION
| AclOp_DESCRIBE_CONFIGS
| AclOp_ALTER_CONFIGS
| AclOp_IDEMPOTENT_WRITE
| AclOp_CREATE_TOKENS
| AclOp_DESCRIBE_TOKENS
deriving (Eq, Enum, Ord)
instance Show AclOperation where
show AclOp_UNKNOWN = "Unknown"
show AclOp_ANY = "Any"
show AclOp_ALL = "All"
show AclOp_READ = "Read"
show AclOp_WRITE = "Write"
show AclOp_CREATE = "Create"
show AclOp_DELETE = "Delete"
show AclOp_ALTER = "Alter"
show AclOp_DESCRIBE = "Describe"
show AclOp_CLUSTER_ACTION = "ClusterAction"
show AclOp_DESCRIBE_CONFIGS = "DescribeConfigs"
show AclOp_ALTER_CONFIGS = "AlterConfigs"
show AclOp_IDEMPOTENT_WRITE = "IdempotentWrite"
show AclOp_CREATE_TOKENS = "CreateTokens"
show AclOp_DESCRIBE_TOKENS = "DescribeTokens"
instance Read AclOperation where
readsPrec _ s = case s of
"Unknown" -> [(AclOp_UNKNOWN, "")]
"Any" -> [(AclOp_ANY, "")]
"All" -> [(AclOp_ALL, "")]
"Read" -> [(AclOp_READ, "")]
"Write" -> [(AclOp_WRITE, "")]
"Create" -> [(AclOp_CREATE, "")]
"Delete" -> [(AclOp_DELETE, "")]
"Alter" -> [(AclOp_ALTER, "")]
"Describe" -> [(AclOp_DESCRIBE, "")]
"ClusterAction" -> [(AclOp_CLUSTER_ACTION, "")]
"DescribeConfigs" -> [(AclOp_DESCRIBE_CONFIGS, "")]
"AlterConfigs" -> [(AclOp_ALTER_CONFIGS, "")]
"IdempotentWrite" -> [(AclOp_IDEMPOTENT_WRITE, "")]
"CreateTokens" -> [(AclOp_CREATE_TOKENS, "")]
"DescribeTokens" -> [(AclOp_DESCRIBE_TOKENS, "")]
_ -> []

-- [0..3]
data AclPermissionType
= AclPerm_UNKNOWN
| AclPerm_ANY -- used in filter
| AclPerm_DENY
| AclPerm_ALLOW
deriving (Eq, Enum, Ord)
instance Show AclPermissionType where
show AclPerm_UNKNOWN = "Unknown"
show AclPerm_ANY = "Any"
show AclPerm_DENY = "Deny"
show AclPerm_ALLOW = "Allow"
instance Read AclPermissionType where
readsPrec _ s = case s of
"Unknown" -> [(AclPerm_UNKNOWN, "")]
"Any" -> [(AclPerm_ANY, "")]
"Deny" -> [(AclPerm_DENY, "")]
"Allow" -> [(AclPerm_ALLOW, "")]
_ -> []

-- | Data of an access control entry (ACE), which is a 4-tuple of principal,
-- host, operation and permission type.
-- Used in both 'AccessControlEntry' and 'AccessControlEntryFilter',
-- with slightly different field requirements.
data AccessControlEntryData = AccessControlEntryData
{ aceDataPrincipal :: Text
, aceDataHost :: Text
, aceDataOperation :: AclOperation
, aceDataPermissionType :: AclPermissionType
} deriving (Eq, Ord)
instance Show AccessControlEntryData where
show AccessControlEntryData{..} =
"(principal=" <> s_principal <>
", host=" <> s_host <>
", operation=" <> show aceDataOperation <>
", permissionType=" <> show aceDataPermissionType <> ")"
where s_principal = if T.null aceDataPrincipal then "<any>" else T.unpack aceDataPrincipal
s_host = if T.null aceDataHost then "<any>" else T.unpack aceDataHost

-- | An access control entry (ACE).
-- Requirements: principal and host can not be null.
-- operation can not be 'AclOp_ANY'.
-- permission type can not be 'AclPerm_ANY'.
newtype AccessControlEntry = AccessControlEntry
{ aceData :: AccessControlEntryData
} deriving (Eq, Ord)
instance Show AccessControlEntry where
show AccessControlEntry{..} = show aceData

-- | A filter which matches access control entry(ACE)s.
-- Requirements: principal and host can both be null.
newtype AccessControlEntryFilter = AccessControlEntryFilter
{ aceFilterData :: AccessControlEntryData
}
instance Show AccessControlEntryFilter where
show AccessControlEntryFilter{..} = show aceFilterData

instance Matchable AccessControlEntry AccessControlEntryFilter where
-- See org.apache.kafka.common.acl.AccessControlEntryFilter#matches
match AccessControlEntry{..} AccessControlEntryFilter{..}
| not (T.null (aceDataPrincipal aceFilterData)) &&
aceDataPrincipal aceFilterData /= aceDataPrincipal aceData = False
| not (T.null (aceDataHost aceFilterData)) &&
aceDataHost aceFilterData /= aceDataHost aceData = False
| aceDataOperation aceFilterData /= AclOp_ANY &&
aceDataOperation aceFilterData /= aceDataOperation aceData = False
| otherwise = aceDataPermissionType aceFilterData == AclPerm_ANY ||
aceDataPermissionType aceFilterData == aceDataPermissionType aceData
matchAtMostOne = isNothing . indefiniteFieldInFilter
indefiniteFieldInFilter AccessControlEntryFilter{ aceFilterData = AccessControlEntryData{..} }
| T.null aceDataPrincipal = Just "Principal is NULL"
| T.null aceDataHost = Just "Host is NULL"
| aceDataOperation == AclOp_ANY = Just "Operation is ANY"
| aceDataOperation == AclOp_UNKNOWN = Just "Operation is UNKNOWN"
| aceDataPermissionType == AclPerm_ANY = Just "Permission type is ANY"
| aceDataPermissionType == AclPerm_UNKNOWN = Just "Permission type is UNKNOWN"
| otherwise = Nothing

-- | A binding between a resource pattern and an access control entry (ACE).
data AclBinding = AclBinding
{ aclBindingResourcePattern :: ResourcePattern
, aclBindingACE :: AccessControlEntry
} deriving (Eq, Ord)
instance Show AclBinding where
show AclBinding{..} =
"(pattern=" <> show aclBindingResourcePattern <>
", entry=" <> show aclBindingACE <> ")"

-- | A filter which can match 'AclBinding's.
data AclBindingFilter = AclBindingFilter
{ aclBindingFilterResourcePatternFilter :: ResourcePatternFilter
, aclBindingFilterACEFilter :: AccessControlEntryFilter
}
instance Show AclBindingFilter where
show AclBindingFilter{..} =
"(patternFilter=" <> show aclBindingFilterResourcePatternFilter <>
", entryFilter=" <> show aclBindingFilterACEFilter <> ")"

instance Matchable AclBinding AclBindingFilter where
-- See org.apache.kafka.common.acl.AclBindingFilter#matches
match AclBinding{..} AclBindingFilter{..} =
match aclBindingResourcePattern aclBindingFilterResourcePatternFilter &&
match aclBindingACE aclBindingFilterACEFilter
matchAtMostOne AclBindingFilter{..} =
matchAtMostOne aclBindingFilterResourcePatternFilter &&
matchAtMostOne aclBindingFilterACEFilter
indefiniteFieldInFilter AclBindingFilter{..} =
indefiniteFieldInFilter aclBindingFilterResourcePatternFilter <>
indefiniteFieldInFilter aclBindingFilterACEFilter

-- TODO: validate
-- 1. No UNKNOWN contained
-- 2. resource pattern does not contain '/'
validateAclBinding :: AclBinding -> Either String ()
validateAclBinding AclBinding{..} = Right () -- FIXME
86 changes: 86 additions & 0 deletions hstream-kafka/HStream/Kafka/Common/AclEntry.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module HStream.Kafka.Common.AclEntry where

import Data.Aeson ((.:), (.=))
import qualified Data.Aeson as Aeson
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T

import HStream.Kafka.Common.Acl
import HStream.Kafka.Common.Resource
import HStream.Kafka.Common.Security

data AclEntry = AclEntry
{ aclEntryPrincipal :: Principal
, aclEntryHost :: Text
, aclEntryOperation :: AclOperation
, aclEntryPermissionType :: AclPermissionType
} deriving (Eq, Ord)
instance Show AclEntry where
show AclEntry{..} =
s_principal <>
" has " <> show aclEntryPermissionType <>
" permission for operations: " <> show aclEntryOperation <>
" from hosts: " <> s_host
where s_principal = show aclEntryPrincipal
s_host = T.unpack aclEntryHost
instance Aeson.ToJSON AclEntry where
toJSON AclEntry{..} =
Aeson.object [ "host" .= aclEntryHost
, "permissionType" .= show aclEntryPermissionType
, "operation" .= show aclEntryOperation
, "principal" .= show aclEntryPrincipal
]
instance Aeson.FromJSON AclEntry where
parseJSON (Aeson.Object v) = AclEntry
<$> (principalFromText <$> v .: "principal")
<*> v .: "host"
<*> (read <$> v .: "operation")
<*> (read <$> v .: "permissionType")
parseJSON o = fail $ "Invalid AclEntry: " <> show o

aceToAclEntry :: AccessControlEntry -> AclEntry
aceToAclEntry AccessControlEntry{ aceData = AccessControlEntryData{..} } =
AclEntry{..}
where aclEntryPrincipal = principalFromText aceDataPrincipal
aclEntryHost = aceDataHost
aclEntryOperation = aceDataOperation
aclEntryPermissionType = aceDataPermissionType

aclEntryToAce :: AclEntry -> AccessControlEntry
aclEntryToAce AclEntry{..} =
AccessControlEntry{ aceData = AccessControlEntryData{..} }
where aceDataPrincipal = T.pack (show aclEntryPrincipal)
aceDataHost = aclEntryHost
aceDataOperation = aclEntryOperation
aceDataPermissionType = aclEntryPermissionType

type Acls = Set.Set AclEntry
type Version = Int

defaultVersion :: Version
defaultVersion = 1

data AclResourceNode = AclResourceNode
{ aclResNodeVersion :: Version
, aclResNodeAcls :: Acls
} deriving (Show)
instance Aeson.ToJSON AclResourceNode where
toJSON AclResourceNode{..} =
Aeson.object [ "version" .= defaultVersion -- FIXME: version
, "acls" .= aclResNodeAcls
]
instance Aeson.FromJSON AclResourceNode where
parseJSON (Aeson.Object v) = AclResourceNode
<$> v .: "version"
<*> v .: "acls"
parseJSON o = fail $ "Invalid AclResourceNode: " <> show o

data AclCache = AclCache
{ aclCacheAcls :: Map.Map ResourcePattern Acls
, aclCacheResources :: Map.Map (AccessControlEntry,ResourceType,PatternType)
(Set.Set Text)
}

------------------------------------
Loading

0 comments on commit 1446c5a

Please sign in to comment.