packages feed

hscim-0.4.0: src/Web/Scim/Class/Group.hs

{-# LANGUAGE AllowAmbiguousTypes #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2022 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Web.Scim.Class.Group
  ( GroupSite (..),
    GroupDB (..),
    GroupTypes (..),
    StoredGroup,
    Group (..),
    Member (..),
    groupServer,
  )
where

import Data.Aeson
import qualified Data.Aeson as Aeson
import Data.Text
import Servant
import Servant.API.Generic
import Servant.Server.Generic
import Web.Scim.Class.Auth
import Web.Scim.ContentType
import Web.Scim.Handler
import Web.Scim.Schema.Common
import Web.Scim.Schema.ListResponse
import Web.Scim.Schema.Meta

----------------------------------------------------------------------------
-- /Groups API

type Schema = Text

-- | Configurable parts of 'Group'.
class GroupTypes tag where
  -- | Group ID type.
  type GroupId tag

-- TODO
data Member = Member
  { value :: Text,
    typ :: Text,
    ref :: Text
  }
  deriving (Show, Eq, Generic)

instance FromJSON Member where
  parseJSON = either (fail . show) (genericParseJSON parseOptions) . jsonLower

instance ToJSON Member where
  toJSON = genericToJSON serializeOptions

data Group = Group
  { schemas :: [Schema],
    displayName :: Text,
    members :: [Member]
  }
  deriving (Show, Eq, Generic)

instance FromJSON Group where
  parseJSON = either (fail . show) (genericParseJSON parseOptions) . jsonLower

instance ToJSON Group where
  toJSON = genericToJSON serializeOptions

type StoredGroup tag = WithMeta (WithId (GroupId tag) Group)

data GroupSite tag route = GroupSite
  { gsGetGroups ::
      route
        :- Get '[SCIM] (ListResponse (StoredGroup tag)),
    gsGetGroup ::
      route
        :- Capture "id" (GroupId tag)
          :> Get '[SCIM] (StoredGroup tag),
    gsPostGroup ::
      route
        :- ReqBody '[SCIM] Group
          :> PostCreated '[SCIM] (StoredGroup tag),
    gsPutGroup ::
      route
        :- Capture "id" (GroupId tag)
          :> ReqBody '[SCIM] Group
          :> Put '[SCIM] (StoredGroup tag),
    gsPatchGroup ::
      route
        :- Capture "id" (GroupId tag)
          :> ReqBody '[SCIM] Aeson.Value
          :> Patch '[SCIM] (StoredGroup tag),
    gsDeleteGroup ::
      route
        :- Capture "id" (GroupId tag)
          :> DeleteNoContent
  }
  deriving (Generic)

----------------------------------------------------------------------------
-- Methods used by the API

class (Monad m, GroupTypes tag, AuthDB tag m) => GroupDB tag m where
  -- | Get all groups.
  getGroups ::
    AuthInfo tag ->
    ScimHandler m (ListResponse (StoredGroup tag))

  -- | Get a single group by ID.
  --
  -- Should throw 'notFound' if the group does not.
  getGroup ::
    AuthInfo tag ->
    GroupId tag ->
    ScimHandler m (StoredGroup tag)

  -- | Create a new group.
  --
  -- Should throw 'conflict' if uniqueness constraints are violated.
  postGroup ::
    AuthInfo tag ->
    Group ->
    ScimHandler m (StoredGroup tag)

  -- | Overwrite an existing group.
  --
  -- Should throw 'notFound' if the group does not exist, and 'conflict' if uniqueness
  -- constraints are violated.
  putGroup ::
    AuthInfo tag ->
    GroupId tag ->
    Group ->
    ScimHandler m (StoredGroup tag)

  -- | Modify an existing group.
  --
  -- Should throw 'notFound' if the group doesn't exist, and 'conflict' if uniqueness
  -- constraints are violated.
  --
  -- FUTUREWORK: add types for PATCH (instead of 'Aeson.Value').
  -- See <https://tools.ietf.org/html/rfc7644#section-3.5.2>
  patchGroup ::
    AuthInfo tag ->
    GroupId tag ->
    -- | PATCH payload
    Aeson.Value ->
    ScimHandler m (StoredGroup tag)

  -- | Delete a group.
  --
  -- Should throw 'notFound' if the group does not exist.
  deleteGroup ::
    AuthInfo tag ->
    GroupId tag ->
    ScimHandler m ()

----------------------------------------------------------------------------
-- API handlers

groupServer ::
  forall tag m.
  GroupDB tag m =>
  Maybe (AuthData tag) ->
  GroupSite tag (AsServerT (ScimHandler m))
groupServer authData =
  GroupSite
    { gsGetGroups = do
        auth <- authCheck @tag authData
        getGroups @tag auth,
      gsGetGroup = \gid -> do
        auth <- authCheck @tag authData
        getGroup @tag auth gid,
      gsPostGroup = \gr -> do
        auth <- authCheck @tag authData
        postGroup @tag auth gr,
      gsPutGroup = \gid gr -> do
        auth <- authCheck @tag authData
        putGroup @tag auth gid gr,
      gsPatchGroup = \gid patch -> do
        auth <- authCheck @tag authData
        patchGroup @tag auth gid patch,
      gsDeleteGroup = \gid -> do
        auth <- authCheck @tag authData
        deleteGroup @tag auth gid
        pure NoContent
    }