Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update default ACL behaviour #25

Merged
merged 4 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions src/main/scala/run/cosy/Solid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ object Solid:
// todo: replace names with .ac

given system: ActorSystem[Nothing] = ctx.system
val defaultAcl = uri.copy(path = uri.path ?/ ".acl")
val rootRef: ActorRef[AcceptMsg] = ctx.spawn(
BasicContainer(uri.withoutSlash, fpath, Root(defaultAcl)),
BasicContainer(uri.withoutSlash, fpath, Root(uri.withSlash)),
"solid"
)
SolidPostOffice(system).addRoot(uri, rootRef)
SolidPostOffice(system).addRoot(uri.withSlash, rootRef)
// todo: why this and given reg?
val solid = new Solid(uri, fpath)
given timeout: Scheduler = system.scheduler
Expand Down Expand Up @@ -244,13 +243,6 @@ class Solid(
// todo: the path here supposes that the root container is at the root of the web server
// we may want more flexibility here...

// todo: remove this commented code. Just there until commit
// def routeWith(replyTo: ActorRef[HttpResponse]): LDP.Route = LDP.RouteMsg(
// NonEmptyList("/", remaining),
// LDP.CmdMessage(SolidCmd.plain(reqc.request), agent, replyTo),
// acDir
// ).nextRoute(Root(baseUri.copy(path = Uri.Path("/.acl"))))

// todo: do we need the nextRoute(...) added above?
SolidPostOffice(sys).ask[HttpResponse](SolidCmd.plain(reqc.request), agent)
.map(RouteResult.Complete)
20 changes: 12 additions & 8 deletions src/main/scala/run/cosy/http/auth/Guard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import run.cosy.ldp.Messages.{CmdMessage, Route}
import run.cosy.ldp.fs.BasicContainer
import run.cosy.ldp.rdf.LocatedGraphs.{LGs, LocatedGraph, Pointed, PtsLGraph}
import run.cosy.ldp.ACInfo.*
import run.cosy.ldp.fs.BasicContainer.aclLinks

import java.util.concurrent.TimeUnit
import scala.concurrent.duration.Duration
Expand Down Expand Up @@ -173,16 +174,15 @@ object Guard:
* @param msg
* the message to authorize
* @param msgACL
* the closest default ACL from the message if known
* @param aclUri
* the URL of the acl that allows access
* the closest default ACL from the message if known, or the effective acl
* @param aclUri the direct act for the request resource
*/
def Authorize[T](msg: CmdMessage[T], msgACL: ACInfo, aclUri: Uri)(using
context: ActorContext[ScriptMsg[?] | Do]
): Unit =
// todo!!: set timeout in config!!
given timeOut: akka.util.Timeout = akka.util.Timeout(Duration(3, TimeUnit.SECONDS))
context.log.info(s"Authorize(WannaDo($msg), <$aclUri>)")
context.log.info(s"Authorize(WannaDo($msg), <$msgACL>)")
import SolidCmd.{Get, Wait}
import run.cosy.http.auth.Guard.*
import cats.free.Free.pure
Expand All @@ -192,6 +192,8 @@ object Guard:
msg.commands match
case p: Plain[?] =>
import msg.given
val effectiveAcl = msgACL.aclUri

// todo: this is an optimisation to send the message directly to the ACL actor,
// but it requires getting the acl ActorRef be of a type which accepts ScriptMsg[Boolean]
// containers do that, but we'd need to check if
Expand All @@ -208,25 +210,27 @@ object Guard:
context.self, // todo: see above, should be able to send directly to destination
ref =>
ScriptMsg[Boolean](
authorizeScript(msgACL.acl, msg.from, msg.target, p.req.method),
authorizeScript(effectiveAcl, msg.from, msg.target, p.req.method),
WebServerAgent,
ref
)
) {
case Success(true) => Do(msg)
case Success(true) =>
context.log.info(s"authorized ${msg.target} by user ${msg.from} ")
Do(msg)
case Success(false) =>
context.log.info(s"failed to authorize ${msg.target} ")
msg.respondWithScr(HttpResponse(
StatusCodes.Unauthorized,
`WWW-Authenticate`(HttpChallenge("HttpSig", s"${msg.target}")) ::
Link(BasicContainer.aclLinks(aclUri, msgACL)) :: Nil
Link(aclLinks(msg.target, aclUri, msgACL)) :: Nil
))
case Failure(e) =>
context.log.info(s"Unable to authorize ${msg.target}: $e ")
msg.respondWithScr(HttpResponse(
StatusCodes.Unauthorized,
`WWW-Authenticate`(HttpChallenge("HttpSig", s"${msg.target}"))
:: Link(BasicContainer.aclLinks(aclUri, msgACL)) :: Nil,
:: Link(aclLinks(msg.target, aclUri, msgACL)) :: Nil,
HttpEntity(ContentTypes.`text/plain(UTF-8)`, e.getMessage)
))
}
Expand Down
13 changes: 9 additions & 4 deletions src/main/scala/run/cosy/http/util/UriX.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ object UriX:
case Path.Segment(head, tail) => Some(head)
case _ => None

