Skip to content

Commit

Permalink
Merge pull request #3 from bblfish/CommandLine
Browse files Browse the repository at this point in the history
The code in this PR was used in a demonstration at last Wednesday's Solid CG meeting, and a video of if the demo is up https://forum.solidproject.org/t/http-sig-auth-demo-for-big-linked-data/6646
  • Loading branch information
bblfish authored Jun 12, 2023
2 parents 706daf1 + cb2643e commit abbac16
Show file tree
Hide file tree
Showing 78 changed files with 5,765 additions and 1,495 deletions.
47 changes: 43 additions & 4 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
version = 3.5.9
version = "3.7.4"
runner.dialect = scala3

indent {
main = 2
matchSite = 1
significant = 3
}
align {
preset = none
stripMargin = false
}
maxColumn = 100
assumeStandardLibraryStripMargin = true
rewrite.scala3 {
convertToNewSyntax = true
removeOptionalBraces = yes
}
runner.dialectOverride.allowQuestionMarkAsTypeWildcard = false
newlines {
selectChains = fold
beforeMultiline = fold
}
comments.wrapSingleLineMlcAsSlc = false
docstrings{
wrap = "no"
oneline = fold
style = SpaceAsterisk
}


fileOverride {
"glob:**.sbt" {
runner.dialect = scala212source3
}

"glob:**/project/**.scala" {
runner.dialect = scala212source3
}
"glob:**/interface/**.scala" {
runner.dialect = scala212source3
}

rewrite.rules = [Imports]
rewrite.imports.sort = scalastyle
"glob:**/sbt-plugin/**.scala" {
runner.dialect = scala212source3
}
}
4 changes: 2 additions & 2 deletions Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The server can signal its support for any of these via a [WWW-Authenticate](http
3. [HttpSig](https://github.com/bblfish/authentication-panel/blob/main/proposals/HttpSignature.md): the most efficient and secure authentication, best for connecting to other servers
4. UMA: as described in [Solid-OIDC](https://solidproject.org/TR/oidc-primer) is useful as a way of tying into widely deployed OAuth systems, but not efficient for highly decentralised apps.
5. Credential based - find the spec
6. Cookies: once a user is authenticated for a realm, using a cookie may be enough to continue the interaction.
6. Cookies: once a user is authenticated for a realm, using a cookie may be enough to continue the interaction. But cookie use in the browser by non-origin apps is limited and tricky.([see detailed course](https://www.youtube.com/watch?v=34wC1C61lg0))

We will get going with 1 and 3, but keeping 2 and 4 in mind will be helpful to make sure the architecture is correct. In any case the system should be extensible so that others can contribute other auth schemes without problem.

Expand Down Expand Up @@ -66,7 +66,7 @@ trait Client[F[_]]:
//...
```

This parallels the way Http4s defines server applications. As Ross Baker quipped in his [introductory talk]() ([video](https://www.youtube.com/watch?v=urdtmx4h5LE)):
This parallels the way Http4s defines server applications. As Ross Baker quipped in his introductory talk ([video](https://www.youtube.com/watch?v=urdtmx4h5LE)):

> HTTP applications are just a Kleisli function from a streaming request to a polymorphic effect of a streaming response. So what's the problem?
Expand Down
12 changes: 3 additions & 9 deletions app/src/main/scala/run/cosy/solid/app/http/Fetcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,15 @@ import org.http4s.implicits.*
import org.http4s.{ParseResult, QValue, Request, Uri, client}
import run.cosy.app.io.n3.N3Parser


import java.nio.charset.Charset
import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}

import org.w3.banana.RDF
import org.w3.banana.RDF.*
import org.w3.banana.Ops

case class Fetcher[Rdf<:RDF](store: Store[Rdf])(using ops: Ops[Rdf]) {
import ops.{given,*}


}
case class Fetcher[Rdf <: RDF](store: Store[Rdf])(using ops: Ops[Rdf]):
import ops.{given, *}

@JSExportTopLevel("Fetcher")
object Fetcher {

}
object Fetcher {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ package run.cosy.solid.app.http

// import org.http4s.headers.{Accept,*}

object RDFMediaTypes {

}
object RDFMediaTypes {}
4 changes: 1 addition & 3 deletions app/src/main/scala/run/cosy/solid/app/http/Web.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
package run.cosy.solid.app.http

class Web {

}
class Web {}
134 changes: 69 additions & 65 deletions app/src/main/scala/solidapp/Example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,68 +15,72 @@ import scala.scalajs.js.annotation.{JSExport, JSExportTopLevel}
@JSExportTopLevel("Example")
object Example:

import org.scalajs.dom
import dom.{document, html}

val clnt: client.Client[IO] = FetchClientBuilder[IO].create
// val rdfHeaders = org.http4s.Headers

import org.http4s.client.dsl.io.given
import org.http4s.headers.*

def main(args: Array[String]): Unit =
document.addEventListener("DOMContentLoaded", { (e: dom.Event) =>
println(e)
})

def setupUI(): Unit =
val button = document.createElement("button")
button.textContent = "Click me!"
button.addEventListener("click", { (e: dom.MouseEvent) =>
addClickedMessage()
})
document.body.appendChild(button)
appendPar(document.body, "Hello World")
end setupUI

@JSExport
def addClickedMessage(): Unit =
val url: ParseResult[Uri] = urlEntry()
url.fold(
fail => appendPar(document.body, "could not parse url " + fail),
uri => {
appendPar(document.body, "You clicked to fetch " + uri)
onClick(uri)
}
)

def urlEntry(): ParseResult[Uri] =
val urlStr = input.value
println("URL=" + urlStr)
Uri.fromString(urlStr)

def input: html.Input = document.getElementById("url").asInstanceOf[html.Input]

def onClick(uri: Uri): Unit =
val utfStr: fs2.Stream[cats.effect.IO, String] = clnt.stream(req(uri)).flatMap(_.body)
.through(text.utf8.decode)

val ios: fs2.Stream[cats.effect.IO, INothing] = utfStr.through(N3Parser.parse)
.chunks.foreach { chunk =>
IO(appendPar(document.body, s"chunk size ${chunk.size} starts with ${chunk.head}"))
}

ios.compile.lastOrError.unsafeRunAsync {
case Left(err) => appendPar(document.body, err.toString)
case Right(answer) => appendPar(document.body, "good answer")
}
end onClick

def req(uri: Uri): Request[IO] = GET(uri, Accept(turtle.withQValue(QValue.One)))

def appendPar(targetNode: dom.Node, text: String): Unit =
val parNode = document.createElement("p")
parNode.textContent = text
targetNode.appendChild(parNode)

end Example
import org.scalajs.dom
import dom.{document, html}

val clnt: client.Client[IO] = FetchClientBuilder[IO].create
// val rdfHeaders = org.http4s.Headers

import org.http4s.client.dsl.io.given
import org.http4s.headers.*

def main(args: Array[String]): Unit = document.addEventListener(
"DOMContentLoaded",
{ (e: dom.Event) =>
println(e)
}
)

def setupUI(): Unit =
val button = document.createElement("button")
button.textContent = "Click me!"
button.addEventListener(
"click",
{ (e: dom.MouseEvent) =>
addClickedMessage()
}
)
document.body.appendChild(button)
appendPar(document.body, "Hello World")
end setupUI

@JSExport
def addClickedMessage(): Unit =
val url: ParseResult[Uri] = urlEntry()
url.fold(
fail => appendPar(document.body, "could not parse url " + fail),
uri =>
appendPar(document.body, "You clicked to fetch " + uri)
onClick(uri)
)

def urlEntry(): ParseResult[Uri] =
val urlStr = input.value
println("URL=" + urlStr)
Uri.fromString(urlStr)

def input: html.Input = document.getElementById("url").asInstanceOf[html.Input]

def onClick(uri: Uri): Unit =
val utfStr: fs2.Stream[cats.effect.IO, String] = clnt.stream(req(uri)).flatMap(_.body)
.through(text.utf8.decode)

val ios: fs2.Stream[cats.effect.IO, INothing] = utfStr.through(N3Parser.parse).chunks
.foreach { chunk =>
IO(appendPar(document.body, s"chunk size ${chunk.size} starts with ${chunk.head}"))
}

ios.compile.lastOrError.unsafeRunAsync {
case Left(err) => appendPar(document.body, err.toString)
case Right(answer) => appendPar(document.body, "good answer")
}
end onClick

def req(uri: Uri): Request[IO] = GET(uri, Accept(turtle.withQValue(QValue.One)))

def appendPar(targetNode: dom.Node, text: String): Unit =
val parNode = document.createElement("p")
parNode.textContent = text
targetNode.appendChild(parNode)

end Example
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,9 @@ import run.cosy.rdfjs.model.Quad

import scala.scalajs.js

class FetcherMunitTests extends munit.FunSuite:
Fetcher.setupUI()

class FetcherMunitTests extends munit.FunSuite {
Fetcher.setupUI()

test("HelloWorld") {
assert(document.querySelectorAll("p").count(_.textContent == "Hello World") == 1)
}

}
test("HelloWorld") {
assert(document.querySelectorAll("p").count(_.textContent == "Hello World") == 1)
}
51 changes: 30 additions & 21 deletions authn/shared/src/main/scala/net/bblfish/app/auth/AuthNClient.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Typelevel
* Copyright 2021 bblfish.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,34 +34,43 @@ import org.http4s.{BasicCredentials, Header, Request, Response, Status}
import scala.util.{Failure, Success, Try}

/** Client Authentication is a middleware that transforms a Client into a new Client that can use a
* Wallet to have requests signed.
* Wallet to have requests signed. It will try to sign a request
* 1. before it is sent using information it has available from the local cache on the server. So
* it will try to find an relevant ACL that it can use to determine if it can sign something
* 1. if the server returns a 401 it will use the response info to fetch the ACL rules and if it
* can, sign the request
*/
object AuthNClient:
def apply[F[_]: Concurrent](wallet: Wallet[F])(
client: Client[F]
): Client[F] =
def apply[F[_]: Concurrent](wallet: Wallet[F])(
client: Client[F]
): Client[F] =

def authLoop(
req: Request[F],
attempts: Int,
hotswap: Hotswap[F, Response[F]]
): F[Response[F]] =
hotswap.clear *> // Release the prior connection before allocating a new
def authLoop(
req: Request[F],
attempts: Int,
hotswap: Hotswap[F, Response[F]]
): F[Response[F]] = hotswap.clear *> // Release the prior connection before allocating a new
// todo: we should enhance the req with a signature if we already have info on the server
hotswap.swap(client.run(req)).flatMap { (resp: Response[F]) =>
// todo: may want a lot more flexibility than attempt numbering to determine if we should retry or not.
resp.status match
case Status.Unauthorized if attempts < 1 =>
wallet.sign(resp, req).flatMap(newReq => authLoop(newReq, attempts + 1, hotswap))
case _ => resp.pure[F]
case Status.Unauthorized if attempts < 1 =>
wallet.sign(resp, req).flatMap(newReq => authLoop(newReq, attempts + 1, hotswap))
case _ => resp.pure[F]
}

Client { req =>
// using the pattern from FollowRedirect example using Hotswap.
// Not 100% sure this is so much needed here...
Hotswap.create[F, Response[F]].flatMap { hotswap =>
Resource.eval(authLoop(req, 0, hotswap))
Client { req =>
// using the pattern from FollowRedirect example using Hotswap.
// Not 100% sure this is so much needed here...
Hotswap.create[F, Response[F]].flatMap { hotswap =>
Resource.eval(
wallet.signFromDB(req).map {
case Right(signedReq) => signedReq
case Left(_) => req
}.flatMap(req => authLoop(req, 0, hotswap))
)
}
}
}
end apply
end apply

end AuthNClient
Loading

0 comments on commit abbac16

Please sign in to comment.