forked from tangled.org/core
this repo has no description

refactor signed client

Changed files
+176 -43
appview
knotserver
rbac
+93 -9
appview/state/signer.go
···
package state
import (
+
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
+
"encoding/json"
+
"fmt"
"net/http"
+
"net/url"
"time"
)
type SignerTransport struct {
Secret string
-
}
-
-
func SignedClient(secret string) *http.Client {
-
return &http.Client{
-
Timeout: 5 * time.Second,
-
Transport: SignerTransport{
-
Secret: secret,
-
},
-
}
}
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
···
req.Header.Set("X-Timestamp", timestamp)
return http.DefaultTransport.RoundTrip(req)
}
+
+
type SignedClient struct {
+
Secret string
+
Url *url.URL
+
client *http.Client
+
}
+
+
func NewSignedClient(domain, secret string) (*SignedClient, error) {
+
client := &http.Client{
+
Timeout: 5 * time.Second,
+
Transport: SignerTransport{
+
Secret: secret,
+
},
+
}
+
+
url, err := url.Parse(fmt.Sprintf("http://%s", domain))
+
if err != nil {
+
return nil, err
+
}
+
+
signedClient := &SignedClient{
+
Secret: secret,
+
client: client,
+
Url: url,
+
}
+
+
return signedClient, nil
+
}
+
+
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
+
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
+
}
+
+
func (s *SignedClient) Init(did string, keys []string) (*http.Response, error) {
+
const (
+
Method = "POST"
+
Endpoint = "/init"
+
)
+
+
body, _ := json.Marshal(map[string]interface{}{
+
"did": did,
+
"keys": keys,
+
})
+
+
req, err := s.newRequest(Method, Endpoint, body)
+
if err != nil {
+
return nil, err
+
}
+
+
return s.client.Do(req)
+
}
+
+
func (s *SignedClient) NewRepo(did, repoName string) (*http.Response, error) {
+
const (
+
Method = "PUT"
+
Endpoint = "/repo/new"
+
)
+
+
body, _ := json.Marshal(map[string]interface{}{
+
"did": did,
+
"name": repoName,
+
})
+
+
req, err := s.newRequest(Method, Endpoint, body)
+
if err != nil {
+
return nil, err
+
}
+
+
return s.client.Do(req)
+
}
+
+
func (s *SignedClient) AddMember(did string, keys []string) (*http.Response, error) {
+
const (
+
Method = "PUT"
+
Endpoint = "/member/add"
+
)
+
+
body, _ := json.Marshal(map[string]interface{}{
+
"did": did,
+
"keys": keys,
+
})
+
+
req, err := s.newRequest(Method, Endpoint, body)
+
if err != nil {
+
return nil, err
+
}
+
+
return s.client.Do(req)
+
}
+43 -30
appview/state/state.go
···
package state
import (
-
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
-
"encoding/json"
"fmt"
"log"
"net/http"
+
"path/filepath"
"strings"
"time"
···
}
log.Println("checking ", domain)
-
url := fmt.Sprintf("http://%s/init", domain)
-
-
body, _ := json.Marshal(map[string]interface{}{
-
"did": user.Did,
-
"keys": []string{},
-
})
-
pingRequest, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
+
secret, err := s.db.GetRegistrationKey(domain)
if err != nil {
-
log.Println("failed to build ping request", err)
+
log.Printf("no key found for domain %s: %s\n", domain, err)
return
}
-
secret, err := s.db.GetRegistrationKey(domain)
+
client, err := NewSignedClient(domain, secret)
if err != nil {
-
log.Printf("no key found for domain %s: %s\n", domain, err)
-
return
+
log.Println("failed to create client to ", domain)
}
-
client := SignedClient(secret)
-
resp, err := client.Do(pingRequest)
+
resp, err := client.Init(user.Did, []string{})
if err != nil {
w.Write([]byte("no dice"))
log.Println("domain was unreachable after 5 seconds")
···
var members []string
if reg.Registered != nil {
-
members, err = s.enforcer.E.GetUsersForRole("server:member", domain)
+
members, err = s.enforcer.GetUserByRole("server:member", domain)
if err != nil {
w.Write([]byte("failed to fetch member list"))
return
}
}
-
ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain)
+
ok, err := s.enforcer.IsServerOwner(user.Did, domain)
isOwner := err == nil && ok
p := pages.KnotParams{
···
}
// list all members for this domain
-
memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain)
+
memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
if err != nil {
w.Write([]byte("failed to fetch member list"))
return
···
log.Printf("failed to create record: %s", err)
return
}
+
log.Println("created atproto record: ", resp.Uri)
-
log.Println("created atproto record: ", resp.Uri)
+
secret, err := s.db.GetRegistrationKey(domain)
+
if err != nil {
+
log.Printf("no key found for domain %s: %s\n", domain, err)
+
return
+
}
+
+
ksClient, err := NewSignedClient(domain, secret)
+
if err != nil {
+
log.Println("failed to create client to ", domain)
+
return
+
}
+
+
ksResp, err := ksClient.AddMember(memberIdent.DID.String(), []string{})
+
if err != nil {
+
log.Printf("failet to make request to %s: %s", domain, err)
+
}
+
+
if ksResp.StatusCode != http.StatusNoContent {
+
w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
+
return
+
}
err = s.enforcer.AddMember(domain, memberIdent.DID.String())
if err != nil {
···
return
}
-
client := SignedClient(secret)
-
url := fmt.Sprintf("http://%s/repo/new", domain)
-
body, _ := json.Marshal(map[string]interface{}{
-
"did": user.Did,
-
"name": repoName,
-
})
-
createRepoRequest, err := http.NewRequest("PUT", url, bytes.NewReader(body))
+
client, err := NewSignedClient(domain, secret)
+
if err != nil {
+
log.Println("failed to create client to ", domain)
+
}
-
resp, err := client.Do(createRepoRequest)
-
+
resp, err := client.NewRepo(user.Did, repoName)
if err != nil {
log.Println("failed to send create repo request", err)
return
}
-
if resp.StatusCode != http.StatusNoContent {
log.Println("server returned ", resp.StatusCode)
return
···
Name: repoName,
Knot: domain,
}
-
err = s.db.AddRepo(repo)
if err != nil {
log.Println("failed to add repo to db", err)
+
return
+
}
+
+
// acls
+
err = s.enforcer.AddRepo(user.Did, domain, filepath.Join(user.Did, repoName))
+
if err != nil {
+
log.Println("failed to set up acls", err)
return
}
+1 -1
knotserver/handler.go
···
r.Route("/member", func(r chi.Router) {
r.Use(h.VerifySignature)
-
r.Put("/add", h.NewRepo)
+
r.Put("/add", h.AddMember)
})
// Initialize the knot with an owner and public key.
+8 -3
knotserver/jetstream.go
···
"io"
"log"
"net/http"
-
"path"
+
"net/url"
"strings"
"time"
···
}
func (h *Handle) fetchAndAddKeys(did string) {
-
resp, err := http.Get(path.Join(h.c.AppViewEndpoint, did))
+
keysEndpoint, err := url.JoinPath(h.c.AppViewEndpoint, "keys", did)
+
if err != nil {
+
log.Printf("error building endpoint url: %s: %v", did, err)
+
return
+
}
+
+
resp, err := http.Get(keysEndpoint)
if err != nil {
log.Printf("error getting keys for %s: %v", did, err)
return
···
}
func (h *Handle) processMessages(messages <-chan []byte) {
-
log.Println("waiting for knot to be initialized")
<-h.init
log.Println("initalized jetstream watcher")
+31
rbac/rbac.go
···
import (
"database/sql"
"path"
+
"strings"
sqladapter "github.com/Blank-Xu/sql-adapter"
"github.com/casbin/casbin/v2"
···
{"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo
})
return err
+
}
+
+
func (e *Enforcer) GetUserByRole(role, domain string) ([]string, error) {
+
var membersWithoutRoles []string
+
+
// this includes roles too, casbin does not differentiate.
+
// the filtering criteria is to remove strings not starting with `did:`
+
members, err := e.E.Enforcer.GetImplicitUsersForRole(role, domain)
+
for _, m := range members {
+
if strings.HasPrefix(m, "did:") {
+
membersWithoutRoles = append(membersWithoutRoles, m)
+
}
+
}
+
if err != nil {
+
return nil, err
+
}
+
+
return membersWithoutRoles, nil
+
}
+
+
func (e *Enforcer) isRole(user, role, domain string) (bool, error) {
+
return e.E.HasGroupingPolicy(user, role, domain)
+
}
+
+
func (e *Enforcer) IsServerOwner(user, domain string) (bool, error) {
+
return e.isRole(user, "server:owner", domain)
+
}
+
+
func (e *Enforcer) IsServerMember(user, domain string) (bool, error) {
+
return e.isRole(user, "server:member", domain)
}
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin