Monorepo for Tangled tangled.org

add server policies through the firehose

Changed files
+205 -159
api
appview
cmd
knotserver
knotserver
lexicons
rbac
+65 -75
api/tangled/cbor_gen.go
··· 216 217 return nil 218 } 219 - func (t *KnotPolicy) MarshalCBOR(w io.Writer) error { 220 if t == nil { 221 _, err := w.Write(cbg.CborNull) 222 return err 223 } 224 225 cw := cbg.NewCborWriter(w) 226 227 - if _, err := cw.Write([]byte{165}); err != nil { 228 return err 229 } 230 ··· 240 return err 241 } 242 243 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.knot.policy"))); err != nil { 244 - return err 245 - } 246 - if _, err := cw.WriteString(string("sh.tangled.knot.policy")); err != nil { 247 return err 248 } 249 - 250 - // t.Action (string) (string) 251 - if len("action") > 1000000 { 252 - return xerrors.Errorf("Value in field \"action\" was too long") 253 - } 254 - 255 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("action"))); err != nil { 256 - return err 257 - } 258 - if _, err := cw.WriteString(string("action")); err != nil { 259 - return err 260 - } 261 - 262 - if len(t.Action) > 1000000 { 263 - return xerrors.Errorf("Value in field t.Action was too long") 264 - } 265 - 266 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Action))); err != nil { 267 - return err 268 - } 269 - if _, err := cw.WriteString(string(t.Action)); err != nil { 270 return err 271 } 272 ··· 293 return err 294 } 295 296 - // t.Object (string) (string) 297 - if len("object") > 1000000 { 298 - return xerrors.Errorf("Value in field \"object\" was too long") 299 } 300 301 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("object"))); err != nil { 302 return err 303 } 304 - if _, err := cw.WriteString(string("object")); err != nil { 305 return err 306 } 307 308 - if len(t.Object) > 1000000 { 309 - return xerrors.Errorf("Value in field t.Object was too long") 310 } 311 312 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Object))); err != nil { 313 return err 314 } 315 - if _, err := cw.WriteString(string(t.Object)); err != nil { 316 return err 317 } 318 319 - // t.Subject (string) (string) 320 - if len("subject") > 1000000 { 321 - return xerrors.Errorf("Value in field \"subject\" was too long") 322 - } 323 324 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 325 - return err 326 - } 327 - if _, err := cw.WriteString(string("subject")); err != nil { 328 - return err 329 - } 330 331 - if len(t.Subject) > 1000000 { 332 - return xerrors.Errorf("Value in field t.Subject was too long") 333 - } 334 335 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 336 - return err 337 - } 338 - if _, err := cw.WriteString(string(t.Subject)); err != nil { 339 - return err 340 } 341 return nil 342 } 343 344 - func (t *KnotPolicy) UnmarshalCBOR(r io.Reader) (err error) { 345 - *t = KnotPolicy{} 346 347 cr := cbg.NewCborReader(r) 348 ··· 361 } 362 363 if extra > cbg.MaxLength { 364 - return fmt.Errorf("KnotPolicy: map struct too large (%d)", extra) 365 } 366 367 n := extra ··· 393 394 t.LexiconTypeID = string(sval) 395 } 396 - // t.Action (string) (string) 397 - case "action": 398 - 399 - { 400 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 401 - if err != nil { 402 - return err 403 - } 404 - 405 - t.Action = string(sval) 406 - } 407 // t.Domain (string) (string) 408 case "domain": 409 ··· 415 416 t.Domain = string(sval) 417 } 418 - // t.Object (string) (string) 419 - case "object": 420 421 { 422 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 424 return err 425 } 426 427 - t.Object = string(sval) 428 } 429 - // t.Subject (string) (string) 430 - case "subject": 431 432 { 433 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 434 if err != nil { 435 return err 436 } 437 438 - t.Subject = string(sval) 439 } 440 441 default:
··· 216 217 return nil 218 } 219 + func (t *KnotMember) MarshalCBOR(w io.Writer) error { 220 if t == nil { 221 _, err := w.Write(cbg.CborNull) 222 return err 223 } 224 225 cw := cbg.NewCborWriter(w) 226 + fieldCount := 4 227 228 + if t.AddedAt == nil { 229 + fieldCount-- 230 + } 231 + 232 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 233 return err 234 } 235 ··· 245 return err 246 } 247 248 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.knot.member"))); err != nil { 249 return err 250 } 251 + if _, err := cw.WriteString(string("sh.tangled.knot.member")); err != nil { 252 return err 253 } 254 ··· 275 return err 276 } 277 278 + // t.Member (string) (string) 279 + if len("member") > 1000000 { 280 + return xerrors.Errorf("Value in field \"member\" was too long") 281 } 282 283 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("member"))); err != nil { 284 return err 285 } 286 + if _, err := cw.WriteString(string("member")); err != nil { 287 return err 288 } 289 290 + if len(t.Member) > 1000000 { 291 + return xerrors.Errorf("Value in field t.Member was too long") 292 } 293 294 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Member))); err != nil { 295 return err 296 } 297 + if _, err := cw.WriteString(string(t.Member)); err != nil { 298 return err 299 } 300 301 + // t.AddedAt (string) (string) 302 + if t.AddedAt != nil { 303 304 + if len("addedAt") > 1000000 { 305 + return xerrors.Errorf("Value in field \"addedAt\" was too long") 306 + } 307 308 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("addedAt"))); err != nil { 309 + return err 310 + } 311 + if _, err := cw.WriteString(string("addedAt")); err != nil { 312 + return err 313 + } 314 315 + if t.AddedAt == nil { 316 + if _, err := cw.Write(cbg.CborNull); err != nil { 317 + return err 318 + } 319 + } else { 320 + if len(*t.AddedAt) > 1000000 { 321 + return xerrors.Errorf("Value in field t.AddedAt was too long") 322 + } 323 + 324 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.AddedAt))); err != nil { 325 + return err 326 + } 327 + if _, err := cw.WriteString(string(*t.AddedAt)); err != nil { 328 + return err 329 + } 330 + } 331 } 332 return nil 333 } 334 335 + func (t *KnotMember) UnmarshalCBOR(r io.Reader) (err error) { 336 + *t = KnotMember{} 337 338 cr := cbg.NewCborReader(r) 339 ··· 352 } 353 354 if extra > cbg.MaxLength { 355 + return fmt.Errorf("KnotMember: map struct too large (%d)", extra) 356 } 357 358 n := extra ··· 384 385 t.LexiconTypeID = string(sval) 386 } 387 // t.Domain (string) (string) 388 case "domain": 389 ··· 395 396 t.Domain = string(sval) 397 } 398 + // t.Member (string) (string) 399 + case "member": 400 401 { 402 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 404 return err 405 } 406 407 + t.Member = string(sval) 408 } 409 + // t.AddedAt (string) (string) 410 + case "addedAt": 411 412 { 413 + b, err := cr.ReadByte() 414 if err != nil { 415 return err 416 } 417 + if b != cbg.CborNull[0] { 418 + if err := cr.UnreadByte(); err != nil { 419 + return err 420 + } 421 422 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 423 + if err != nil { 424 + return err 425 + } 426 + 427 + t.AddedAt = (*string)(&sval) 428 + } 429 } 430 431 default:
+25
api/tangled/knotmember.go
···
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.knot.member 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + KnotMemberNSID = "sh.tangled.knot.member" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("sh.tangled.knot.member", &KnotMember{}) 17 + } // 18 + // RECORDTYPE: KnotMember 19 + type KnotMember struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.knot.member" cborgen:"$type,const=sh.tangled.knot.member"` 21 + AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 22 + // domain: domain that this member now belongs to 23 + Domain string `json:"domain" cborgen:"domain"` 24 + Member string `json:"member" cborgen:"member"` 25 + }
-29
api/tangled/knotpolicy.go
··· 1 - // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 - 3 - package tangled 4 - 5 - // schema: sh.tangled.knot.policy 6 - 7 - import ( 8 - "github.com/bluesky-social/indigo/lex/util" 9 - ) 10 - 11 - const ( 12 - KnotPolicyNSID = "sh.tangled.knot.policy" 13 - ) 14 - 15 - func init() { 16 - util.RegisterType("sh.tangled.knot.policy", &KnotPolicy{}) 17 - } // 18 - // RECORDTYPE: KnotPolicy 19 - type KnotPolicy struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.knot.policy" cborgen:"$type,const=sh.tangled.knot.policy"` 21 - // action: action associated with the key 22 - Action string `json:"action" cborgen:"action"` 23 - // domain: domain of the key 24 - Domain string `json:"domain" cborgen:"domain"` 25 - // object: object associated with the key 26 - Object string `json:"object" cborgen:"object"` 27 - // subject: subject of the key 28 - Subject string `json:"subject" cborgen:"subject"` 29 - }
···
+1
appview/consts.go
··· 11 SessionExpiry = "expiry" 12 SessionAuthenticated = "authenticated" 13 TimeLayout = "2006-01-02 15:04:05.999999999 -0700 MST" 14 )
··· 11 SessionExpiry = "expiry" 12 SessionAuthenticated = "authenticated" 13 TimeLayout = "2006-01-02 15:04:05.999999999 -0700 MST" 14 + SqliteDbPath = "appview.db" 15 )
+3 -4
appview/state/rbac.go rbac/rbac.go
··· 1 - package state 2 3 import ( 4 "database/sql" ··· 37 return matched 38 } 39 40 - func NewEnforcer() (*Enforcer, error) { 41 m, err := model.NewModelFromString(Model) 42 if err != nil { 43 return nil, err 44 } 45 46 - // TODO: conf this 47 - db, err := sql.Open("sqlite3", "appview.db") 48 if err != nil { 49 return nil, err 50 }
··· 1 + package rbac 2 3 import ( 4 "database/sql" ··· 37 return matched 38 } 39 40 + func NewEnforcer(path string) (*Enforcer, error) { 41 m, err := model.NewModelFromString(Model) 42 if err != nil { 43 return nil, err 44 } 45 46 + db, err := sql.Open("sqlite3", path) 47 if err != nil { 48 return nil, err 49 }
+35 -11
appview/state/state.go
··· 22 "github.com/sotangled/tangled/appview/auth" 23 "github.com/sotangled/tangled/appview/db" 24 "github.com/sotangled/tangled/appview/pages" 25 ) 26 27 type State struct { 28 db *db.DB 29 auth *auth.Auth 30 - enforcer *Enforcer 31 } 32 33 func Make() (*State, error) { 34 - db, err := db.Make("appview.db") 35 if err != nil { 36 return nil, err 37 } ··· 41 return nil, err 42 } 43 44 - enforcer, err := NewEnforcer() 45 if err != nil { 46 return nil, err 47 } ··· 175 Collection: tangled.PublicKeyNSID, 176 Repo: did, 177 Rkey: uuid.New().String(), 178 - Record: &lexutil.LexiconTypeDecoder{Val: &tangled.PublicKey{ 179 - Created: time.Now().String(), 180 - Key: key, 181 - Name: name, 182 - }}, 183 }) 184 - 185 // invalid record 186 if err != nil { 187 log.Printf("failed to create record: %s", err) ··· 382 w.Write([]byte("failed to resolve member did to a handle")) 383 return 384 } 385 - 386 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 387 388 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 389 if err != nil { 390 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) ··· 481 r.Post("/key", s.RegistrationKey) 482 483 r.Route("/{domain}", func(r chi.Router) { 484 - r.Get("/", s.KnotServerInfo) 485 r.Post("/init", s.InitKnotServer) 486 r.Route("/member", func(r chi.Router) { 487 r.Use(RoleMiddleware(s, "server:owner")) 488 r.Get("/", s.ListMembers)
··· 22 "github.com/sotangled/tangled/appview/auth" 23 "github.com/sotangled/tangled/appview/db" 24 "github.com/sotangled/tangled/appview/pages" 25 + "github.com/sotangled/tangled/rbac" 26 ) 27 28 type State struct { 29 db *db.DB 30 auth *auth.Auth 31 + enforcer *rbac.Enforcer 32 } 33 34 func Make() (*State, error) { 35 + 36 + db, err := db.Make(appview.SqliteDbPath) 37 if err != nil { 38 return nil, err 39 } ··· 43 return nil, err 44 } 45 46 + enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath) 47 if err != nil { 48 return nil, err 49 } ··· 177 Collection: tangled.PublicKeyNSID, 178 Repo: did, 179 Rkey: uuid.New().String(), 180 + Record: &lexutil.LexiconTypeDecoder{ 181 + Val: &tangled.PublicKey{ 182 + Created: time.Now().Format(time.RFC3339), 183 + Key: key, 184 + Name: name, 185 + }}, 186 }) 187 // invalid record 188 if err != nil { 189 log.Printf("failed to create record: %s", err) ··· 384 w.Write([]byte("failed to resolve member did to a handle")) 385 return 386 } 387 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 388 389 + // announce this relation into the firehose, store into owners' pds 390 + client, _ := s.auth.AuthorizedClient(r) 391 + currentUser := s.auth.GetUser(r) 392 + addedAt := time.Now().Format(time.RFC3339) 393 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 394 + Collection: tangled.KnotMemberNSID, 395 + Repo: currentUser.Did, 396 + Rkey: uuid.New().String(), 397 + Record: &lexutil.LexiconTypeDecoder{ 398 + Val: &tangled.KnotMember{ 399 + Member: memberIdent.DID.String(), 400 + Domain: domain, 401 + AddedAt: &addedAt, 402 + }}, 403 + }) 404 + // invalid record 405 + if err != nil { 406 + log.Printf("failed to create record: %s", err) 407 + return 408 + } 409 + 410 + log.Println("created atproto record: ", resp.Uri) 411 + 412 err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 413 if err != nil { 414 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) ··· 505 r.Post("/key", s.RegistrationKey) 506 507 r.Route("/{domain}", func(r chi.Router) { 508 r.Post("/init", s.InitKnotServer) 509 + r.Get("/", s.KnotServerInfo) 510 r.Route("/member", func(r chi.Router) { 511 r.Use(RoleMiddleware(s, "server:owner")) 512 r.Get("/", s.ListMembers)
+1 -1
cmd/gen.go
··· 15 "api/tangled/cbor_gen.go", 16 "tangled", 17 shtangled.PublicKey{}, 18 - shtangled.KnotPolicy{}, 19 ); err != nil { 20 panic(err) 21 }
··· 15 "api/tangled/cbor_gen.go", 16 "tangled", 17 shtangled.PublicKey{}, 18 + shtangled.KnotMember{}, 19 ); err != nil { 20 panic(err) 21 }
+7 -1
cmd/knotserver/main.go
··· 11 "github.com/sotangled/tangled/knotserver" 12 "github.com/sotangled/tangled/knotserver/config" 13 "github.com/sotangled/tangled/knotserver/db" 14 ) 15 16 func main() { ··· 34 log.Fatalf("failed to setup db: %s", err) 35 } 36 37 - mux, err := knotserver.Setup(ctx, c, db) 38 if err != nil { 39 log.Fatal(err) 40 }
··· 11 "github.com/sotangled/tangled/knotserver" 12 "github.com/sotangled/tangled/knotserver/config" 13 "github.com/sotangled/tangled/knotserver/db" 14 + "github.com/sotangled/tangled/rbac" 15 ) 16 17 func main() { ··· 35 log.Fatalf("failed to setup db: %s", err) 36 } 37 38 + e, err := rbac.NewEnforcer(c.Server.DBPath) 39 + if err != nil { 40 + log.Fatalf("failed to setup rbac enforcer: %s", err) 41 + } 42 + 43 + mux, err := knotserver.Setup(ctx, c, db, e) 44 if err != nil { 45 log.Fatal(err) 46 }
+26 -4
knotserver/handler.go
··· 12 "github.com/sotangled/tangled/knotserver/config" 13 "github.com/sotangled/tangled/knotserver/db" 14 "github.com/sotangled/tangled/knotserver/jsclient" 15 ) 16 17 type Handle struct { 18 c *config.Config 19 db *db.DB 20 js *jsclient.JetstreamClient 21 22 // init is a channel that is closed when the knot has been initailized 23 // i.e. when the first user (knot owner) has been added. ··· 25 knotInitialized bool 26 } 27 28 - func Setup(ctx context.Context, c *config.Config, db *db.DB) (http.Handler, error) { 29 r := chi.NewRouter() 30 31 h := Handle{ 32 c: c, 33 db: db, 34 init: make(chan struct{}), 35 } 36 37 - err := h.StartJetstream(ctx) 38 if err != nil { 39 return nil, fmt.Errorf("failed to start jetstream: %w", err) 40 } ··· 94 } 95 96 func (h *Handle) StartJetstream(ctx context.Context) error { 97 - colections := []string{tangled.PublicKeyNSID} 98 dids := []string{} 99 100 - h.js = jsclient.NewJetstreamClient(colections, dids) 101 messages, err := h.js.ReadJetstream(ctx) 102 if err != nil { 103 return fmt.Errorf("failed to read from jetstream: %w", err) ··· 126 log.Printf("failed to add public key: %v", err) 127 } else { 128 log.Printf("added public key from firehose: %s", data["did"]) 129 } 130 default: 131 }
··· 12 "github.com/sotangled/tangled/knotserver/config" 13 "github.com/sotangled/tangled/knotserver/db" 14 "github.com/sotangled/tangled/knotserver/jsclient" 15 + "github.com/sotangled/tangled/rbac" 16 + ) 17 + 18 + const ( 19 + ThisServer = "thisserver" // resource identifier for rbac enforcement 20 ) 21 22 type Handle struct { 23 c *config.Config 24 db *db.DB 25 js *jsclient.JetstreamClient 26 + e *rbac.Enforcer 27 28 // init is a channel that is closed when the knot has been initailized 29 // i.e. when the first user (knot owner) has been added. ··· 31 knotInitialized bool 32 } 33 34 + func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer) (http.Handler, error) { 35 r := chi.NewRouter() 36 37 h := Handle{ 38 c: c, 39 db: db, 40 + e: e, 41 init: make(chan struct{}), 42 } 43 44 + err := e.AddDomain(ThisServer) 45 + if err != nil { 46 + return nil, fmt.Errorf("failed to setup enforcer: %w", err) 47 + } 48 + 49 + err = h.StartJetstream(ctx) 50 if err != nil { 51 return nil, fmt.Errorf("failed to start jetstream: %w", err) 52 } ··· 106 } 107 108 func (h *Handle) StartJetstream(ctx context.Context) error { 109 + collections := []string{tangled.PublicKeyNSID, tangled.KnotMemberNSID} 110 dids := []string{} 111 112 + h.js = jsclient.NewJetstreamClient(collections, dids) 113 messages, err := h.js.ReadJetstream(ctx) 114 if err != nil { 115 return fmt.Errorf("failed to read from jetstream: %w", err) ··· 138 log.Printf("failed to add public key: %v", err) 139 } else { 140 log.Printf("added public key from firehose: %s", data["did"]) 141 + } 142 + case tangled.KnotMemberNSID: 143 + did := data["did"].(string) 144 + record := commit["record"].(map[string]interface{}) 145 + ok, err := h.e.E.Enforce(did, ThisServer, ThisServer, "server:invite") 146 + if err != nil || !ok { 147 + log.Printf("failed to add member from did %s", did) 148 + } else { 149 + log.Printf("adding member") 150 + h.e.AddMember(ThisServer, record["member"].(string)) 151 } 152 default: 153 }
+8
knotserver/jsclient/jetstream.go
··· 55 j.triggerReconnect() 56 } 57 58 func (j *JetstreamClient) triggerReconnect() { 59 select { 60 case j.reconnectCh <- struct{}{}:
··· 55 j.triggerReconnect() 56 } 57 58 + // Adds one did to the did list 59 + func (j *JetstreamClient) AddDid(did string) { 60 + j.mu.Lock() 61 + j.dids = append(j.dids, did) 62 + j.mu.Unlock() 63 + j.triggerReconnect() 64 + } 65 + 66 func (j *JetstreamClient) triggerReconnect() { 67 select { 68 case j.reconnectCh <- struct{}{}:
+1
knotserver/routes.go
··· 432 } 433 434 h.js.UpdateDids([]string{data.Did}) 435 // Signal that the knot is ready 436 close(h.init) 437
··· 432 } 433 434 h.js.UpdateDids([]string{data.Did}) 435 + h.e.AddOwner(ThisServer, data.Did) 436 // Signal that the knot is ready 437 close(h.init) 438
+33
lexicons/member.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.knot.member", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "member", 14 + "domain" 15 + ], 16 + "properties": { 17 + "member": { 18 + "type": "string", 19 + "format": "did" 20 + }, 21 + "domain": { 22 + "type": "string", 23 + "description": "domain that this member now belongs to" 24 + }, 25 + "addedAt": { 26 + "type": "string", 27 + "format": "datetime" 28 + } 29 + } 30 + } 31 + } 32 + } 33 + }
-34
lexicons/policy.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.knot.policy", 4 - "needsCbor": true, 5 - "needsType": true, 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": ["subject", "domain", "object", "action"], 13 - "properties": { 14 - "subject": { 15 - "type": "string", 16 - "description": "subject of the key" 17 - }, 18 - "domain": { 19 - "type": "string", 20 - "description": "domain of the key" 21 - }, 22 - "object": { 23 - "type": "string", 24 - "description": "object associated with the key" 25 - }, 26 - "action": { 27 - "type": "string", 28 - "description": "action associated with the key" 29 - } 30 - } 31 - } 32 - } 33 - } 34 - }
···