/** find the container for this uri */
def container: Uri = uri.withPath(uri.path.container)

/** remove uri without the final slash, or the same */
def withoutSlash: Uri =
val rev: Uri.Path = uri.path.reverse
Expand All @@ -57,10 +60,12 @@ object UriX:

/** add slash to the end the final slash, or the same */
def withSlash: Uri =
uri.withPath(uri.path ++ Uri.Path./)
// rev match
// case Uri.Path.Segment(_) => uri.withPath(Uri.Path.Slash(rev).reverse)
// case _ => uri
// uri.withPath(uri.path ++ Uri.Path./)
val rev = uri.path.reverse
rev match
case Uri.Path.Empty => uri.withPath(Uri.Path./)
case Uri.Path.Segment(name,tail) => uri.withPath(Slash(Uri.Path.Segment(name,tail)).reverse)
case _ => uri

/** replace fileName with Name in Uri or else place filename after slash or add an initial
* slash Todo: improve - this definintion feels very ad-hoc ...
Expand Down
45 changes: 31 additions & 14 deletions src/main/scala/run/cosy/ldp/ACInfo.scala
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/*
* Copyright 2021 Henry Story
*
* SPDX-License-Identifier: Apache-2.0
*/
/** Copyright 2021 Henry Story
*
* SPDX-License-Identifier: Apache-2.0
*/

package run.cosy.ldp

import akka.actor.ActorPath
import akka.actor.typed.ActorRef
import akka.http.scaladsl.model.Uri
import akka.http.scaladsl.model.Uri.Path
import run.cosy.http.util.UriX.*
import run.cosy.ldp.ACInfo.{ACContainer, ACtrlRef}
import run.cosy.ldp.Messages.{Route, ScriptMsg, WannaDo}
import run.cosy.ldp.fs.Resource.AcceptMsg

Expand All @@ -19,18 +20,37 @@ import scala.annotation.tailrec

/** see [../README](../README.md). Avery ACInfo gives us the acl Uri Path. But we can either know
* the actor of the container or of the AC Resource itself, hence two subtypes
*
* @param container
* the URI of the container in which the ACL is stored. (the container may be a resource, since
* resources can have a number of representations (and so contain those).
*/
enum ACInfo(val acl: Uri):
enum ACInfo(val container: Uri):
// todo: this should not be needed, use ACContainer instead
// mhh, could also be useful for links from acl files to themselves.
// todo: in which case find a better name
case Root(acu: Uri) extends ACInfo(acu)
case Root(containerUrl: Uri) extends ACInfo(containerUrl)
// todo: container type should be ActorRef[Route|ScriptMsg[Boolean]] so that we can send acl scripts directly
/** The ACContainer actor is not the ACL actor, but its parent */
case ACContainer(acu: Uri, container: ActorRef[Route]) extends ACInfo(acu)
case ACContainer(containerUrl: Uri, ref: ActorRef[Route]) extends ACInfo(containerUrl)

/** The ACRef actor is the one containing the ACR graph */
case ACtrlRef(acu: Uri, actor: ActorRef[AcceptMsg]) extends ACInfo(acu)
case ACtrlRef(
forContainer: Uri,
acName: String,
actor: ActorRef[AcceptMsg],
isContainerAcl: Boolean
) extends ACInfo(forContainer)

