shomei-core-0.2.0.0: src/Shomei/Authorization/Role/Store.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}
-- | The role port: the declared role catalog (the "registry", @shomei_roles@), the durable
-- "user has role" facts (@shomei_role_grants@), and the role→permission definitions
-- (@shomei_role_permissions@).
--
-- Roles are flat: a grant is a @(user, role)@ pair with no project, organization, or resource
-- scope. A role implies a set of flat verb-noun /permissions/ (EP-9), resolved to the union
-- across a subject's roles at token mint. This is Shōmei's tier-1 authorization story —
-- self-contained, requiring no second system, and sufficient to gate Shōmei's own @\/admin@
-- surface. Fine-grained, relationship-derived authorization ("editor of /this/ project", live
-- revocation, caveats) is deliberately out of scope; see @docs\/user\/security.md@ for the
-- two-tier boundary.
module Shomei.Authorization.Role.Store
( RoleStore (..),
RoleDefinition (..),
defineRole,
listDefinedRoles,
grantRole,
revokeRole,
listRolesForUser,
allowPermission,
disallowPermission,
listPermissionsForRole,
permissionsForRoles,
)
where
import Data.Set (Set)
import Effectful (Dispatch (..), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (send)
import Shomei.Authorization.Claims.Domain (Permission, Role)
import Shomei.Id (UserId)
import Shomei.Prelude
-- | One row of the role registry (the @shomei_roles@ table): a role an operator has declared
-- grantable, with an optional human description.
data RoleDefinition = RoleDefinition
{ role :: !Role,
description :: !(Maybe Text),
createdAt :: !UTCTime
}
deriving stock (Generic, Eq, Show)
data RoleStore :: Effect where
-- | Declare a role in the registry. Returns 'True' if newly defined, 'False' if it already
-- existed (idempotent; the description of an existing role is NOT updated).
DefineRole :: Role -> Maybe Text -> UTCTime -> RoleStore m Bool
-- | The full registry, sorted by role name. Deployments have few roles; no paging.
ListDefinedRoles :: RoleStore m [RoleDefinition]
-- | Record a grant, with an optional expiry ('Nothing' = does not expire). Returns 'True' when
-- state changed: a new grant, or an existing grant whose expiry differs (re-granting updates
-- the expiry — upsert). Callers publish the audit event only on 'True'.
GrantRole :: UserId -> Role -> Maybe UserId -> Maybe UTCTime -> UTCTime -> RoleStore m Bool
-- | Remove a grant. Returns 'True' if a grant was removed.
RevokeRole :: UserId -> Role -> RoleStore m Bool
-- | The subject's roles as of the given instant: grants whose @expires_at@ is at or before it
-- are excluded. Callers pass the mint timestamp.
ListRolesForUser :: UserId -> UTCTime -> RoleStore m (Set Role)
-- | Attach a permission to a role. 'True' = newly attached, 'False' = already present.
AllowPermission :: Role -> Permission -> UTCTime -> RoleStore m Bool
-- | Detach a permission from a role. 'True' = something was detached.
DisallowPermission :: Role -> Permission -> RoleStore m Bool
-- | The permissions attached to a single role, sorted.
ListPermissionsForRole :: Role -> RoleStore m (Set Permission)
-- | The union of permissions across a role set — one query, used by the mint path.
PermissionsForRoles :: Set Role -> RoleStore m (Set Permission)
type instance DispatchOf RoleStore = Dynamic
defineRole :: (RoleStore :> es) => Role -> Maybe Text -> UTCTime -> Eff es Bool
defineRole r desc ts = send (DefineRole r desc ts)
listDefinedRoles :: (RoleStore :> es) => Eff es [RoleDefinition]
listDefinedRoles = send ListDefinedRoles
grantRole :: (RoleStore :> es) => UserId -> Role -> Maybe UserId -> Maybe UTCTime -> UTCTime -> Eff es Bool
grantRole uid r by expiry ts = send (GrantRole uid r by expiry ts)
revokeRole :: (RoleStore :> es) => UserId -> Role -> Eff es Bool
revokeRole uid r = send (RevokeRole uid r)
listRolesForUser :: (RoleStore :> es) => UserId -> UTCTime -> Eff es (Set Role)
listRolesForUser uid asOf = send (ListRolesForUser uid asOf)
allowPermission :: (RoleStore :> es) => Role -> Permission -> UTCTime -> Eff es Bool
allowPermission r p ts = send (AllowPermission r p ts)
disallowPermission :: (RoleStore :> es) => Role -> Permission -> Eff es Bool
disallowPermission r p = send (DisallowPermission r p)
listPermissionsForRole :: (RoleStore :> es) => Role -> Eff es (Set Permission)
listPermissionsForRole = send . ListPermissionsForRole
permissionsForRoles :: (RoleStore :> es) => Set Role -> Eff es (Set Permission)
permissionsForRoles = send . PermissionsForRoles