mirror of
https://code.sup39.dev/repos/Wqawg
synced 2024-12-27 16:54:53 +09:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
7b61749198
5 changed files with 307 additions and 2 deletions
|
@ -74,6 +74,7 @@ import Vervis.Persist.Collab
|
||||||
import Vervis.Persist.Discussion
|
import Vervis.Persist.Discussion
|
||||||
import Vervis.RemoteActorStore
|
import Vervis.RemoteActorStore
|
||||||
import Vervis.Ticket
|
import Vervis.Ticket
|
||||||
|
import Vervis.Web.Collab
|
||||||
|
|
||||||
-- Meaning: An actor is adding some object to some target
|
-- Meaning: An actor is adding some object to some target
|
||||||
-- Behavior:
|
-- Behavior:
|
||||||
|
@ -518,6 +519,19 @@ deckResolve now deckID (Verse authorIdMsig body) (AP.Resolve uObject) = do
|
||||||
_ -> throwE "Local route but not a ticket of mine"
|
_ -> throwE "Local route but not a ticket of mine"
|
||||||
taskID <- decodeKeyHashidE taskHash "Invalid TicketDeck keyhashid"
|
taskID <- decodeKeyHashidE taskHash "Invalid TicketDeck keyhashid"
|
||||||
|
|
||||||
|
-- Verify that a capability is provided
|
||||||
|
uCap <- do
|
||||||
|
let muCap = AP.activityCapability $ actbActivity body
|
||||||
|
fromMaybeE muCap "No capability provided"
|
||||||
|
|
||||||
|
-- Verify the sender is authorized by the tracker to resolve a ticket
|
||||||
|
verifyCapability''
|
||||||
|
uCap
|
||||||
|
authorIdMsig
|
||||||
|
(GrantResourceDeck deckID)
|
||||||
|
AP.RoleTriage
|
||||||
|
|
||||||
|
{-
|
||||||
-- Check capability
|
-- Check capability
|
||||||
capability <- do
|
capability <- do
|
||||||
|
|
||||||
|
@ -536,6 +550,7 @@ deckResolve now deckID (Verse authorIdMsig body) (AP.Resolve uObject) = do
|
||||||
Left (actorByKey, _, outboxItemID) ->
|
Left (actorByKey, _, outboxItemID) ->
|
||||||
return (actorByKey, outboxItemID)
|
return (actorByKey, outboxItemID)
|
||||||
_ -> throwE "Capability is remote i.e. definitely not by me"
|
_ -> throwE "Capability is remote i.e. definitely not by me"
|
||||||
|
-}
|
||||||
|
|
||||||
maybeNew <- withDBExcept $ do
|
maybeNew <- withDBExcept $ do
|
||||||
|
|
||||||
|
@ -557,12 +572,14 @@ deckResolve now deckID (Verse authorIdMsig body) (AP.Resolve uObject) = do
|
||||||
mractid <- lift $ insertToInbox now authorIdMsig body (actorInbox actorRecip) False
|
mractid <- lift $ insertToInbox now authorIdMsig body (actorInbox actorRecip) False
|
||||||
for mractid $ \ resolveDB -> do
|
for mractid $ \ resolveDB -> do
|
||||||
|
|
||||||
|
{-
|
||||||
-- Verify the sender is authorized by the tracker to resolve a ticket
|
-- Verify the sender is authorized by the tracker to resolve a ticket
|
||||||
verifyCapability'
|
verifyCapability'
|
||||||
capability
|
capability
|
||||||
authorIdMsig
|
authorIdMsig
|
||||||
(GrantResourceDeck deckID)
|
(GrantResourceDeck deckID)
|
||||||
AP.RoleTriage
|
AP.RoleTriage
|
||||||
|
-}
|
||||||
|
|
||||||
-- Prepare forwarding the Resolve to my followers & ticket
|
-- Prepare forwarding the Resolve to my followers & ticket
|
||||||
-- followers
|
-- followers
|
||||||
|
|
|
@ -1289,8 +1289,8 @@ getPublishResolveR = do
|
||||||
|
|
||||||
postPublishResolveR :: Handler ()
|
postPublishResolveR :: Handler ()
|
||||||
postPublishResolveR = do
|
postPublishResolveR = do
|
||||||
federation <- getsYesod $ appFederation . appSettings
|
--federation <- getsYesod $ appFederation . appSettings
|
||||||
unless federation badMethod
|
--unless federation badMethod
|
||||||
|
|
||||||
(uTicket, (uCap, cap)) <- runFormPostRedirect PublishResolveR resolveForm
|
(uTicket, (uCap, cap)) <- runFormPostRedirect PublishResolveR resolveForm
|
||||||
|
|
||||||
|
|
284
src/Vervis/Web/Collab.hs
Normal file
284
src/Vervis/Web/Collab.hs
Normal file
|
@ -0,0 +1,284 @@
|
||||||
|
{- This file is part of Vervis.
|
||||||
|
-
|
||||||
|
- Written in 2023 by fr33domlover <fr33domlover@riseup.net>.
|
||||||
|
-
|
||||||
|
- ♡ Copying is an act of love. Please copy, reuse and share.
|
||||||
|
-
|
||||||
|
- The author(s) have dedicated all copyright and related and neighboring
|
||||||
|
- rights to this software to the public domain worldwide. This software is
|
||||||
|
- distributed without any warranty.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the CC0 Public Domain Dedication along
|
||||||
|
- with this software. If not, see
|
||||||
|
- <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Vervis.Web.Collab
|
||||||
|
( verifyCapability''
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
|
import Control.Exception.Base
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.Trans.Except
|
||||||
|
import Control.Monad.Trans.Maybe
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Bifunctor
|
||||||
|
import Data.Bitraversable
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Default.Class
|
||||||
|
import Data.Foldable
|
||||||
|
import Data.Maybe (fromMaybe, isJust)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Time.Clock
|
||||||
|
import Data.Traversable
|
||||||
|
import Database.Persist
|
||||||
|
import Network.HTTP.Client hiding (Proxy, proxy)
|
||||||
|
import Network.HTTP.Types.Method
|
||||||
|
import Network.HTTP.Types.Status
|
||||||
|
import Optics.Core
|
||||||
|
import Text.Blaze.Html (Html)
|
||||||
|
import Yesod.Auth (requireAuth)
|
||||||
|
import Yesod.Core
|
||||||
|
import Yesod.Core.Handler (redirect, setMessage, lookupPostParam, notFound)
|
||||||
|
import Yesod.Form.Functions (runFormPost, runFormGet)
|
||||||
|
import Yesod.Form.Types (FormResult (..))
|
||||||
|
import Yesod.Persist.Core (runDB, get404, getBy404)
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Database.Esqueleto as E
|
||||||
|
|
||||||
|
import Control.Concurrent.Actor
|
||||||
|
import Database.Persist.JSON
|
||||||
|
import Development.PatchMediaType
|
||||||
|
import Network.FedURI
|
||||||
|
import Web.ActivityPub hiding (Project (..), Repo (..), Actor (..), ActorDetail (..), ActorLocal (..))
|
||||||
|
import Web.Actor
|
||||||
|
import Web.Actor.Persist
|
||||||
|
import Yesod.ActivityPub
|
||||||
|
import Yesod.Hashids
|
||||||
|
import Yesod.MonadSite
|
||||||
|
|
||||||
|
import qualified Web.ActivityPub as AP
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Except.Local
|
||||||
|
import Data.Either.Local
|
||||||
|
import Data.Paginate.Local
|
||||||
|
import Database.Persist.Local
|
||||||
|
import Yesod.Form.Local
|
||||||
|
import Yesod.Persist.Local
|
||||||
|
|
||||||
|
import Vervis.Actor
|
||||||
|
import Vervis.Actor2
|
||||||
|
import Vervis.Data.Actor
|
||||||
|
import Vervis.Data.Collab
|
||||||
|
import Vervis.FedURI
|
||||||
|
import Vervis.Foundation
|
||||||
|
import Vervis.Model
|
||||||
|
import Vervis.Persist.Actor
|
||||||
|
import Vervis.Persist.Collab
|
||||||
|
import Vervis.Settings
|
||||||
|
import Vervis.Ticket
|
||||||
|
import Vervis.TicketFilter
|
||||||
|
import Vervis.Time
|
||||||
|
|
||||||
|
import qualified Vervis.Recipient as VR
|
||||||
|
|
||||||
|
verifyCapability''
|
||||||
|
:: FedURI
|
||||||
|
-> Either
|
||||||
|
(LocalActorBy Key, ActorId, OutboxItemId)
|
||||||
|
(RemoteAuthor, LocalURI, Maybe ByteString)
|
||||||
|
-> GrantResourceBy Key
|
||||||
|
-> AP.Role
|
||||||
|
-> ActE ()
|
||||||
|
verifyCapability'' uCap recipientActor resource requiredRole = do
|
||||||
|
manager <- asksEnv envHttpManager
|
||||||
|
encodeRouteHome <- getEncodeRouteHome
|
||||||
|
uResource <-
|
||||||
|
encodeRouteHome . VR.renderLocalActor <$>
|
||||||
|
hashLocalActor (grantResourceLocalActor resource)
|
||||||
|
now <- liftIO getCurrentTime
|
||||||
|
grants <- traverseGrants manager uResource now
|
||||||
|
unless (checkRole grants) $
|
||||||
|
throwE "checkRole returns False"
|
||||||
|
where
|
||||||
|
traverseGrants manager uResource now = do
|
||||||
|
encodeRouteHome <- getEncodeRouteHome
|
||||||
|
uActor <-
|
||||||
|
case recipientActor of
|
||||||
|
Left (a, _, _) -> encodeRouteHome . VR.renderLocalActor <$> hashLocalActor a
|
||||||
|
Right (a, _, _) -> return $ remoteAuthorURI a
|
||||||
|
go uCap uActor []
|
||||||
|
where
|
||||||
|
go u@(ObjURI h lu) recipActor l = do
|
||||||
|
cap <- parseActivityURI' u
|
||||||
|
AP.Doc host activity <-
|
||||||
|
case cap of
|
||||||
|
Left (actor, _, itemID) -> withDBExcept $ do
|
||||||
|
item <- getE itemID "No such OutboxItemId in DB"
|
||||||
|
let outboxID = outboxItemOutbox item
|
||||||
|
actorID <- do
|
||||||
|
ma <- lift $ getKeyBy $ UniqueActorOutbox outboxID
|
||||||
|
fromMaybeE ma "Item's outbox doesn't belong to any actor"
|
||||||
|
itemActor <- lift $ getLocalActor actorID
|
||||||
|
unless (itemActor == actor) $
|
||||||
|
throwE "No such local activity in DB, actor and item mismatch"
|
||||||
|
let obj = persistJSONDoc $ outboxItemActivity item
|
||||||
|
case fromJSON $ Object obj of
|
||||||
|
Error s -> throwE $ "Parsing local activity JSON object into an Activity failed: " <> T.pack s
|
||||||
|
Success doc -> return doc
|
||||||
|
Right _ -> do
|
||||||
|
ract <- lift $ withDB $ runMaybeT $ do
|
||||||
|
instanceID <- MaybeT $ getKeyBy $ UniqueInstance h
|
||||||
|
objectID <- MaybeT $ getKeyBy $ UniqueRemoteObject instanceID lu
|
||||||
|
MaybeT $ getValBy $ UniqueRemoteActivity objectID
|
||||||
|
case ract of
|
||||||
|
Just act -> do
|
||||||
|
let obj = persistJSONDoc $ remoteActivityContent act
|
||||||
|
case fromJSON $ Object obj of
|
||||||
|
Error s -> throwE $ "Parsing cached remote activity JSON object into an Activity failed: " <> T.pack s
|
||||||
|
Success doc -> return doc
|
||||||
|
Nothing -> withExceptT T.pack $ AP.fetchAP manager $ Left u
|
||||||
|
luId <- fromMaybeE (AP.activityId activity) "Activity without id"
|
||||||
|
unless (u == ObjURI host luId) $
|
||||||
|
throwE "Fetched URI and activity id mismatch"
|
||||||
|
grant <-
|
||||||
|
case AP.activitySpecific activity of
|
||||||
|
AP.GrantActivity g -> return g
|
||||||
|
_ -> throwE "Not a Grant activity"
|
||||||
|
|
||||||
|
unless (AP.grantContext grant == uResource) $
|
||||||
|
throwE "Grant.context isn't me, the resource"
|
||||||
|
unless (AP.grantTarget grant == recipActor) $
|
||||||
|
throwE "Grant.target isn't the actor of the previous grant"
|
||||||
|
when (any ((== u) . view _1) l) $
|
||||||
|
throwE "This Grant is already listed in l"
|
||||||
|
for_ (AP.grantStart grant) $ \ start ->
|
||||||
|
unless (start <= now) $
|
||||||
|
throwE "Grant starts in the future"
|
||||||
|
for_ (AP.grantEnd grant) $ \ end ->
|
||||||
|
unless (now < end) $
|
||||||
|
throwE "Grant has already expired"
|
||||||
|
|
||||||
|
role <-
|
||||||
|
case AP.grantObject grant of
|
||||||
|
AP.RXRole r -> pure r
|
||||||
|
RXDelegator -> throwE "Role is delegator"
|
||||||
|
(targetIsProject, targetIsTeam) <- do
|
||||||
|
routeOrRemote <- parseFedURI $ AP.grantTarget grant
|
||||||
|
case routeOrRemote of
|
||||||
|
Left route -> do
|
||||||
|
actor <- nameExceptT "Grant.target" $ parseLocalActorE' route
|
||||||
|
return $
|
||||||
|
case actor of
|
||||||
|
LocalActorGroup _ -> (False, True)
|
||||||
|
LocalActorProject _ -> (True, False)
|
||||||
|
_ -> (False, False)
|
||||||
|
Right (ObjURI hTarget luTarget) -> do
|
||||||
|
mact <- lift $ withDB $ runMaybeT $ do
|
||||||
|
instanceID <- MaybeT $ getKeyBy $ UniqueInstance h
|
||||||
|
objectID <- MaybeT $ getKeyBy $ UniqueRemoteObject instanceID lu
|
||||||
|
MaybeT $ getValBy $ UniqueRemoteActor objectID
|
||||||
|
typ <-
|
||||||
|
case mact of
|
||||||
|
Just act -> return $ remoteActorType act
|
||||||
|
Nothing -> do
|
||||||
|
actor <- ExceptT $ first T.pack <$> AP.fetchAPID manager (AP.actorId . AP.actorLocal) hTarget luTarget
|
||||||
|
return $ AP.actorType $ AP.actorDetail actor
|
||||||
|
return (typ == AP.ActorTypeProject, typ == AP.ActorTypeTeam)
|
||||||
|
|
||||||
|
case AP.grantDelegates grant of
|
||||||
|
|
||||||
|
Nothing -> nameExceptT "Leaf-Grant" $ withDBExcept $ do
|
||||||
|
(capActor, capItem) <-
|
||||||
|
case cap of
|
||||||
|
Left (actor, _, itemID) -> return (actor, itemID)
|
||||||
|
Right _ -> throwE "Remote, so definitely not by me"
|
||||||
|
-- We already checked that the activity exists in DB
|
||||||
|
-- So proceed to find the Collab or Stem record
|
||||||
|
if null l
|
||||||
|
|
||||||
|
-- This is thr only Grant in the chain, so we're
|
||||||
|
-- looking for a Collab record
|
||||||
|
then nameExceptT "Collab" $ do
|
||||||
|
-- Find the Collab record
|
||||||
|
collabID <- do
|
||||||
|
maybeEnable <- lift $ getValBy $ UniqueCollabEnableGrant capItem
|
||||||
|
collabEnableCollab <$>
|
||||||
|
fromMaybeE maybeEnable "No CollabEnable for this activity"
|
||||||
|
-- Find the recipient of that Collab
|
||||||
|
recipID <-
|
||||||
|
lift $ bimap collabRecipLocalPerson collabRecipRemoteActor <$>
|
||||||
|
requireEitherAlt
|
||||||
|
(getValBy $ UniqueCollabRecipLocal collabID)
|
||||||
|
(getValBy $ UniqueCollabRecipRemote collabID)
|
||||||
|
"No collab recip"
|
||||||
|
"Both local and remote recips for collab"
|
||||||
|
-- Find the local topic, on which this Collab gives access
|
||||||
|
topic <- lift $ getCollabTopic collabID
|
||||||
|
-- Verify that topic is indeed the sender of the Grant
|
||||||
|
unless (grantResourceLocalActor topic == capActor) $
|
||||||
|
error "Grant sender isn't the topic"
|
||||||
|
-- Verify the topic matches the resource specified
|
||||||
|
unless (topic == resource) $
|
||||||
|
throwE "Capability topic is some other local resource"
|
||||||
|
|
||||||
|
-- There are more Grants in the chain, so we're
|
||||||
|
-- looking for a Stem record
|
||||||
|
else nameExceptT "Stem" $ do
|
||||||
|
-- Find the Stem record
|
||||||
|
stemID <- do
|
||||||
|
scaID <- do
|
||||||
|
maybeSCA <- lift $ getValBy $ UniqueStemDelegateLocalGrant capItem
|
||||||
|
stemDelegateLocalStem <$>
|
||||||
|
fromMaybeE maybeSCA "No StemDelegateLocal for this activity"
|
||||||
|
lift $ stemComponentAcceptStem <$> getJust scaID
|
||||||
|
-- Find the local topic, on which this Stem gives access
|
||||||
|
topic <- lift $ getStemIdent stemID
|
||||||
|
-- Verify that topic is indeed the sender of the Grant
|
||||||
|
unless (componentActor topic == capActor) $
|
||||||
|
error "Grant sender isn't the Stem ident"
|
||||||
|
-- Verify the topic matches the resource specified
|
||||||
|
unless (componentActor topic == grantResourceLocalActor resource) $
|
||||||
|
throwE "Capability topic is some other local resource"
|
||||||
|
|
||||||
|
return $ (u, activity, grant, role, targetIsProject, targetIsTeam) : l
|
||||||
|
|
||||||
|
Just uParent -> nameExceptT "Extension-Grant" $ do
|
||||||
|
case cap of
|
||||||
|
Left (actor, _, _)
|
||||||
|
| grantResourceLocalActor resource == actor ->
|
||||||
|
throwE "Grant.delegates specified but Grant's actor is me"
|
||||||
|
_ -> return ()
|
||||||
|
(luResult, _) <- fromMaybeE (AP.grantResult grant) "Grant.result not specified"
|
||||||
|
req <- either (throwE . T.pack . displayException) pure $ requestFromURI $ uriFromObjURI $ ObjURI host luResult
|
||||||
|
let req' =
|
||||||
|
req { method = "HEAD"
|
||||||
|
}
|
||||||
|
response <- liftIO $ httpNoBody req' manager
|
||||||
|
let status = responseStatus response
|
||||||
|
unless (status == ok200 || status == noContent204) $
|
||||||
|
throwE "Result URI gave neither 200 nor 204 status"
|
||||||
|
let uNextRecip = ObjURI host $ AP.activityActor activity
|
||||||
|
go uParent uNextRecip $ (u, activity, grant, role, targetIsProject, targetIsTeam) : l
|
||||||
|
checkRole [] = error "Ended up with empty list of grants, impossible"
|
||||||
|
checkRole (g:gs) = go g gs (view _4 g)
|
||||||
|
where
|
||||||
|
go (u, activity, grant, _, targetIsProject, targetIsTeam) rest role =
|
||||||
|
case rest of
|
||||||
|
[] ->
|
||||||
|
checkLeaf && role >= requiredRole
|
||||||
|
h@(_, _, next, role', _, _) : rest' ->
|
||||||
|
role' <= role && checkItem next && go h rest' role'
|
||||||
|
where
|
||||||
|
checkLeaf = AP.grantAllows grant == AP.Invoke
|
||||||
|
checkItem h =
|
||||||
|
AP.grantAllows grant == AP.GatherAndConvey &&
|
||||||
|
targetIsProject
|
||||||
|
||
|
||||||
|
AP.grantAllows grant == AP.Distribute &&
|
||||||
|
targetIsTeam &&
|
||||||
|
(AP.grantAllows h == AP.Distribute || AP.grantAllows h == AP.Invoke)
|
|
@ -384,6 +384,7 @@ data ActorType
|
||||||
| ActorTypeTicketTracker
|
| ActorTypeTicketTracker
|
||||||
| ActorTypePatchTracker
|
| ActorTypePatchTracker
|
||||||
| ActorTypeProject
|
| ActorTypeProject
|
||||||
|
| ActorTypeTeam
|
||||||
| ActorTypeOther Text
|
| ActorTypeOther Text
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
|
@ -394,6 +395,7 @@ parseActorType t
|
||||||
| t == "TicketTracker" = ActorTypeTicketTracker
|
| t == "TicketTracker" = ActorTypeTicketTracker
|
||||||
| t == "PatchTracker" = ActorTypePatchTracker
|
| t == "PatchTracker" = ActorTypePatchTracker
|
||||||
| t == "Project" = ActorTypeProject
|
| t == "Project" = ActorTypeProject
|
||||||
|
| t == "Team" = ActorTypeTeam
|
||||||
| otherwise = ActorTypeOther t
|
| otherwise = ActorTypeOther t
|
||||||
|
|
||||||
renderActorType :: ActorType -> Text
|
renderActorType :: ActorType -> Text
|
||||||
|
@ -403,6 +405,7 @@ renderActorType = \case
|
||||||
ActorTypeTicketTracker -> "TicketTracker"
|
ActorTypeTicketTracker -> "TicketTracker"
|
||||||
ActorTypePatchTracker -> "PatchTracker"
|
ActorTypePatchTracker -> "PatchTracker"
|
||||||
ActorTypeProject -> "Project"
|
ActorTypeProject -> "Project"
|
||||||
|
ActorTypeTeam -> "Team"
|
||||||
ActorTypeOther t -> t
|
ActorTypeOther t -> t
|
||||||
|
|
||||||
instance FromJSON ActorType where
|
instance FromJSON ActorType where
|
||||||
|
|
|
@ -256,6 +256,7 @@ library
|
||||||
Vervis.Time
|
Vervis.Time
|
||||||
|
|
||||||
Vervis.Web.Actor
|
Vervis.Web.Actor
|
||||||
|
Vervis.Web.Collab
|
||||||
Vervis.Web.Darcs
|
Vervis.Web.Darcs
|
||||||
Vervis.Web.Delivery
|
Vervis.Web.Delivery
|
||||||
Vervis.Web.Discussion
|
Vervis.Web.Discussion
|
||||||
|
|
Loading…
Reference in a new issue