// todo: different directories may want their own naming conventions for Urls, so we should pass
// the acl name around too. But for now, let's calculate it
def aclUri: Uri = this match
case ACtrlRef(container, acName, _, isContainer) =>
if isContainer then container ?/ acName
else if acName.endsWith(".acl")
then container.sibling(acName)
else container.sibling(acName + ".acl")
case other => other.container ?/ ".acl"

object ACInfo:
extension (path: Uri.Path)
Expand All @@ -43,10 +63,7 @@ object ACInfo:
rec(path, root)

extension (path: ActorPath)
def toUri: Uri.Path =
path.elements.foldLeft(Uri.Path./)((p, name) => p / name)

def toUri(starting: Int) =
path.elements.drop(starting).foldLeft(Uri.Path./)((p, name) => p ?/ name)
def toUriPath(starting: Int = 0): Path =
path.elements.drop(starting).foldLeft(Uri.Path./)((p, name) => p ?/ name)

end ACInfo
12 changes: 5 additions & 7 deletions src/main/scala/run/cosy/ldp/ResourceRegistry.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import akka.actor.typed.*
import akka.actor.typed.scaladsl.Behaviors
import akka.http.scaladsl.model.{HttpRequest, HttpResponse, Uri}
import run.cosy.Solid.pathToList
import run.cosy.http.util.UriX.*
import run.cosy.ldp.fs.BasicContainer

import java.util.concurrent.atomic.AtomicReference
import scala.annotation.tailrec
import run.cosy.ldp.DirTree
import run.cosy.ldp.ACInfo
import run.cosy.ldp.ACInfo.ACContainer

import run.cosy.ldp.Messages.*

/** Whenever an LDPR actor goes up it should register itself here, so that messages can be routed
* directly to the right actor, rather than passing the messages through the container hierarchy.
* See [[ResourceRegistry.md ResourceRegistry]]
Expand All @@ -42,7 +41,6 @@ class ResourceRegistry(rootUri: Uri, rootLDPC: ActorRef[Messages.Route])
extends PathDB[ActorRef[Messages.Route], Boolean]:

override def hasAcl(a: Boolean): Boolean = a
val rootACUri = rootUri.copy(path = rootUri.path ?/ ".acl")

// todo: remove the Option, requires knowing the root ActorRef
/** We map the information in the DB to what is more useable. todo: Later if this works we can
Expand All @@ -54,9 +52,9 @@ class ResourceRegistry(rootUri: Uri, rootLDPC: ActorRef[Messages.Route])
getActorRef(path).map { (lst, containerRef, lastACContainerRefOpt) =>
val ac: ACInfo = lastACContainerRefOpt match
case None => // todo: it would be nice if we could get rid of this
Root(rootACUri)
Root(rootUri.withSlash)
case Some(lastAC) => // todo: replace 2 below with calcualted value
val acr = lastAC.path.toUri(2) ?/ ".acl"
ACContainer(rootUri.copy(path = acr), lastAC)
val acr = lastAC.path.toUriPath(2)
ACContainer(rootUri.copy(path = acr).withSlash, lastAC)
(lst, containerRef, ac)
}.getOrElse((path, rootLDPC, Root(rootACUri)))
}.getOrElse((path, rootLDPC, Root(rootUri.withSlash)))
6 changes: 4 additions & 2 deletions src/main/scala/run/cosy/ldp/SolidPostOffice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ class SolidPostOffice(system: ActorSystem[?]) extends Extension:
type Ref = ActorRef[Messages.Route]
type Attr = Boolean

/** to start very simple we start with only allowing domain roots. todo: later add data
* structures to have paths higher on a server. todo: also wrap in a more secure structure later
/** to start very simple we start with only allowing domain roots.
* todo: later add data structures to have paths higher on a server.
* todo: also wrap in a more secure structure later
* todo: the map as to be from (protocol, Authority) to resourceRegistry
*/
val roots: AtomicReference[Map[Uri.Authority, ResourceRegistry]] = new AtomicReference(Map())

Expand Down
Loading